@eventual-ai/mcap-viewer 0.1.0-alpha.0 → 0.1.0-alpha.2
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.
- package/README.md +15 -14
- package/dist/{types/core → core}/index.d.ts +2 -2
- package/dist/core/index.js +3 -0
- package/dist/core/mcap/codecs/h264.js +22 -0
- package/dist/{types/core → core}/mcap/codecs/video.d.ts +2 -1
- package/dist/core/mcap/codecs/video.js +64 -0
- package/dist/{types/core → core}/mcap/index.d.ts +1 -1
- package/dist/core/mcap/index.js +2 -0
- package/dist/core/mcap/loading.js +50 -0
- package/dist/{types/core → core}/mcap/reader.d.ts +6 -1
- package/dist/core/mcap/reader.js +141 -0
- package/dist/{types/core → core}/mcap/recording.d.ts +8 -5
- package/dist/core/mcap/recording.js +72 -0
- package/dist/core/mcap/source.js +67 -0
- package/dist/{types/core → core}/mcap/types.d.ts +3 -3
- package/dist/core/mcap/types.js +1 -0
- package/dist/{types/core → core}/playback/index.d.ts +1 -1
- package/dist/core/playback/index.js +1 -0
- package/dist/{types/core → core}/playback/player.d.ts +8 -2
- package/dist/core/playback/player.js +344 -0
- package/dist/{types/core → core}/playback/snapshot.d.ts +2 -2
- package/dist/core/playback/snapshot.js +11 -0
- package/dist/{types/core → core}/playback/video-sequence.d.ts +7 -7
- package/dist/core/playback/video-sequence.js +116 -0
- package/dist/{types/core → core}/renderers/image.d.ts +2 -0
- package/dist/core/renderers/image.js +165 -0
- package/dist/core/renderers/index.js +1 -0
- package/dist/{types/core → core}/renderers/video-decoder.d.ts +4 -1
- package/dist/core/renderers/video-decoder.js +146 -0
- package/dist/core/schemas.d.ts +19 -0
- package/dist/core/schemas.js +13 -0
- package/dist/data-loader/index.d.ts +2 -0
- package/dist/data-loader/index.js +1 -0
- package/dist/{types/data-loader → data-loader}/loader.d.ts +4 -4
- package/dist/data-loader/loader.js +44 -0
- package/dist/{types/data-loader → data-loader}/types.d.ts +3 -3
- package/dist/data-loader/types.js +1 -0
- package/dist/index.js +2 -0
- package/package.json +17 -16
- package/dist/THIRD_PARTY_LICENSES.md +0 -415
- package/dist/core.js +0 -534
- package/dist/data-loader.js +0 -53
- package/dist/mcap-viewer.js +0 -11
- package/dist/recording-tVYbg6KM.js +0 -9109
- package/dist/types/core/schemas.d.ts +0 -15
- package/dist/types/data-loader/index.d.ts +0 -2
- /package/dist/{types/core → core}/mcap/codecs/h264.d.ts +0 -0
- /package/dist/{types/core → core}/mcap/loading.d.ts +0 -0
- /package/dist/{types/core → core}/mcap/source.d.ts +0 -0
- /package/dist/{types/core → core}/renderers/index.d.ts +0 -0
- /package/dist/{types/index.d.ts → index.d.ts} +0 -0
|
@@ -10,8 +10,11 @@ export declare class CompressedVideoDecoder {
|
|
|
10
10
|
private pending;
|
|
11
11
|
private failed;
|
|
12
12
|
constructor(output: (frame: VideoFrame, time: bigint) => void, error: (error: Error) => void);
|
|
13
|
-
|
|
13
|
+
/** Decoding starts at the first keyframe; earlier delta packets are dropped. */
|
|
14
|
+
decode(time: bigint, bytes: Uint8Array, keyframe: boolean, format?: string): void;
|
|
14
15
|
private reject;
|
|
16
|
+
/** Report once and ignore further packets until `reset`. */
|
|
17
|
+
private fail;
|
|
15
18
|
flush(): Promise<void>;
|
|
16
19
|
reset(): void;
|
|
17
20
|
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { inspectVideo } from "../mcap/codecs/video.js";
|
|
2
|
+
export class CompressedVideoDecoder {
|
|
3
|
+
output;
|
|
4
|
+
error;
|
|
5
|
+
decoder;
|
|
6
|
+
origin = 0n;
|
|
7
|
+
times = new Map();
|
|
8
|
+
generation = 0;
|
|
9
|
+
initializing;
|
|
10
|
+
flushing;
|
|
11
|
+
pending = [];
|
|
12
|
+
failed = false;
|
|
13
|
+
constructor(output, error) {
|
|
14
|
+
this.output = output;
|
|
15
|
+
this.error = error;
|
|
16
|
+
}
|
|
17
|
+
/** Decoding starts at the first keyframe; earlier delta packets are dropped. */
|
|
18
|
+
decode(time, bytes, keyframe, format = "h264") {
|
|
19
|
+
if (this.failed)
|
|
20
|
+
return;
|
|
21
|
+
try {
|
|
22
|
+
if (!this.decoder && !this.initializing) {
|
|
23
|
+
if (!keyframe)
|
|
24
|
+
return;
|
|
25
|
+
const name = format === "h265" ? "HEVC" : "H.264";
|
|
26
|
+
const codec = inspectVideo(bytes, format).codec;
|
|
27
|
+
if (!codec) {
|
|
28
|
+
this.fail(new Error(`${name} keyframe is missing codec headers`));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (typeof VideoDecoder === "undefined") {
|
|
32
|
+
this.fail(new Error("This browser does not support WebCodecs video decoding"));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
this.origin = time;
|
|
36
|
+
const generation = this.generation;
|
|
37
|
+
const config = { codec, optimizeForLatency: true };
|
|
38
|
+
const configure = () => {
|
|
39
|
+
if (generation !== this.generation)
|
|
40
|
+
return;
|
|
41
|
+
this.decoder = new VideoDecoder({
|
|
42
|
+
output: (frame) => {
|
|
43
|
+
if (generation !== this.generation) {
|
|
44
|
+
frame.close();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const timestamp = this.times.get(frame.timestamp);
|
|
48
|
+
this.times.delete(frame.timestamp);
|
|
49
|
+
if (timestamp === undefined) {
|
|
50
|
+
frame.close();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
this.output(frame, timestamp);
|
|
54
|
+
},
|
|
55
|
+
error: (error) => {
|
|
56
|
+
if (generation === this.generation)
|
|
57
|
+
this.reject(error, config.codec);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
this.decoder.configure(config);
|
|
61
|
+
for (const chunk of this.pending.splice(0))
|
|
62
|
+
this.decoder.decode(chunk);
|
|
63
|
+
};
|
|
64
|
+
if (typeof VideoDecoder.isConfigSupported === "function") {
|
|
65
|
+
this.initializing = VideoDecoder.isConfigSupported(config)
|
|
66
|
+
.then((support) => {
|
|
67
|
+
if (generation !== this.generation)
|
|
68
|
+
return;
|
|
69
|
+
if (!support.supported)
|
|
70
|
+
throw new Error(`This browser or device does not support ${name} video decoding (${config.codec})`);
|
|
71
|
+
configure();
|
|
72
|
+
})
|
|
73
|
+
.catch((error) => {
|
|
74
|
+
if (generation === this.generation)
|
|
75
|
+
this.reject(error, config.codec);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
else
|
|
79
|
+
configure();
|
|
80
|
+
}
|
|
81
|
+
// WebCodecs timestamps are whole microseconds; frames are far further apart.
|
|
82
|
+
const timestamp = Number((time - this.origin) / 1000n);
|
|
83
|
+
this.times.set(timestamp, time);
|
|
84
|
+
const chunk = new EncodedVideoChunk({
|
|
85
|
+
type: keyframe ? "key" : "delta",
|
|
86
|
+
timestamp,
|
|
87
|
+
data: bytes,
|
|
88
|
+
});
|
|
89
|
+
if (this.decoder)
|
|
90
|
+
this.decoder.decode(chunk);
|
|
91
|
+
else
|
|
92
|
+
this.pending.push(chunk);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
this.error(error instanceof Error ? error : new Error("Video decode failed"));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
reject(error, codec) {
|
|
99
|
+
this.fail(new Error(`Video decoding failed (${codec}): ${error instanceof Error ? error.message : String(error)}`));
|
|
100
|
+
}
|
|
101
|
+
/** Report once and ignore further packets until `reset`. */
|
|
102
|
+
fail(error) {
|
|
103
|
+
this.failed = true;
|
|
104
|
+
this.pending = [];
|
|
105
|
+
this.times.clear();
|
|
106
|
+
this.error(error);
|
|
107
|
+
}
|
|
108
|
+
flush() {
|
|
109
|
+
if (this.flushing)
|
|
110
|
+
return this.flushing;
|
|
111
|
+
const generation = this.generation;
|
|
112
|
+
const flushing = (async () => {
|
|
113
|
+
if (this.initializing)
|
|
114
|
+
await this.initializing;
|
|
115
|
+
const decoder = this.decoder;
|
|
116
|
+
if (generation !== this.generation ||
|
|
117
|
+
!decoder ||
|
|
118
|
+
decoder.state !== "configured")
|
|
119
|
+
return;
|
|
120
|
+
try {
|
|
121
|
+
await decoder.flush();
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (generation === this.generation)
|
|
125
|
+
this.error(error instanceof Error ? error : new Error("Video decode failed"));
|
|
126
|
+
}
|
|
127
|
+
})().finally(() => {
|
|
128
|
+
// Concurrent callers share one flush; later calls flush again.
|
|
129
|
+
if (this.flushing === flushing)
|
|
130
|
+
this.flushing = undefined;
|
|
131
|
+
});
|
|
132
|
+
this.flushing = flushing;
|
|
133
|
+
return flushing;
|
|
134
|
+
}
|
|
135
|
+
reset() {
|
|
136
|
+
this.generation++;
|
|
137
|
+
if (this.decoder && this.decoder.state !== "closed")
|
|
138
|
+
this.decoder.close();
|
|
139
|
+
this.decoder = undefined;
|
|
140
|
+
this.initializing = undefined;
|
|
141
|
+
this.flushing = undefined;
|
|
142
|
+
this.pending = [];
|
|
143
|
+
this.failed = false;
|
|
144
|
+
this.times.clear();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface Timestamp {
|
|
2
|
+
/** int64 as a decimal string. */
|
|
3
|
+
seconds: string;
|
|
4
|
+
nanos: number;
|
|
5
|
+
}
|
|
6
|
+
/** One native H.264 or H.265 access unit, the only message MDA sends. */
|
|
7
|
+
export interface CompressedVideo {
|
|
8
|
+
timestamp: Timestamp | null;
|
|
9
|
+
frame_id: string;
|
|
10
|
+
data: Uint8Array;
|
|
11
|
+
format: string;
|
|
12
|
+
keyframe: boolean;
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
/** uint64 decode position as a decimal string; absent when unset. */
|
|
16
|
+
decode_order?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare const videoSchema = "eventual.mda.v1.CompressedVideo";
|
|
19
|
+
export declare function compressedVideo(value: unknown): CompressedVideo;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const videoSchema = "eventual.mda.v1.CompressedVideo";
|
|
2
|
+
export function compressedVideo(value) {
|
|
3
|
+
if (!value ||
|
|
4
|
+
typeof value !== "object" ||
|
|
5
|
+
!("data" in value) ||
|
|
6
|
+
!(value.data instanceof Uint8Array) ||
|
|
7
|
+
!("format" in value) ||
|
|
8
|
+
typeof value.format !== "string" ||
|
|
9
|
+
!("keyframe" in value) ||
|
|
10
|
+
typeof value.keyframe !== "boolean")
|
|
11
|
+
throw new Error("Invalid CompressedVideo message");
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DataLoader } from "./loader.js";
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { McapRecording } from "../core/mcap/recording.js";
|
|
2
2
|
import type { ReadOptions } from "../core/mcap/reader.js";
|
|
3
|
-
import type { Capabilities, Discovery, ManifestRequest, ManifestResult,
|
|
3
|
+
import type { Capabilities, Discovery, ManifestRequest, ManifestResult, DataLoaderOptions, RequestOptions } from "./types.js";
|
|
4
4
|
/** Backend-specific discovery and manifests for the generic MCAP playback core. */
|
|
5
|
-
export declare class
|
|
5
|
+
export declare class DataLoader {
|
|
6
6
|
private readonly baseUrl;
|
|
7
7
|
private readonly headers;
|
|
8
|
-
constructor({ baseUrl, headers }:
|
|
8
|
+
constructor({ baseUrl, headers }: DataLoaderOptions);
|
|
9
9
|
private request;
|
|
10
10
|
/** List the service's supported canonical data types. */
|
|
11
11
|
capabilities(options?: RequestOptions): Promise<Capabilities>;
|
|
12
12
|
/** Discover a datasource's canonical schemas and actual wire encodings. */
|
|
13
13
|
discover(datasource: string, options?: RequestOptions): Promise<Discovery>;
|
|
14
|
-
/** Resolve
|
|
14
|
+
/** Resolve one MCAP URL per span, in request order; each span succeeds or fails independently. */
|
|
15
15
|
manifest(request: ManifestRequest, options?: RequestOptions): Promise<readonly ManifestResult[]>;
|
|
16
16
|
/** Open the single MCAP shared by all topics in one successful manifest result. */
|
|
17
17
|
openSpan(result: ManifestResult, options?: ReadOptions): Promise<McapRecording>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { McapRecording } from "../core/mcap/recording.js";
|
|
2
|
+
/** Backend-specific discovery and manifests for the generic MCAP playback core. */
|
|
3
|
+
export class DataLoader {
|
|
4
|
+
baseUrl;
|
|
5
|
+
headers;
|
|
6
|
+
constructor({ baseUrl, headers }) {
|
|
7
|
+
this.baseUrl = new URL(`${String(baseUrl).replace(/\/+$/, "")}/`);
|
|
8
|
+
this.headers = new Headers(headers);
|
|
9
|
+
}
|
|
10
|
+
async request(path, options, body) {
|
|
11
|
+
const headers = new Headers(this.headers);
|
|
12
|
+
new Headers(options.headers).forEach((value, key) => headers.set(key, value));
|
|
13
|
+
if (body !== undefined)
|
|
14
|
+
headers.set("Content-Type", "application/json");
|
|
15
|
+
const response = await fetch(new URL(`v1/${path}`, this.baseUrl), {
|
|
16
|
+
method: body === undefined ? "GET" : "POST",
|
|
17
|
+
headers,
|
|
18
|
+
signal: options.signal,
|
|
19
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
20
|
+
cache: "no-store",
|
|
21
|
+
});
|
|
22
|
+
if (!response.ok)
|
|
23
|
+
throw new Error(`MDA request failed (${response.status}): ${await response.text()}`);
|
|
24
|
+
return response.json();
|
|
25
|
+
}
|
|
26
|
+
/** List the service's supported canonical data types. */
|
|
27
|
+
capabilities(options = {}) {
|
|
28
|
+
return this.request("capabilities", options);
|
|
29
|
+
}
|
|
30
|
+
/** Discover a datasource's canonical schemas and actual wire encodings. */
|
|
31
|
+
discover(datasource, options = {}) {
|
|
32
|
+
return this.request(`discover?${new URLSearchParams({ datasource })}`, options);
|
|
33
|
+
}
|
|
34
|
+
/** Resolve one MCAP URL per span, in request order; each span succeeds or fails independently. */
|
|
35
|
+
manifest(request, options = {}) {
|
|
36
|
+
return this.request("manifest", options, request);
|
|
37
|
+
}
|
|
38
|
+
/** Open the single MCAP shared by all topics in one successful manifest result. */
|
|
39
|
+
async openSpan(result, options = {}) {
|
|
40
|
+
if ("error" in result)
|
|
41
|
+
throw new Error(`${result.error.code}: ${result.error.message}`);
|
|
42
|
+
return McapRecording.open(result.manifest.url, options);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -2,7 +2,7 @@ import type { LoadOptions } from "../core/mcap/source.js";
|
|
|
2
2
|
export interface RequestOptions extends LoadOptions {
|
|
3
3
|
headers?: HeadersInit;
|
|
4
4
|
}
|
|
5
|
-
export interface
|
|
5
|
+
export interface DataLoaderOptions {
|
|
6
6
|
baseUrl: string | URL;
|
|
7
7
|
headers?: HeadersInit;
|
|
8
8
|
}
|
|
@@ -17,7 +17,7 @@ export interface Capabilities {
|
|
|
17
17
|
status: "live";
|
|
18
18
|
}>>;
|
|
19
19
|
}
|
|
20
|
-
export type
|
|
20
|
+
export type Span = {
|
|
21
21
|
id: string;
|
|
22
22
|
} & ({
|
|
23
23
|
start: number;
|
|
@@ -29,7 +29,7 @@ export type ReplaySpan = {
|
|
|
29
29
|
export interface ManifestRequest {
|
|
30
30
|
datasource: string;
|
|
31
31
|
topics: readonly string[];
|
|
32
|
-
spans: readonly
|
|
32
|
+
spans: readonly Span[];
|
|
33
33
|
}
|
|
34
34
|
export interface Manifest {
|
|
35
35
|
url: string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
package/package.json
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eventual-ai/mcap-viewer",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.2",
|
|
4
4
|
"description": "Browser playback of native H.264/H.265 video from streamed MCAP recordings.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"types": "./dist/
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
-
"types": "./dist/
|
|
11
|
-
"import": "./dist/
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
12
|
},
|
|
13
13
|
"./core": {
|
|
14
|
-
"types": "./dist/
|
|
15
|
-
"import": "./dist/core.js"
|
|
14
|
+
"types": "./dist/core/index.d.ts",
|
|
15
|
+
"import": "./dist/core/index.js"
|
|
16
16
|
},
|
|
17
17
|
"./data-loader": {
|
|
18
|
-
"types": "./dist/
|
|
19
|
-
"import": "./dist/data-loader.js"
|
|
18
|
+
"types": "./dist/data-loader/index.d.ts",
|
|
19
|
+
"import": "./dist/data-loader/index.js"
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
"files": [
|
|
@@ -30,22 +30,23 @@
|
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
32
|
"dev": "vite --host 127.0.0.1",
|
|
33
|
-
"build": "
|
|
34
|
-
"build:example": "vite build
|
|
35
|
-
"fixtures": "
|
|
33
|
+
"build": "rm -rf dist && tsc",
|
|
34
|
+
"build:example": "vite build",
|
|
35
|
+
"fixtures": "mcap_viewer_test_root=\"$PWD\"; cd \"${MDA_TEST_SERVICE_ROOT:-../services/mda}\" && uv run --locked pytest \"$mcap_viewer_test_root/tests/server_fixture.py\" -q",
|
|
36
36
|
"pretest": "npm run fixtures && npm run build && npm run typecheck && npm run build:example",
|
|
37
37
|
"test": "node --import tsx --test tests/*.test.js",
|
|
38
|
-
"typecheck": "tsc --noEmit
|
|
38
|
+
"typecheck": "tsc --noEmit && tsc -p tests/tsconfig.json",
|
|
39
39
|
"prepublishOnly": "npm run build",
|
|
40
40
|
"lint": "prettier --check src tests examples vite.config.js package.json tsconfig.json",
|
|
41
41
|
"format": "prettier --write src tests examples vite.config.js package.json tsconfig.json"
|
|
42
42
|
},
|
|
43
|
-
"
|
|
44
|
-
"@foxglove/wasm-zstd": "^1.0.1",
|
|
43
|
+
"dependencies": {
|
|
45
44
|
"@mcap/core": "^2.4.0",
|
|
46
|
-
"buffer": "^6.0.3",
|
|
47
|
-
"prettier": "^3.6.2",
|
|
48
45
|
"protobufjs": "^8.8.1",
|
|
46
|
+
"zstddec": "^0.3.4"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"prettier": "^3.6.2",
|
|
49
50
|
"tsx": "^4.23.15",
|
|
50
51
|
"typescript": "^5.9.3",
|
|
51
52
|
"vite": "^7.3.7"
|