@eventual-ai/mcap-viewer 0.0.0-stage → 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +69 -2
  3. package/dist/core/index.d.ts +7 -0
  4. package/dist/core/index.js +3 -0
  5. package/dist/core/mcap/codecs/h264.d.ts +4 -0
  6. package/dist/core/mcap/codecs/h264.js +22 -0
  7. package/dist/core/mcap/codecs/video.d.ts +6 -0
  8. package/dist/core/mcap/codecs/video.js +64 -0
  9. package/dist/core/mcap/index.d.ts +5 -0
  10. package/dist/core/mcap/index.js +2 -0
  11. package/dist/core/mcap/loading.d.ts +18 -0
  12. package/dist/core/mcap/loading.js +50 -0
  13. package/dist/core/mcap/reader.d.ts +8 -0
  14. package/dist/core/mcap/reader.js +121 -0
  15. package/dist/core/mcap/recording.d.ts +31 -0
  16. package/dist/core/mcap/recording.js +72 -0
  17. package/dist/core/mcap/source.d.ts +22 -0
  18. package/dist/core/mcap/source.js +67 -0
  19. package/dist/core/mcap/types.d.ts +36 -0
  20. package/dist/core/mcap/types.js +1 -0
  21. package/dist/core/playback/index.d.ts +2 -0
  22. package/dist/core/playback/index.js +1 -0
  23. package/dist/core/playback/player.d.ts +66 -0
  24. package/dist/core/playback/player.js +344 -0
  25. package/dist/core/playback/snapshot.d.ts +3 -0
  26. package/dist/core/playback/snapshot.js +11 -0
  27. package/dist/core/playback/video-sequence.d.ts +24 -0
  28. package/dist/core/playback/video-sequence.js +116 -0
  29. package/dist/core/renderers/image.d.ts +37 -0
  30. package/dist/core/renderers/image.js +165 -0
  31. package/dist/core/renderers/index.d.ts +2 -0
  32. package/dist/core/renderers/index.js +1 -0
  33. package/dist/core/renderers/video-decoder.d.ts +20 -0
  34. package/dist/core/renderers/video-decoder.js +146 -0
  35. package/dist/core/schemas.d.ts +19 -0
  36. package/dist/core/schemas.js +13 -0
  37. package/dist/data-loader/index.d.ts +2 -0
  38. package/dist/data-loader/index.js +1 -0
  39. package/dist/data-loader/loader.d.ts +18 -0
  40. package/dist/data-loader/loader.js +52 -0
  41. package/dist/data-loader/types.d.ts +45 -0
  42. package/dist/data-loader/types.js +1 -0
  43. package/dist/index.d.ts +2 -0
  44. package/dist/index.js +2 -0
  45. package/package.json +52 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eventual, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,70 @@
