npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@eventual-ai/mcap-viewer

v0.1.0-alpha.5

Published

Browser playback of native H.264/H.265 video and image annotations from streamed MCAP recordings.

Readme

@eventual-ai/mcap-viewer

A TypeScript browser SDK for playing native H.264/H.265 video and timestamped samples from streamed MCAP recordings. /core owns playback and independent renderers; /data-loader handles the MCAP Delivery API (MDA). The package root exports both.

npm install @eventual-ai/mcap-viewer@alpha
import {
  McapRecording,
  Player,
  ImageRenderer,
} from "@eventual-ai/mcap-viewer/core";

const recording = await McapRecording.open(url, { signal });
const player = new Player(recording);
const renderer = new ImageRenderer(canvas, { imageTopic: "head_left" });
player.subscribe(renderer);
await player.seek(recording.start);
await player.play();

open reads files, URLs and custom streams progressively. Messages must be ordered by logTime; unordered input fails. Do not await recording.loaded before playback. Observe loading and late errors with recording.subscribe. When unmounting, call player.close(), then renderer.destroy(), then recording.close().

A listener attached during buffering receives the current frame snapshot when playback leaves buffering. Late subscribers report snapshot failures through the player error handler; their asynchronous decoder flush does not pause existing listeners. Seeking waits for all participating listeners to finish their snapshot.

The core delivers every topic at its native timestamp. Video topics use eventual.mda.v1.CompressedVideo: native H.264/HEVC packets with their decode order, in MCAP with embedded Protobuf schemas and, for spans over 20 MiB, ZSTD-compressed chunks. Recordings can contain annotations or other samples without video. Times remain bigint nanoseconds.

import { DataLoader } from "@eventual-ai/mcap-viewer/data-loader";

const loader = new DataLoader({
  baseUrl: MDA_URL,
  headers: { Authorization: `Bearer ${token}` },
});
const topics = await loader.discover(datasource);
const [result] = await loader.manifest({
  datasource,
  topics: ["head_left"],
  spans: [{ producer, start: 10, end: 20 }],
});
const recording = await loader.openSpan(result, { signal });

Each manifest supplies one streamed MCAP URL. Payload requests omit control credentials. Playback covers the requested span, which the MCAP records in its mda metadata, so McapRecording.open on a saved file starts at the same time (bounds: null keeps the keyframe preroll). discover maps topic names to canonical schemas and actual wire encodings:

{
  "head_left": {
    "schema": "eventual.mda.v1.CompressedVideo",
    "schema_encoding": "protobuf",
    "message_encoding": "protobuf"
  }
}

capabilities lists canonical schemas with status: "live".

Video requires WebCodecs over HTTPS or localhost; available codecs depend on the browser and device. Unsupported codecs report an error through renderer.onError.

Using offline or in a sandboxed page

The core plays plain MCAP files without a backend. Open a Blob or File, a URL, or a custom McapStreamSource:

// A file the user picks or drops, or any Blob, such as one bundled with the page.
input.addEventListener("change", async () => {
  const recording = await McapRecording.open(input.files![0]);
});

// A file next to the page, when the host serves .mcap files.
const recording = await McapRecording.open(new URL("clip.mcap", location.href));

A URL is fetched without cookies or credentials, unless you open new HttpSource(url, { credentials: "include" }). In a sandboxed iframe without allow-same-origin, the page has an opaque origin, so even a same-origin URL needs CORS headers; open a Blob instead when the host does not send them.

For a host that serves only text files, store the MCAP as base64 parts and open them with Base64Source, which decodes each part as it arrives:

base64 -i clip.mcap | split -b 1000000 - clip.mcap.b64.
import { Base64Source } from "@eventual-ai/mcap-viewer/core";

const recording = await McapRecording.open(
  new Base64Source(["clip.mcap.b64.aa", "clip.mcap.b64.ab"]),
);

Base64Source fetches the parts with credentials: "same-origin", so a host that serves a page's own files only with its session still serves them. Pass { credentials } as its second argument to change that.

Opening makes no other network request: the ZSTD decompressor is WebAssembly embedded in the package and compiled from its bytes, so a Blob opens under connect-src 'none'. A Content Security Policy must allow WebAssembly compilation and, while Protobuf decoding generates code at runtime, eval:

Content-Security-Policy: script-src 'self' 'wasm-unsafe-eval' 'unsafe-eval'

Pages without a bundler can import dist/mcap-viewer.standalone.js, which holds /core and its dependencies in one ES module: copy it next to the page and import { McapRecording, Player, ImageRenderer } from "./mcap-viewer.standalone.js".

A file exported by MDA plays the requested span, recorded in its mda metadata as start_ns and an exclusive end_ns; the file starts at the preceding keyframe, which is decoded but not shown. bounds replaces that span, and bounds: null plays every message:

const span = await McapRecording.open(file); // the span in the mda metadata
const all = await McapRecording.open(file, { bounds: null }); // every message
const part = await McapRecording.open(file, {
  bounds: { start: span.start, end: span.start + 999_999_999n }, // the first second
});

