avbridge 1.0.0

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 (103) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/LICENSE +21 -0
  3. package/README.md +415 -0
  4. package/dist/avi-M5B4SHRM.cjs +164 -0
  5. package/dist/avi-M5B4SHRM.cjs.map +1 -0
  6. package/dist/avi-POCGZ4JX.js +162 -0
  7. package/dist/avi-POCGZ4JX.js.map +1 -0
  8. package/dist/chunk-5ISVAODK.js +80 -0
  9. package/dist/chunk-5ISVAODK.js.map +1 -0
  10. package/dist/chunk-F7YS2XOA.cjs +2966 -0
  11. package/dist/chunk-F7YS2XOA.cjs.map +1 -0
  12. package/dist/chunk-FKM7QBZU.js +2957 -0
  13. package/dist/chunk-FKM7QBZU.js.map +1 -0
  14. package/dist/chunk-J5MCMN3S.js +27 -0
  15. package/dist/chunk-J5MCMN3S.js.map +1 -0
  16. package/dist/chunk-L4NPOJ36.cjs +180 -0
  17. package/dist/chunk-L4NPOJ36.cjs.map +1 -0
  18. package/dist/chunk-NZU7W256.cjs +29 -0
  19. package/dist/chunk-NZU7W256.cjs.map +1 -0
  20. package/dist/chunk-PQTZS7OA.js +147 -0
  21. package/dist/chunk-PQTZS7OA.js.map +1 -0
  22. package/dist/chunk-WD2ZNQA7.js +177 -0
  23. package/dist/chunk-WD2ZNQA7.js.map +1 -0
  24. package/dist/chunk-Y5FYF5KG.cjs +153 -0
  25. package/dist/chunk-Y5FYF5KG.cjs.map +1 -0
  26. package/dist/chunk-Z2FJ5TJC.cjs +82 -0
  27. package/dist/chunk-Z2FJ5TJC.cjs.map +1 -0
  28. package/dist/element.cjs +433 -0
  29. package/dist/element.cjs.map +1 -0
  30. package/dist/element.d.cts +158 -0
  31. package/dist/element.d.ts +158 -0
  32. package/dist/element.js +431 -0
  33. package/dist/element.js.map +1 -0
  34. package/dist/index.cjs +576 -0
  35. package/dist/index.cjs.map +1 -0
  36. package/dist/index.d.cts +80 -0
  37. package/dist/index.d.ts +80 -0
  38. package/dist/index.js +554 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/libav-http-reader-FPYDBMYK.cjs +16 -0
  41. package/dist/libav-http-reader-FPYDBMYK.cjs.map +1 -0
  42. package/dist/libav-http-reader-NQJVY273.js +3 -0
  43. package/dist/libav-http-reader-NQJVY273.js.map +1 -0
  44. package/dist/libav-import-2JURFHEW.js +8 -0
  45. package/dist/libav-import-2JURFHEW.js.map +1 -0
  46. package/dist/libav-import-GST2AMPL.cjs +30 -0
  47. package/dist/libav-import-GST2AMPL.cjs.map +1 -0
  48. package/dist/libav-loader-KA2MAWLM.js +3 -0
  49. package/dist/libav-loader-KA2MAWLM.js.map +1 -0
  50. package/dist/libav-loader-ZHOERPHW.cjs +12 -0
  51. package/dist/libav-loader-ZHOERPHW.cjs.map +1 -0
  52. package/dist/player-BBwbCkdL.d.cts +365 -0
  53. package/dist/player-BBwbCkdL.d.ts +365 -0
  54. package/dist/source-SC6ZEQYR.cjs +28 -0
  55. package/dist/source-SC6ZEQYR.cjs.map +1 -0
  56. package/dist/source-ZFS4H7J3.js +3 -0
  57. package/dist/source-ZFS4H7J3.js.map +1 -0
  58. package/dist/variant-routing-GOHB2RZN.cjs +12 -0
  59. package/dist/variant-routing-GOHB2RZN.cjs.map +1 -0
  60. package/dist/variant-routing-JOBWXYKD.js +3 -0
  61. package/dist/variant-routing-JOBWXYKD.js.map +1 -0
  62. package/package.json +95 -0
  63. package/src/classify/index.ts +1 -0
  64. package/src/classify/rules.ts +214 -0
  65. package/src/convert/index.ts +2 -0
  66. package/src/convert/remux.ts +522 -0
  67. package/src/convert/transcode.ts +329 -0
  68. package/src/diagnostics.ts +99 -0
  69. package/src/element/avbridge-player.ts +576 -0
  70. package/src/element.ts +19 -0
  71. package/src/events.ts +71 -0
  72. package/src/index.ts +42 -0
  73. package/src/libav-stubs.d.ts +24 -0
  74. package/src/player.ts +455 -0
  75. package/src/plugins/builtin.ts +37 -0
  76. package/src/plugins/registry.ts +32 -0
  77. package/src/probe/avi.ts +242 -0
  78. package/src/probe/index.ts +59 -0
  79. package/src/probe/mediabunny.ts +194 -0
  80. package/src/strategies/fallback/audio-output.ts +293 -0
  81. package/src/strategies/fallback/clock.ts +7 -0
  82. package/src/strategies/fallback/decoder.ts +660 -0
  83. package/src/strategies/fallback/index.ts +170 -0
  84. package/src/strategies/fallback/libav-import.ts +27 -0
  85. package/src/strategies/fallback/libav-loader.ts +190 -0
  86. package/src/strategies/fallback/variant-routing.ts +43 -0
  87. package/src/strategies/fallback/video-renderer.ts +216 -0
  88. package/src/strategies/hybrid/decoder.ts +641 -0
  89. package/src/strategies/hybrid/index.ts +139 -0
  90. package/src/strategies/native.ts +107 -0
  91. package/src/strategies/remux/annexb.ts +112 -0
  92. package/src/strategies/remux/index.ts +79 -0
  93. package/src/strategies/remux/mse.ts +234 -0
  94. package/src/strategies/remux/pipeline.ts +254 -0
  95. package/src/subtitles/index.ts +91 -0
  96. package/src/subtitles/render.ts +62 -0
  97. package/src/subtitles/srt.ts +62 -0
  98. package/src/subtitles/vtt.ts +5 -0
  99. package/src/types-shim.d.ts +3 -0
  100. package/src/types.ts +360 -0
  101. package/src/util/codec-strings.ts +86 -0
  102. package/src/util/libav-http-reader.ts +315 -0
  103. package/src/util/source.ts +274 -0