1
- # Temporary Holding Version
1
+ # @eventual-ai/mcap-viewer
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A TypeScript browser SDK for playing native H.264/H.265 video from streamed MCAP
4
+ recordings. `/core` plays MCAP video topics; `/data-loader` handles the MCAP
5
+ Delivery API (MDA). The package root exports both.
6
+
7
+ ```sh
8
+ npm install @eventual-ai/mcap-viewer@alpha
9
+ ```
10
+
11
+ ```ts
12
+ import {
13
+ McapRecording,
14
+ Player,
15
+ ImageRenderer,
16
+ } from "@eventual-ai/mcap-viewer/core";
17
+
18
+ const recording = await McapRecording.open(url, { signal });
19
+ const player = new Player(recording);
20
+ const renderer = new ImageRenderer(canvas, { imageTopic: "head_left" });
21
+ player.subscribe(renderer);
22
+ await player.seek(recording.start);
23
+ await player.play();
24
+ ```
25
+
26
+ `open` reads files, URLs and custom streams progressively. Messages must be ordered
27
+ by `logTime`; unordered input fails. Do not await `recording.loaded` before playback.
28
+ Observe loading and late errors with `recording.subscribe`; call `player.close()`,
29
+ `recording.close()` and `renderer.destroy()` when unmounting.
30
+
31
+ The core plays `eventual.mda.v1.CompressedVideo`, the only schema the service
32
+ serves: native H.264/HEVC packets with their decode order, in ZSTD-compressed MCAP
33
+ with embedded Protobuf schemas. Times remain `bigint` nanoseconds.
34
+
35
+ ```ts
36
+ import { DataLoader } from "@eventual-ai/mcap-viewer/data-loader";
37
+
38
+ const loader = new DataLoader({
39
+ baseUrl: MDA_URL,
40
+ headers: { Authorization: `Bearer ${token}` },
41
+ });
42
+ const topics = await loader.discover(datasource);
43
+ const [result] = await loader.manifest({
44
+ datasource,
45
+ topics: ["head_left"],
46
+ spans: [{ id: episodeId, start: 10, end: 20 }],
47
+ });
48
+ const recording = await loader.openSpan(result, { signal });
49
+ ```
50
+
51
+ Each manifest supplies one streamed MCAP URL. Payload requests omit control
52
+ credentials. The loader applies the requested span bounds. `discover` maps topic
53
+ names to canonical schemas and actual wire encodings:
54
+
55
+ ```json
56
+ {
57
+ "head_left": {
58
+ "schema": "eventual.mda.v1.CompressedVideo",
59
+ "schema_encoding": "protobuf",
60
+ "message_encoding": "protobuf"
61
+ }
62
+ }
63
+ ```
64
+
65
+ `capabilities` lists canonical schemas with `status: "live"`.
66
+
67
+ Video requires WebCodecs over HTTPS or localhost; available codecs depend on the
68
+ browser and device. Unsupported codecs report an error through `renderer.onError`.
69
+
70
+ MIT licensed.
@@ -0,0 +1,7 @@
1
+ export { BlobSource, HttpSource, McapRecording } from "./mcap/index.js";
2
+ export type { McapStreamSource, LoadOptions, ReadOptions, Recording, RecordingLoadState, Message, TimeBounds, Topic, } from "./mcap/index.js";
3
+ export { Player } from "./playback/index.js";
4
+ export type { PlaybackListener, PlaybackState, Scheduler, SubscriptionOptions, } from "./playback/index.js";
5
+ export { ImageRenderer } from "./renderers/index.js";
6
+ export type { ImageRendererOptions } from "./renderers/index.js";
7
+ export type { Timestamp, CompressedVideo } from "./schemas.js";
@@ -0,0 +1,3 @@
1
+ export { BlobSource, HttpSource, McapRecording } from "./mcap/index.js";
2
+ export { Player } from "./playback/index.js";
3
+ export { ImageRenderer } from "./renderers/index.js";
@@ -0,0 +1,4 @@
1
+ export declare function inspectAccessUnit(bytes: Uint8Array): {
2
+ keyframe: boolean;
3
+ codec?: string;
4
+ };
@@ -0,0 +1,22 @@
1
+ export function inspectAccessUnit(bytes) {
2
+ let keyframe = false;
3
+ let codec;
4
+ for (let index = 0; index + 3 < bytes.length; index++) {
5
+ if (bytes[index] !== 0 || bytes[index + 1] !== 0)
6
+ continue;
7
+ let header = -1;
8
+ if (bytes[index + 2] === 1)
9
+ header = index + 3;
10
+ else if (bytes[index + 2] === 0 && bytes[index + 3] === 1)
11
+ header = index + 4;
12
+ if (header < 0 || header >= bytes.length)
13
+ continue;
14
+ const kind = bytes[header] & 31;
15
+ if (kind === 5)
16
+ keyframe = true;
17
+ if (kind === 7 && header + 3 < bytes.length)
18
+ codec = `avc1.${Array.from(bytes.slice(header + 1, header + 4), (value) => value.toString(16).padStart(2, "0")).join("")}`;
19
+ index = header;
20
+ }
21
+ return { keyframe, codec };
22
+ }
@@ -0,0 +1,6 @@
1
+ interface VideoAccessUnit {
2
+ keyframe: boolean;
3
+ codec?: string;
4
+ }
5
+ export declare function inspectVideo(bytes: Uint8Array, format: string): VideoAccessUnit;
6
+ export {};
@@ -0,0 +1,64 @@
1
+ import { inspectAccessUnit } from "./h264.js";
2
+ export function inspectVideo(bytes, format) {
3
+ if (format === "h264")
4
+ return inspectAccessUnit(bytes);
5
+ if (format !== "h265")
6
+ throw new Error(`Unsupported video format: ${format}`);
7
+ let keyframe = false;
8
+ let codec;
9
+ const units = [];
10
+ let begin = -1;
11
+ for (let index = 0; index + 3 < bytes.length; index++) {
12
+ if (bytes[index] !== 0 || bytes[index + 1] !== 0)
13
+ continue;
14
+ const length = bytes[index + 2] === 1
15
+ ? 3
16
+ : bytes[index + 2] === 0 && bytes[index + 3] === 1
17
+ ? 4
18
+ : 0;
19
+ if (!length)
20
+ continue;
21
+ if (begin >= 0)
22
+ units.push(bytes.subarray(begin, index));
23
+ begin = index + length;
24
+ index = begin - 1;
25
+ }
26
+ if (begin >= 0)
27
+ units.push(bytes.subarray(begin));
28
+ const parameterSets = new Set();
29
+ for (const unit of units) {
30
+ const kind = (unit[0] >> 1) & 63;
31
+ if (kind >= 16 && kind <= 21)
32
+ keyframe = true;
33
+ if (kind >= 32 && kind <= 34)
34
+ parameterSets.add(kind);
35
+ if (kind !== 33)
36
+ continue;
37
+ const rbsp = [];
38
+ for (let index = 2; index < unit.length; index++) {
39
+ if (index >= 4 &&
40
+ unit[index] === 3 &&
41
+ unit[index - 1] === 0 &&
42
+ unit[index - 2] === 0)
43
+ continue;
44
+ rbsp.push(unit[index]);
45
+ }
46
+ if (rbsp.length < 13)
47
+ continue;
48
+ const profile = rbsp[1];
49
+ let compatibility = 0;
50
+ for (let bit = 0; bit < 32; bit++)
51
+ compatibility +=
52
+ ((rbsp[2 + (bit >> 3)] >> (7 - (bit & 7))) & 1) * 2 ** bit;
53
+ const constraints = rbsp.slice(6, 12);
54
+ while (constraints.length && constraints.at(-1) === 0)
55
+ constraints.pop();
56
+ const suffix = constraints
57
+ .map((value) => value.toString(16).toUpperCase().padStart(2, "0"))
58
+ .join(".");
59
+ codec = `hvc1.${["", "A", "B", "C"][profile >> 6]}${profile & 31}.${compatibility.toString(16).toUpperCase()}.${profile & 32 ? "H" : "L"}${rbsp[12]}${suffix ? `.${suffix}` : ""}`;
60
+ }
61
+ if (parameterSets.size !== 3)
62
+ codec = undefined;
63
+ return { keyframe, codec };
64
+ }
@@ -0,0 +1,5 @@
1
+ export { McapRecording } from "./recording.js";
2
+ export { BlobSource, HttpSource } from "./source.js";
3
+ export type { McapStreamSource, LoadOptions } from "./source.js";
4
+ export type { ReadOptions } from "./reader.js";
5
+ export type { Recording, RecordingLoadState, Message, TimeBounds, Topic, } from "./types.js";
@@ -0,0 +1,2 @@
1
+ export { McapRecording } from "./recording.js";
2
+ export { BlobSource, HttpSource } from "./source.js";
@@ -0,0 +1,18 @@
1
+ import type { RecordingLoadState } from "./types.js";
2
+ export declare class RecordingLoad implements RecordingLoadState {
3
+ bufferedEnd?: bigint;
4
+ error?: Error;
5
+ complete: boolean;
6
+ readonly loaded: Promise<void>;
7
+ readonly controller: AbortController;
8
+ private resolve;
9
+ private reject;
10
+ private readonly listeners;
11
+ constructor();
12
+ subscribe(listener: () => void): () => void;
13
+ advance(time: bigint): void;
14
+ finish(): void;
15
+ fail(error: unknown): void;
16
+ close(): void;
17
+ private emit;
18
+ }
@@ -0,0 +1,50 @@
1
+ export class RecordingLoad {
2
+ bufferedEnd;
3
+ error;
4
+ complete = false;
5
+ loaded;
6
+ controller = new AbortController();
7
+ resolve;
8
+ reject;
9
+ listeners = new Set();
10
+ constructor() {
11
+ this.loaded = new Promise((resolve, reject) => {
12
+ this.resolve = resolve;
13
+ this.reject = reject;
14
+ });
15
+ void this.loaded.catch(() => { });
16
+ }
17
+ subscribe(listener) {
18
+ this.listeners.add(listener);
19
+ return () => {
20
+ this.listeners.delete(listener);
21
+ };
22
+ }
23
+ advance(time) {
24
+ this.bufferedEnd = time;
25
+ this.emit();
26
+ }
27
+ finish() {
28
+ if (this.error)
29
+ return;
30
+ this.complete = true;
31
+ this.resolve();
32
+ this.emit();
33
+ }
34
+ fail(error) {
35
+ if (this.complete || this.error)
36
+ return;
37
+ this.error = error instanceof Error ? error : new Error(String(error));
38
+ this.reject(this.error);
39
+ this.emit();
40
+ }
41
+ close() {
42
+ if (this.complete || this.error)
43
+ return;
44
+ this.controller.abort(new DOMException("MCAP recording closed", "AbortError"));
45
+ }
46
+ emit() {
47
+ for (const listener of this.listeners)
48
+ listener();
49
+ }
50
+ }
@@ -0,0 +1,8 @@
1
+ import "protobufjs/ext/descriptor/index.js";
2
+ import type { LoadOptions, McapStreamSource } from "./source.js";
3
+ import { McapRecording } from "./recording.js";
4
+ import type { TimeBounds } from "./types.js";
5
+ export interface ReadOptions extends LoadOptions {
6
+ bounds?: TimeBounds;
7
+ }
8
+ export declare function readMcap(input: Blob | McapStreamSource, options?: ReadOptions): Promise<McapRecording>;
@@ -0,0 +1,121 @@
1
+ import { McapStreamReader } from "@mcap/core";
2
+ import protobuf from "protobufjs";
3
+ import "protobufjs/ext/descriptor/index.js";
4
+ import { ZSTDDecoder } from "zstddec";
5
+ import { BlobSource } from "./source.js";
6
+ import { RecordingLoad } from "./loading.js";
7
+ import { McapRecording } from "./recording.js";
8
+ const zstd = new ZSTDDecoder();
9
+ /** Decode messages with their embedded Protobuf schema; other encodings stay raw bytes. */
10
+ function decoder(channel, schema) {
11
+ if (channel.messageEncoding !== "protobuf" || schema?.encoding !== "protobuf")
12
+ return (bytes) => bytes;
13
+ const root = protobuf.Root;
14
+ const type = root.fromDescriptor(schema.data).lookupType(schema.name);
15
+ return (bytes) => type.toObject(type.decode(bytes), {
16
+ defaults: true,
17
+ longs: String,
18
+ bytes: Uint8Array,
19
+ });
20
+ }
21
+ export async function readMcap(input, options = {}) {
22
+ options.signal?.throwIfAborted();
23
+ await zstd.init();
24
+ options.signal?.throwIfAborted();
25
+ const loading = new RecordingLoad();
26
+ const signal = options.signal
27
+ ? AbortSignal.any([options.signal, loading.controller.signal])
28
+ : loading.controller.signal;
29
+ const source = input instanceof Blob ? new BlobSource(input) : input;
30
+ const reader = new McapStreamReader({
31
+ validateCrcs: true,
32
+ decompressHandlers: {
33
+ zstd: (bytes, size) => zstd.decode(bytes, Number(size)),
34
+ },
35
+ });
36
+ const schemas = new Map();
37
+ const channels = new Map();
38
+ const decoders = new Map();
39
+ const byTopic = new Map();
40
+ const messages = [];
41
+ const metadata = new Map();
42
+ const recording = new McapRecording(metadata, channels, messages, byTopic, loading, options.bounds);
43
+ let resolveReady;
44
+ let rejectReady;
45
+ const ready = new Promise((resolve, reject) => {
46
+ resolveReady = resolve;
47
+ rejectReady = reject;
48
+ });
49
+ async function consume() {
50
+ let lastTime;
51
+ for await (const bytes of source.stream({ signal })) {
52
+ signal.throwIfAborted();
53
+ if (!bytes.length)
54
+ continue;
55
+ reader.append(bytes);
56
+ let bufferedEnd = loading.bufferedEnd;
57
+ for (let record = reader.nextRecord(); record; record = reader.nextRecord()) {
58
+ signal.throwIfAborted();
59
+ if (record.type === "Schema")
60
+ schemas.set(record.id, record);
61
+ else if (record.type === "Metadata")
62
+ metadata.set(record.name, record.metadata);
63
+ else if (record.type === "Channel") {
64
+ // The summary section repeats channels; keep the objects messages reference.
65
+ if (channels.has(record.id))
66
+ continue;
67
+ const schema = schemas.get(record.schemaId);
68
+ const channel = {
69
+ id: record.id,
70
+ topic: record.topic,
71
+ schemaName: schema?.name,
72
+ encoding: record.messageEncoding,
73
+ schemaEncoding: schema?.encoding,
74
+ schemaData: schema?.data,
75
+ metadata: record.metadata,
76
+ };
77
+ channels.set(record.id, channel);
78
+ decoders.set(record.id, decoder(record, schema));
79
+ if (!byTopic.has(channel.topic))
80
+ byTopic.set(channel.topic, []);
81
+ }
82
+ else if (record.type === "Message") {
83
+ if (lastTime !== undefined && record.logTime < lastTime)
84
+ throw new Error("MCAP messages must be ordered by logTime");
85
+ if (lastTime !== undefined && record.logTime > lastTime)
86
+ bufferedEnd = record.logTime - 1n;
87
+ lastTime = record.logTime;
88
+ const channel = channels.get(record.channelId);
89
+ const decode = decoders.get(record.channelId);
90
+ if (!channel || !decode)
91
+ throw new Error(`Missing MCAP channel ${record.channelId}`);
92
+ const message = {
93
+ channel,
94
+ time: record.logTime,
95
+ publishTime: record.publishTime,
96
+ sequence: record.sequence,
97
+ value: decode(record.data),
98
+ data: record.data,
99
+ };
100
+ messages.push(message);
101
+ byTopic.get(channel.topic).push(message);
102
+ }
103
+ }
104
+ if (bufferedEnd !== undefined && bufferedEnd !== loading.bufferedEnd) {
105
+ loading.advance(bufferedEnd);
106
+ if (recording.bufferedEnd !== undefined)
107
+ resolveReady(recording);
108
+ }
109
+ }
110
+ signal.throwIfAborted();
111
+ if (!reader.done())
112
+ throw new Error("Incomplete MCAP recording");
113
+ loading.finish();
114
+ resolveReady(recording);
115
+ }
116
+ void consume().catch((error) => {
117
+ loading.fail(error);
118
+ rejectReady(error);
119
+ });
120
+ return ready;
121
+ }
@@ -0,0 +1,31 @@
1
+ import type { Recording, RecordingLoadState, Message, TimeBounds, Topic } from "./types.js";
2
+ import type { ReadOptions } from "./reader.js";
3
+ import type { McapStreamSource } from "./source.js";
4
+ import type { RecordingLoad } from "./loading.js";
5
+ export declare class McapRecording implements Recording {
6
+ readonly metadata: ReadonlyMap<string, ReadonlyMap<string, string>>;
7
+ readonly channels: ReadonlyMap<number, Topic>;
8
+ readonly messages: readonly Message[];
9
+ readonly byTopic: ReadonlyMap<string, readonly Message[]>;
10
+ private readonly loading;
11
+ readonly bounds?: TimeBounds | undefined;
12
+ /** Stream an MCAP file, URL, or byte source independently of a backend. */
13
+ static open(input: Blob | McapStreamSource | string | URL, options?: ReadOptions): Promise<McapRecording>;
14
+ constructor(metadata: ReadonlyMap<string, ReadonlyMap<string, string>>, channels: ReadonlyMap<number, Topic>, messages: readonly Message[], byTopic: ReadonlyMap<string, readonly Message[]>, loading: RecordingLoad, bounds?: TimeBounds | undefined);
15
+ get start(): bigint;
16
+ get end(): bigint;
17
+ get counts(): Readonly<Record<string, number>>;
18
+ get bufferedEnd(): bigint | undefined;
19
+ get complete(): boolean;
20
+ get error(): Error | undefined;
21
+ get loaded(): Promise<void>;
22
+ /** Observe buffered playback coverage, completion, or a loading error. */
23
+ subscribe(listener: (state: RecordingLoadState) => void): () => void;
24
+ /**
25
+ * Cancel the shared source load for this recording and all bounded views.
26
+ * Players attached to it stop without reporting an error.
27
+ */
28
+ close(): void;
29
+ /** Select playback bounds without removing samples needed for seeking. */
30
+ withBounds(bounds: TimeBounds): McapRecording;
31
+ }
@@ -0,0 +1,72 @@
1
+ import { readMcap } from "./reader.js";
2
+ import { HttpSource } from "./source.js";
3
+ export class McapRecording {
4
+ metadata;
5
+ channels;
6
+ messages;
7
+ byTopic;
8
+ loading;
9
+ bounds;
10
+ /** Stream an MCAP file, URL, or byte source independently of a backend. */
11
+ static async open(input, options = {}) {
12
+ const source = typeof input === "string" || input instanceof URL
13
+ ? new HttpSource(input)
14
+ : input;
15
+ return readMcap(source, options);
16
+ }
17
+ constructor(metadata, channels, messages, byTopic, loading, bounds) {
18
+ this.metadata = metadata;
19
+ this.channels = channels;
20
+ this.messages = messages;
21
+ this.byTopic = byTopic;
22
+ this.loading = loading;
23
+ this.bounds = bounds;
24
+ if (this.start > this.end)
25
+ throw new Error("Recording end must not precede its start");
26
+ }
27
+ get start() {
28
+ return this.bounds?.start ?? this.messages[0]?.time ?? 0n;
29
+ }
30
+ get end() {
31
+ return this.bounds?.end ?? this.messages.at(-1)?.time ?? this.start;
32
+ }
33
+ get counts() {
34
+ return Object.fromEntries([...this.byTopic].map(([topic, records]) => [topic, records.length]));
35
+ }
36
+ get bufferedEnd() {
37
+ if (this.complete)
38
+ return this.end;
39
+ const time = this.loading.bufferedEnd;
40
+ if (time === undefined || time < this.start)
41
+ return undefined;
42
+ return time > this.end ? this.end : time;
43
+ }
44
+ get complete() {
45
+ return this.loading.complete;
46
+ }
47
+ get error() {
48
+ return this.loading.error;
49
+ }
50
+ get loaded() {
51
+ return this.loading.loaded;
52
+ }
53
+ /** Observe buffered playback coverage, completion, or a loading error. */
54
+ subscribe(listener) {
55
+ return this.loading.subscribe(() => listener({
56
+ bufferedEnd: this.bufferedEnd,
57
+ complete: this.complete,
58
+ error: this.error,
59
+ }));
60
+ }
61
+ /**
62
+ * Cancel the shared source load for this recording and all bounded views.
63
+ * Players attached to it stop without reporting an error.
64
+ */
65
+ close() {
66
+ this.loading.close();
67
+ }
68
+ /** Select playback bounds without removing samples needed for seeking. */
69
+ withBounds(bounds) {
70
+ return new McapRecording(this.metadata, this.channels, this.messages, this.byTopic, this.loading, bounds);
71
+ }
72
+ }
@@ -0,0 +1,22 @@
1
+ export interface LoadOptions {
2
+ signal?: AbortSignal;
3
+ }
4
+ export interface McapStreamSource {
5
+ stream(options?: LoadOptions): AsyncIterable<Uint8Array>;
6
+ }
7
+ /** A local Blob or File, read incrementally. */
8
+ export declare class BlobSource implements McapStreamSource {
9
+ readonly blob: Blob;
10
+ constructor(blob: Blob);
11
+ stream({ signal }?: LoadOptions): AsyncIterable<Uint8Array>;
12
+ }
13
+ /** Download a signed MCAP URL without forwarding control API credentials. */
14
+ export declare class HttpSource implements McapStreamSource {
15
+ readonly url: string | URL;
16
+ private readonly options;
17
+ constructor(url: string | URL, options?: LoadOptions);
18
+ private combinedSignal;
19
+ /** Stream the response body directly; no sizing request or full download is needed. */
20
+ stream({ signal }?: LoadOptions): AsyncIterable<Uint8Array>;
21
+ private request;
22
+ }
@@ -0,0 +1,67 @@
1
+ async function* streamBytes(stream, signal) {
2
+ const reader = stream.getReader();
3
+ const abort = () => void reader.cancel(signal?.reason).catch(() => { });
4
+ signal?.addEventListener("abort", abort, { once: true });
5
+ try {
6
+ if (signal?.aborted)
7
+ abort();
8
+ while (true) {
9
+ signal?.throwIfAborted();
10
+ const result = await reader.read();
11
+ signal?.throwIfAborted();
12
+ if (result.done)
13
+ return;
14
+ yield result.value;
15
+ }
16
+ }
17
+ finally {
18
+ signal?.removeEventListener("abort", abort);
19
+ await reader.cancel().catch(() => { });
20
+ reader.releaseLock();
21
+ }
22
+ }
23
+ /** A local Blob or File, read incrementally. */
24
+ export class BlobSource {
25
+ blob;
26
+ constructor(blob) {
27
+ this.blob = blob;
28
+ }
29
+ stream({ signal } = {}) {
30
+ return streamBytes(this.blob.stream(), signal);
31
+ }
32
+ }
33
+ /** Download a signed MCAP URL without forwarding control API credentials. */
34
+ export class HttpSource {
35
+ url;
36
+ options;
37
+ constructor(url, options = {}) {
38
+ this.url = url;
39
+ this.options = options;
40
+ }
41
+ combinedSignal(signal) {
42
+ const constructorSignal = this.options.signal;
43
+ return constructorSignal && signal
44
+ ? AbortSignal.any([constructorSignal, signal])
45
+ : (constructorSignal ?? signal);
46
+ }
47
+ /** Stream the response body directly; no sizing request or full download is needed. */
48
+ async *stream({ signal } = {}) {
49
+ const combinedSignal = this.combinedSignal(signal);
50
+ combinedSignal?.throwIfAborted();
51
+ const response = await this.request(combinedSignal);
52
+ if (!response.body)
53
+ throw new Error("MCAP response has no body");
54
+ yield* streamBytes(response.body, combinedSignal);
55
+ }
56
+ async request(signal) {
57
+ signal?.throwIfAborted();
58
+ const response = await fetch(this.url, {
59
+ signal,
60
+ cache: "no-store",
61
+ credentials: "omit",
62
+ });
63
+ if (!response.ok)
64
+ throw new Error(`MCAP request failed (${response.status}): ${await response.text()}`);
65
+ return response;
66
+ }
67
+ }
@@ -0,0 +1,36 @@
1
+ export interface Topic {
2
+ readonly id: number;
3
+ readonly topic: string;
4
+ readonly schemaName?: string;
5
+ readonly encoding: string;
6
+ readonly schemaEncoding?: string;
7
+ readonly schemaData?: Uint8Array;
8
+ readonly metadata: ReadonlyMap<string, string>;
9
+ }
10
+ export interface Message<T = unknown> {
11
+ readonly channel: Topic;
12
+ readonly time: bigint;
13
+ readonly publishTime: bigint;
14
+ readonly sequence: number;
15
+ readonly value: T;
16
+ readonly data: Uint8Array;
17
+ }
18
+ export interface TimeBounds {
19
+ readonly start: bigint;
20
+ readonly end: bigint;
21
+ }
22
+ export interface RecordingLoadState {
23
+ readonly bufferedEnd?: bigint;
24
+ readonly complete: boolean;
25
+ readonly error?: Error;
26
+ }
27
+ export interface Recording extends TimeBounds {
28
+ readonly bounds?: TimeBounds;
29
+ readonly bufferedEnd?: bigint;
30
+ readonly complete?: boolean;
31
+ readonly error?: Error;
32
+ subscribe?(listener: (state: RecordingLoadState) => void): () => void;
33
+ readonly channels: ReadonlyMap<number, Topic>;
34
+ readonly messages: readonly Message[];
35
+ readonly byTopic: ReadonlyMap<string, readonly Message[]>;
36
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export { Player } from "./player.js";
2
+ export type { PlaybackListener, PlaybackState, Scheduler, SubscriptionOptions, } from "./player.js";
@@ -0,0 +1 @@
1
+ export { Player } from "./player.js";