TimeBounds.end is inclusive, so a span with metadata end_ns has end === end_ns - 1n. A file without mda metadata plays from its first to its last message, and its end grows while it loads.

Camera overlays and native samples

Player also delivers nonvideo topics at their native MCAP timestamps. Seek snapshots include their history over the selected video decode interval, plus the preceding sample, so overlays can follow the displayed frame through seeks and reordered video. Video dependency buffering remains unchanged.

ImageRenderer draws only video. ImageAnnotationsRenderer independently draws eventual.mda.v1.ImageAnnotations on its own canvas. Applications choose the canvas dimensions in annotation pixel coordinates and subscribe it to the desired topics. No video channel, camera metadata, or WebCodecs decoder is required:

import { ImageAnnotationsRenderer } from "@eventual-ai/mcap-viewer/core";

annotationCanvas.width = imageWidth;
annotationCanvas.height = imageHeight;
const annotations = new ImageAnnotationsRenderer(annotationCanvas);
player.subscribe(annotations, { topics: [annotationTopic] });
await player.seek(recording.start);

The annotation renderer selects the latest complete timestamp group at or before the player time. Empty messages clear the preceding annotations. maxAgeNs optionally limits how long a sample remains visible. Call destroy() when unmounting, after unsubscribing or closing the player.

Applications own composition: place the canvases side by side, stack a transparent annotation canvas over video, or show annotations alone. For a stacked view, forward annotation messages and snapshot callbacks without forwarding player state, then call annotations.draw(time) from the video's onDraw(time) hook. Match the annotation canvas dimensions to the video canvas before drawing. This aligns annotations to the frame actually drawn, including reordered video and seeks. Use the video's onClear hook to clear the annotation canvas when no frame is shown. Neither renderer owns the other; this coordination lives in the application, as shown in examples/viewer.js.

ImageAnnotationsRenderer.render(context, message) also draws primitives into an application-owned canvas without selecting samples or clearing it. This supports explicit shared-canvas composition. It supports styled dots (type: 1) and independent line segments (type: 4). The client does no projection or hand-specific geometry. Exported HandPose and HandPose2D types describe native messages for application views.

The example requests the selected video topic and only the comma-separated annotation topics entered in the form. Its episode field accepts a full named producer key as JSON, as well as an opaque string producer. DataLoader passes the key through unchanged.

The example form can be prefilled with url, datasource, recording, topic, annotations, start, and end URL query parameters. recording may contain a JSON producer key. Prefilling never reads a token parameter and does not load automatically.

CameraCalibration and FrameTransform are also exported decoded-message types. Calibration matrices retain the protobuf MatrixRow[] shape (row.values). Missing pose positions/orientations and unprojectable 2D positions remain null; optional 2D depth remains absent when unset. Schema-name constants are exported alongside these types for application dispatch without topic-name assumptions.

Text intervals

TextLabelView independently subscribes to eventual.mda.v1.TextLabel topics. Its activeLabels and onChange(labels) expose every active label with its topic, value, and complete bigint start/end bounds. The application owns rendering and layout. Activity is start <= time && time < end; overlapping and equal-time labels remain distinct. The wire TextLabel keeps start_time and end_time as decimal strings. No interval is clipped to the requested playback span.

const labels = new TextLabelView();
labels.onChange = active => renderLabels(active);
player.subscribe(labels, { topics: ["subtasks/caption", "subtasks/state_changes"] });

Seeks reconstruct all overlapping text intervals, including long intervals whose start precedes later labels. Playback expires labels at their exclusive end even when no new message arrives. reset() clears a view for reuse; destroy() ignores later callbacks. Text labels are independent of video and annotation renderers.

The example accepts explicit comma-separated label topics in its labels field or URL query parameter, defaulting to subtasks/caption. With video selected, labels appear in a separate subtitle overlay. The Subtitles button toggles its visibility without fetching or decoding again. Clear the optional video topic for labels-only playback; the separate label list uses the same player clock and controls.

Standalone annotation playback requires an explicit annotation image width and height in native pixel coordinates. Those fields appear only when annotation topics are selected without video; annotation-width and annotation-height query parameters can prefill them. The example hides the unused video canvas. Video overlays continue to size from decoded video frames, without using these standalone dimensions or fetching calibration data.

Training transport metadata

Manifest results can include content_id and expires_at (Unix seconds). These fields are optional in TypeScript for compatibility with older MDA deployments. capabilities().limits reports the maximum spans, topics, span-topic pairs and span duration when supported.

Use content_id for exact-recording cache identity, not the signed URL. It is not a checksum of the downloaded bytes. Request the same manifest again to replace an expired URL. The server resolves the current data; if content_id changes, download and cache the new recording under that ID.

HttpError, exported from both /core and /data-loader, exposes status, body and retryAfterMs on failed control or payload HTTP requests. The retry delay supports both seconds and HTTP-date Retry-After headers. The client does not retry automatically; callers can apply their own bounded backoff with jitter and cancellation. Payload requests continue to omit control credentials.

License

MIT, except the embedded Zstandard decompressor, which is BSD-3-Clause; see LICENSE.