@@ -0,0 +1,641 @@
1
+ /**
2
+ * Hybrid decoder: libav.js demux + WebCodecs VideoDecoder + libav audio decode.
3
+ *
4
+ * This is the hardware-accelerated path for files in containers mediabunny
5
+ * can't read (AVI, ASF, FLV) but whose codecs ARE browser-supported.
6
+ * libav.js handles demuxing, then:
7
+ *
8
+ * - **Video**: bridge.packetToEncodedVideoChunk → VideoDecoder (hardware)
9
+ * - **Audio**: libav ff_decode_multi (software). Chrome's AudioDecoder
10
+ * rejects raw MP3 packets from AVI, and audio decode is cheap enough
11
+ * that software decode is fine.
12
+ *
13
+ * The demux pump loop, seek handling, and synthetic timestamp logic mirror
14
+ * fallback/decoder.ts. The key difference is the video decode path.
15
+ */
16
+
17
+ import { loadLibav, type LibavVariant } from "../fallback/libav-loader.js";
18
+ import { VideoRenderer } from "../fallback/video-renderer.js";
19
+ import { AudioOutput } from "../fallback/audio-output.js";
20
+ import type { MediaContext } from "../../types.js";
21
+ import { pickLibavVariant } from "../fallback/variant-routing.js";
22
+
23
+ export interface HybridDecoderHandles {
24
+ destroy(): Promise<void>;
25
+ seek(timeSec: number): Promise<void>;
26
+ stats(): Record<string, unknown>;
27
+ onFatalError(handler: (reason: string) => void): void;
28
+ }
29
+
30
+ export interface StartHybridDecoderOptions {
31
+ /** Normalized source — either a Blob in memory or a URL we'll stream via Range requests. */
32
+ source: import("../../util/source.js").NormalizedSource;
33
+ filename: string;
34
+ context: MediaContext;
35
+ renderer: VideoRenderer;
36
+ audio: AudioOutput;
37
+ }
38
+
39
+ export async function startHybridDecoder(opts: StartHybridDecoderOptions): Promise<HybridDecoderHandles> {
40
+ const variant: LibavVariant = pickLibavVariant(opts.context);
41
+ const libav = (await loadLibav(variant)) as unknown as LibavRuntime;
42
+ const bridge = await loadBridge();
43
+
44
+ // For URL sources, prepareLibavInput attaches an HTTP block reader so
45
+ // libav demuxes via Range requests. For Blob sources, it falls back to
46
+ // mkreadaheadfile (in-memory). The returned handle owns cleanup.
47
+ const { prepareLibavInput } = await import("../../util/libav-http-reader.js");
48
+ const inputHandle = await prepareLibavInput(libav as unknown as Parameters<typeof prepareLibavInput>[0], opts.filename, opts.source);
49
+
50
+ const readPkt = await libav.av_packet_alloc();
51
+ const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
52
+ const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
53
+ const audioStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO) ?? null;
54
+
55
+ if (!videoStream && !audioStream) {
56
+ throw new Error("hybrid decoder: file has no decodable streams");
57
+ }
58
+
59
+ // ── Fatal error callback ──────────────────────────────────────────────
60
+ let fatalHandler: ((reason: string) => void) | null = null;
61
+ let fatalFired = false;
62
+
63
+ function fireFatal(reason: string): void {
64
+ if (fatalFired) return;
65
+ fatalFired = true;
66
+ fatalHandler?.(reason);
67
+ }
68
+
69
+ // ── WebCodecs VideoDecoder ────────────────────────────────────────────
70
+ let videoDecoder: VideoDecoder | null = null;
71
+ let videoTimeBase: [number, number] | undefined;
72
+
73
+ if (videoStream) {
74
+ try {
75
+ const config = await bridge.videoStreamToConfig(libav, videoStream);
76
+ if (!config) throw new Error("bridge returned null config");
77
+
78
+ const supported = await VideoDecoder.isConfigSupported(config);
79
+ if (!supported.supported) throw new Error(`VideoDecoder does not support config: ${JSON.stringify(config)}`);
80
+
81
+ videoDecoder = new VideoDecoder({
82
+ output: (frame: VideoFrame) => {
83
+ opts.renderer.enqueue(frame);
84
+ videoFramesDecoded++;
85
+ },
86
+ error: (err: DOMException) => {
87
+ console.error("[avbridge] WebCodecs VideoDecoder error:", err);
88
+ fireFatal(`WebCodecs VideoDecoder error: ${err.message}`);
89
+ },
90
+ });
91
+ videoDecoder.configure(config);
92
+
93
+ if (videoStream.time_base_num && videoStream.time_base_den) {
94
+ videoTimeBase = [videoStream.time_base_num, videoStream.time_base_den];
95
+ }
96
+ } catch (err) {
97
+ console.error("[avbridge] hybrid: failed to init WebCodecs VideoDecoder:", err);
98
+ fireFatal(`WebCodecs VideoDecoder init failed: ${(err as Error).message}`);
99
+ // Clean up and throw — the player will escalate to fallback
100
+ await inputHandle.detach().catch(() => {});
101
+ throw err;
102
+ }
103
+ }
104
+
105
+ // ── libav software AudioDecoder ───────────────────────────────────────
106
+ let audioDec: SoftDecoder | null = null;
107
+ let audioTimeBase: [number, number] | undefined;
108
+
109
+ if (audioStream) {
110
+ try {
111
+ const [, c, pkt, frame] = await libav.ff_init_decoder(audioStream.codec_id, {
112
+ codecpar: audioStream.codecpar,
113
+ });
114
+ audioDec = { c, pkt, frame };
115
+ if (audioStream.time_base_num && audioStream.time_base_den) {
116
+ audioTimeBase = [audioStream.time_base_num, audioStream.time_base_den];
117
+ }
118
+ } catch (err) {
119
+ console.warn(
120
+ "[avbridge] hybrid: audio decoder unavailable for this codec — playing video with wall-clock timing:",
121
+ (err as Error).message,
122
+ );
123
+ }
124
+ }
125
+
126
+ // No audio decoder? Switch the audio output into wall-clock mode so the
127
+ // video renderer doesn't stall waiting for an audio clock that never starts.
128
+ if (!audioDec) {
129
+ opts.audio.setNoAudio();
130
+ }
131
+
132
+ if (!videoDecoder && !audioDec) {
133
+ await inputHandle.detach().catch(() => {});
134
+ throw new Error("hybrid decoder: could not initialize any decoders");
135
+ }
136
+
137
+ // ── Mutable state ─────────────────────────────────────────────────────
138
+ let destroyed = false;
139
+ let pumpToken = 0;
140
+ let pumpRunning: Promise<void> | null = null;
141
+
142
+ let packetsRead = 0;
143
+ let videoFramesDecoded = 0;
144
+ let audioFramesDecoded = 0;
145
+ let videoChunksFed = 0;
146
+
147
+ let syntheticVideoUs = 0;
148
+ let syntheticAudioUs = 0;
149
+
150
+ const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
151
+ const videoFps = videoTrackInfo?.fps && videoTrackInfo.fps > 0 ? videoTrackInfo.fps : 30;
152
+ const videoFrameStepUs = Math.max(1, Math.round(1_000_000 / videoFps));
153
+
154
+ // ── Pump loop ─────────────────────────────────────────────────────────
155
+
156
+ async function pumpLoop(myToken: number): Promise<void> {
157
+ while (!destroyed && myToken === pumpToken) {
158
+ let readErr: number;
159
+ let packets: Record<number, LibavPacket[]>;
160
+ try {
161
+ [readErr, packets] = await libav.ff_read_frame_multi(fmt_ctx, readPkt, {
162
+ limit: 16 * 1024,
163
+ });
164
+ } catch (err) {
165
+ console.error("[avbridge] hybrid ff_read_frame_multi failed:", err);
166
+ return;
167
+ }
168
+
169
+ if (myToken !== pumpToken || destroyed) return;
170
+
171
+ const videoPackets = videoStream ? packets[videoStream.index] : undefined;
172
+ const audioPackets = audioStream ? packets[audioStream.index] : undefined;
173
+
174
+ // Feed video packets to WebCodecs VideoDecoder
175
+ if (videoDecoder && videoPackets && videoPackets.length > 0) {
176
+ for (const pkt of videoPackets) {
177
+ if (myToken !== pumpToken || destroyed) return;
178
+ sanitizePacketTimestamp(pkt, () => {
179
+ const ts = syntheticVideoUs;
180
+ syntheticVideoUs += videoFrameStepUs;
181
+ return ts;
182
+ }, videoTimeBase);
183
+ try {
184
+ const chunk = bridge.packetToEncodedVideoChunk(pkt, videoStream);
185
+ videoDecoder.decode(chunk);
186
+ videoChunksFed++;
187
+ } catch (err) {
188
+ if (videoChunksFed === 0) {
189
+ console.warn("[avbridge] hybrid: packetToEncodedVideoChunk failed:", err);
190
+ fireFatal(`WebCodecs chunk creation failed: ${(err as Error).message}`);
191
+ return;
192
+ }
193
+ }
194
+ }
195
+ }
196
+
197
+ // Decode audio with libav software decoder
198
+ if (audioDec && audioPackets && audioPackets.length > 0) {
199
+ await decodeAudioBatch(audioPackets, myToken);
200
+ }
201
+
202
+ packetsRead += (videoPackets?.length ?? 0) + (audioPackets?.length ?? 0);
203
+
204
+ // Backpressure: WebCodecs decodeQueueSize + audio buffer + renderer queue
205
+ while (
206
+ !destroyed &&
207
+ myToken === pumpToken &&
208
+ ((videoDecoder && videoDecoder.decodeQueueSize > 10) ||
209
+ opts.audio.bufferAhead() > 2.0 ||
210
+ opts.renderer.queueDepth() >= opts.renderer.queueHighWater)
211
+ ) {
212
+ await new Promise((r) => setTimeout(r, 50));
213
+ }
214
+
215
+ if (readErr === libav.AVERROR_EOF) {
216
+ // Flush WebCodecs decoder
217
+ if (videoDecoder && videoDecoder.state === "configured") {
218
+ try { await videoDecoder.flush(); } catch { /* ignore */ }
219
+ }
220
+ // Flush libav audio decoder
221
+ if (audioDec) await decodeAudioBatch([], myToken, true);
222
+ return;
223
+ }
224
+ if (readErr && readErr !== 0 && readErr !== -libav.EAGAIN) {
225
+ console.warn("[avbridge] hybrid ff_read_frame_multi returned", readErr);
226
+ return;
227
+ }
228
+ }
229
+ }
230
+
231
+ async function decodeAudioBatch(pkts: LibavPacket[], myToken: number, flush = false) {
232
+ if (!audioDec || destroyed || myToken !== pumpToken) return;
233
+ let frames: LibavFrame[];
234
+ try {
235
+ frames = await libav.ff_decode_multi(
236
+ audioDec.c,
237
+ audioDec.pkt,
238
+ audioDec.frame,
239
+ pkts,
240
+ flush ? { fin: true, ignoreErrors: true } : { ignoreErrors: true },
241
+ );
242
+ } catch (err) {
243
+ console.error("[avbridge] hybrid audio decode failed:", err);
244
+ return;
245
+ }
246
+ if (myToken !== pumpToken || destroyed) return;
247
+
248
+ for (const f of frames) {
249
+ if (myToken !== pumpToken || destroyed) return;
250
+ sanitizeFrameTimestamp(
251
+ f,
252
+ () => {
253
+ const ts = syntheticAudioUs;
254
+ const samples = f.nb_samples ?? 1024;
255
+ const sampleRate = f.sample_rate ?? 44100;
256
+ syntheticAudioUs += Math.round((samples * 1_000_000) / sampleRate);
257
+ return ts;
258
+ },
259
+ audioTimeBase,
260
+ );
261
+ const samples = libavFrameToInterleavedFloat32(f);
262
+ if (samples) {
263
+ opts.audio.schedule(samples.data, samples.channels, samples.sampleRate);
264
+ audioFramesDecoded++;
265
+ }
266
+ }
267
+ }
268
+
269
+ // Kick off initial pump
270
+ pumpToken = 1;
271
+ pumpRunning = pumpLoop(pumpToken).catch((err) =>
272
+ console.error("[avbridge] hybrid pump failed:", err),
273
+ );
274
+
275
+ return {
276
+ onFatalError(handler: (reason: string) => void): void {
277
+ fatalHandler = handler;
278
+ // If fatal already fired before handler was attached, fire immediately
279
+ if (fatalFired) handler("WebCodecs decode failed (error occurred before handler attached)");
280
+ },
281
+
282
+ async destroy() {
283
+ destroyed = true;
284
+ pumpToken++;
285
+ try { await pumpRunning; } catch { /* ignore */ }
286
+ try { if (videoDecoder && videoDecoder.state !== "closed") videoDecoder.close(); } catch { /* ignore */ }
287
+ try { if (audioDec) await libav.ff_free_decoder?.(audioDec.c, audioDec.pkt, audioDec.frame); } catch { /* ignore */ }
288
+ try { await libav.av_packet_free?.(readPkt); } catch { /* ignore */ }
289
+ try { await libav.avformat_close_input_js(fmt_ctx); } catch { /* ignore */ }
290
+ try { await inputHandle.detach(); } catch { /* ignore */ }
291
+ },
292
+
293
+ async seek(timeSec) {
294
+ const newToken = ++pumpToken;
295
+ if (pumpRunning) {
296
+ try { await pumpRunning; } catch { /* ignore */ }
297
+ }
298
+ if (destroyed) return;
299
+
300
+ try {
301
+ const tsUs = Math.floor(timeSec * 1_000_000);
302
+ const [tsLo, tsHi] = libav.f64toi64
303
+ ? libav.f64toi64(tsUs)
304
+ : [tsUs | 0, Math.floor(tsUs / 0x100000000)];
305
+ await libav.av_seek_frame(
306
+ fmt_ctx,
307
+ -1,
308
+ tsLo,
309
+ tsHi,
310
+ libav.AVSEEK_FLAG_BACKWARD ?? 0,
311
+ );
312
+ } catch (err) {
313
+ console.warn("[avbridge] hybrid av_seek_frame failed:", err);
314
+ }
315
+
316
+ // Flush WebCodecs VideoDecoder
317
+ try {
318
+ if (videoDecoder && videoDecoder.state === "configured") {
319
+ await videoDecoder.flush();
320
+ }
321
+ } catch { /* ignore */ }
322
+
323
+ // Flush libav audio decoder
324
+ try {
325
+ if (audioDec) await libav.avcodec_flush_buffers?.(audioDec.c);
326
+ } catch { /* ignore */ }
327
+
328
+ syntheticVideoUs = Math.round(timeSec * 1_000_000);
329
+ syntheticAudioUs = Math.round(timeSec * 1_000_000);
330
+
331
+ pumpRunning = pumpLoop(newToken).catch((err) =>
332
+ console.error("[avbridge] hybrid pump failed (post-seek):", err),
333
+ );
334
+ },
335
+
336
+ stats() {
337
+ return {
338
+ decoderType: "webcodecs-hybrid",
339
+ packetsRead,
340
+ videoFramesDecoded,
341
+ videoChunksFed,
342
+ audioFramesDecoded,
343
+ videoDecodeQueueSize: videoDecoder?.decodeQueueSize ?? 0,
344
+ ...opts.renderer.stats(),
345
+ ...opts.audio.stats(),
346
+ };
347
+ },
348
+ };
349
+ }
350
+
351
+ // ─────────────────────────────────────────────────────────────────────────────
352
+ // Packet timestamp sanitizer for WebCodecs chunks.
353
+ //
354
+ // AVI packets often have AV_NOPTS_VALUE. The bridge's packetToEncodedVideoChunk
355
+ // uses the packet's pts + time_base. We normalize to microseconds with a 1/1e6
356
+ // time_base to avoid overflow.
357
+ // ─────────────────────────────────────────────────────────────────────────────
358
+
359
+ function sanitizePacketTimestamp(
360
+ pkt: LibavPacket,
361
+ nextUs: () => number,
362
+ fallbackTimeBase?: [number, number],
363
+ ): void {
364
+ const lo = pkt.pts ?? 0;
365
+ const hi = pkt.ptshi ?? 0;
366
+ const isInvalid = (hi === -2147483648 && lo === 0) || !Number.isFinite(lo);
367
+ if (isInvalid) {
368
+ const us = nextUs();
369
+ pkt.pts = us;
370
+ pkt.ptshi = 0;
371
+ pkt.time_base_num = 1;
372
+ pkt.time_base_den = 1_000_000;
373
+ return;
374
+ }
375
+ const tb = fallbackTimeBase ?? [1, 1_000_000];
376
+ const pts64 = hi * 0x100000000 + lo;
377
+ const us = Math.round((pts64 * 1_000_000 * tb[0]) / tb[1]);
378
+ if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
379
+ pkt.pts = us;
380
+ pkt.ptshi = us < 0 ? -1 : 0;
381
+ pkt.time_base_num = 1;
382
+ pkt.time_base_den = 1_000_000;
383
+ return;
384
+ }
385
+ const fallback = nextUs();
386
+ pkt.pts = fallback;
387
+ pkt.ptshi = 0;
388
+ pkt.time_base_num = 1;
389
+ pkt.time_base_den = 1_000_000;
390
+ }
391
+
392
+ // Frame timestamp sanitizer (same as fallback/decoder.ts, for audio frames)
393
+ function sanitizeFrameTimestamp(
394
+ frame: LibavFrame,
395
+ nextUs: () => number,
396
+ fallbackTimeBase?: [number, number],
397
+ ): void {
398
+ const lo = frame.pts ?? 0;
399
+ const hi = frame.ptshi ?? 0;
400
+ const isInvalid = (hi === -2147483648 && lo === 0) || !Number.isFinite(lo);
401
+ if (isInvalid) {
402
+ const us = nextUs();
403
+ frame.pts = us;
404
+ frame.ptshi = 0;
405
+ return;
406
+ }
407
+ const tb = fallbackTimeBase ?? [1, 1_000_000];
408
+ const pts64 = hi * 0x100000000 + lo;
409
+ const us = Math.round((pts64 * 1_000_000 * tb[0]) / tb[1]);
410
+ if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
411
+ frame.pts = us;
412
+ frame.ptshi = us < 0 ? -1 : 0;
413
+ return;
414
+ }
415
+ const fallback = nextUs();
416
+ frame.pts = fallback;
417
+ frame.ptshi = 0;
418
+ }
419
+
420
+ // ─────────────────────────────────────────────────────────────────────────────
421
+ // Audio frame → interleaved Float32 (duplicated from fallback/decoder.ts)
422
+ // ─────────────────────────────────────────────────────────────────────────────
423
+
424
+ const AV_SAMPLE_FMT_U8 = 0;
425
+ const AV_SAMPLE_FMT_S16 = 1;
426
+ const AV_SAMPLE_FMT_S32 = 2;
427
+ const AV_SAMPLE_FMT_FLT = 3;
428
+ const AV_SAMPLE_FMT_U8P = 5;
429
+ const AV_SAMPLE_FMT_S16P = 6;
430
+ const AV_SAMPLE_FMT_S32P = 7;
431
+ const AV_SAMPLE_FMT_FLTP = 8;
432
+
433
+ interface InterleavedSamples {
434
+ data: Float32Array;
435
+ channels: number;
436
+ sampleRate: number;
437
+ }
438
+
439
+ function libavFrameToInterleavedFloat32(frame: LibavFrame): InterleavedSamples | null {
440
+ const channels = frame.channels ?? frame.ch_layout_nb_channels ?? 1;
441
+ const sampleRate = frame.sample_rate ?? 44100;
442
+ const nbSamples = frame.nb_samples ?? 0;
443
+ if (nbSamples === 0) return null;
444
+
445
+ const out = new Float32Array(nbSamples * channels);
446
+
447
+ switch (frame.format) {
448
+ case AV_SAMPLE_FMT_FLTP: {
449
+ const planes = ensurePlanes(frame.data, channels);
450
+ for (let ch = 0; ch < channels; ch++) {
451
+ const plane = asFloat32(planes[ch]);
452
+ for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i];
453
+ }
454
+ return { data: out, channels, sampleRate };
455
+ }
456
+ case AV_SAMPLE_FMT_FLT: {
457
+ const flat = asFloat32(frame.data);
458
+ for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i];
459
+ return { data: out, channels, sampleRate };
460
+ }
461
+ case AV_SAMPLE_FMT_S16P: {
462
+ const planes = ensurePlanes(frame.data, channels);
463
+ for (let ch = 0; ch < channels; ch++) {
464
+ const plane = asInt16(planes[ch]);
465
+ for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 32768;
466
+ }
467
+ return { data: out, channels, sampleRate };
468
+ }
469
+ case AV_SAMPLE_FMT_S16: {
470
+ const flat = asInt16(frame.data);
471
+ for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 32768;
472
+ return { data: out, channels, sampleRate };
473
+ }
474
+ case AV_SAMPLE_FMT_S32P: {
475
+ const planes = ensurePlanes(frame.data, channels);
476
+ for (let ch = 0; ch < channels; ch++) {
477
+ const plane = asInt32(planes[ch]);
478
+ for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 2147483648;
479
+ }
480
+ return { data: out, channels, sampleRate };
481
+ }
482
+ case AV_SAMPLE_FMT_S32: {
483
+ const flat = asInt32(frame.data);
484
+ for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 2147483648;
485
+ return { data: out, channels, sampleRate };
486
+ }
487
+ case AV_SAMPLE_FMT_U8P: {
488
+ const planes = ensurePlanes(frame.data, channels);
489
+ for (let ch = 0; ch < channels; ch++) {
490
+ const plane = asUint8(planes[ch]);
491
+ for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = (plane[i] - 128) / 128;
492
+ }
493
+ return { data: out, channels, sampleRate };
494
+ }
495
+ case AV_SAMPLE_FMT_U8: {
496
+ const flat = asUint8(frame.data);
497
+ for (let i = 0; i < nbSamples * channels; i++) out[i] = (flat[i] - 128) / 128;
498
+ return { data: out, channels, sampleRate };
499
+ }
500
+ default:
501
+ return null;
502
+ }
503
+ }
504
+
505
+ function ensurePlanes(data: unknown, channels: number): unknown[] {
506
+ if (Array.isArray(data)) return data;
507
+ const arr = data as { length: number; subarray?: (a: number, b: number) => unknown };
508
+ const len = arr.length;
509
+ const perChannel = Math.floor(len / channels);
510
+ const planes: unknown[] = [];
511
+ for (let ch = 0; ch < channels; ch++) {
512
+ planes.push(arr.subarray ? arr.subarray(ch * perChannel, (ch + 1) * perChannel) : arr);
513
+ }
514
+ return planes;
515
+ }
516
+
517
+ function asFloat32(x: unknown): Float32Array {
518
+ if (x instanceof Float32Array) return x;
519
+ const ta = x as { buffer: ArrayBuffer; byteOffset: number; byteLength: number };
520
+ return new Float32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
521
+ }
522
+ function asInt16(x: unknown): Int16Array {
523
+ if (x instanceof Int16Array) return x;
524
+ const ta = x as { buffer: ArrayBuffer; byteOffset: number; byteLength: number };
525
+ return new Int16Array(ta.buffer, ta.byteOffset, ta.byteLength / 2);
526
+ }
527
+ function asInt32(x: unknown): Int32Array {
528
+ if (x instanceof Int32Array) return x;
529
+ const ta = x as { buffer: ArrayBuffer; byteOffset: number; byteLength: number };
530
+ return new Int32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
531
+ }
532
+ function asUint8(x: unknown): Uint8Array {
533
+ if (x instanceof Uint8Array) return x;
534
+ const ta = x as { buffer: ArrayBuffer; byteOffset: number; byteLength: number };
535
+ return new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength);
536
+ }
537
+
538
+ // ─────────────────────────────────────────────────────────────────────────────
539
+ // Bridge loader
540
+ // ─────────────────────────────────────────────────────────────────────────────
541
+
542
+ async function loadBridge(): Promise<BridgeModule> {
543
+ try {
544
+ const wrapper = await import("../fallback/libav-import.js");
545
+ return wrapper.libavBridge as unknown as BridgeModule;
546
+ } catch (err) {
547
+ throw new Error(
548
+ `failed to load libavjs-webcodecs-bridge: ${(err as Error).message}`,
549
+ );
550
+ }
551
+ }
552
+
553
+ // ─────────────────────────────────────────────────────────────────────────────
554
+ // Structural types
555
+ // ─────────────────────────────────────────────────────────────────────────────
556
+
557
+ interface SoftDecoder {
558
+ c: number;
559
+ pkt: number;
560
+ frame: number;
561
+ }
562
+
563
+ interface LibavStream {
564
+ index: number;
565
+ codec_type: number;
566
+ codec_id: number;
567
+ codecpar: number;
568
+ time_base_num?: number;
569
+ time_base_den?: number;
570
+ }
571
+
572
+ interface LibavPacket {
573
+ data: Uint8Array;
574
+ pts: number;
575
+ ptshi?: number;
576
+ duration?: number;
577
+ durationhi?: number;
578
+ flags: number;
579
+ stream_index: number;
580
+ time_base_num?: number;
581
+ time_base_den?: number;
582
+ }
583
+
584
+ interface LibavFrame {
585
+ data: unknown;
586
+ format: number;
587
+ channels?: number;
588
+ ch_layout_nb_channels?: number;
589
+ sample_rate?: number;
590
+ nb_samples?: number;
591
+ pts?: number;
592
+ ptshi?: number;
593
+ width?: number;
594
+ height?: number;
595
+ }
596
+
597
+ interface LibavRuntime {
598
+ AVMEDIA_TYPE_VIDEO: number;
599
+ AVMEDIA_TYPE_AUDIO: number;
600
+ AVERROR_EOF: number;
601
+ EAGAIN: number;
602
+ AVSEEK_FLAG_BACKWARD?: number;
603
+
604
+ mkreadaheadfile(name: string, blob: Blob): Promise<void>;
605
+ unlinkreadaheadfile(name: string): Promise<void>;
606
+ ff_init_demuxer_file(name: string): Promise<[number, LibavStream[]]>;
607
+ ff_read_frame_multi(
608
+ fmt_ctx: number,
609
+ pkt: number,
610
+ opts?: { limit?: number },
611
+ ): Promise<[number, Record<number, LibavPacket[]>]>;
612
+ ff_init_decoder(
613
+ codec: number | string,
614
+ config?: { codecpar?: number; time_base?: [number, number] },
615
+ ): Promise<[number, number, number, number]>;
616
+ ff_decode_multi(
617
+ c: number,
618
+ pkt: number,
619
+ frame: number,
620
+ packets: LibavPacket[],
621
+ opts?: { fin?: boolean; ignoreErrors?: boolean },
622
+ ): Promise<LibavFrame[]>;
623
+ ff_free_decoder?(c: number, pkt: number, frame: number): Promise<void>;
624
+ av_packet_alloc(): Promise<number>;
625
+ av_packet_free?(pkt: number): Promise<void>;
626
+ av_seek_frame(
627
+ fmt_ctx: number,
628
+ stream: number,
629
+ tsLo: number,
630
+ tsHi: number,
631
+ flags: number,
632
+ ): Promise<number>;
633
+ avcodec_flush_buffers?(c: number): Promise<void>;
634
+ avformat_close_input_js(ctx: number): Promise<void>;
635
+ f64toi64?(val: number): [number, number];
636
+ }
637
+
638
+ interface BridgeModule {
639
+ videoStreamToConfig(libav: unknown, stream: unknown): Promise<VideoDecoderConfig | null>;
640
+ packetToEncodedVideoChunk(pkt: unknown, stream: unknown): EncodedVideoChunk;
641
+ }