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,660 @@
1
+ /**
2
+ * libav.js demux + decode loop for the fallback strategy.
3
+ *
4
+ * Design:
5
+ *
6
+ * - **Always software decode.** The fallback strategy is only entered when
7
+ * classification has decided no browser decoder will handle the codec set,
8
+ * so the WebCodecs hardware path is dead weight here. Going through libav
9
+ * uniformly also avoids brittleness around `EncodedAudioChunk` framing for
10
+ * codecs like MP3-in-AVI where the browser's AudioDecoder rejects libav's
11
+ * raw demuxed packets.
12
+ *
13
+ * - **Cancellable pump loop.** Each pump iteration is gated on a token that
14
+ * `seek()` increments. When the token changes mid-batch, the loop exits
15
+ * and a fresh one starts at the new position. This is how seek interrupts
16
+ * the decoder cleanly without having to await an arbitrarily long
17
+ * `ff_decode_multi` call.
18
+ *
19
+ * - **Synthetic timestamps.** AVI demuxers report `AV_NOPTS_VALUE` for most
20
+ * packets — they're frame-indexed, not time-indexed. We replace any
21
+ * invalid pts with a per-stream synthetic counter (frame index × 1e6/fps
22
+ * for video; sample-accurate for audio) so the bridge's chunk constructor
23
+ * doesn't overflow int64.
24
+ */
25
+
26
+ import { loadLibav, type LibavVariant } from "./libav-loader.js";
27
+ import { VideoRenderer } from "./video-renderer.js";
28
+ import { AudioOutput } from "./audio-output.js";
29
+ import type { MediaContext } from "../../types.js";
30
+ import { pickLibavVariant } from "./variant-routing.js";
31
+
32
+ export interface DecoderHandles {
33
+ destroy(): Promise<void>;
34
+ /** Seek to the given time in seconds. Returns once the new pump has been kicked off. */
35
+ seek(timeSec: number): Promise<void>;
36
+ stats(): Record<string, unknown>;
37
+ }
38
+
39
+ export interface StartDecoderOptions {
40
+ /** Normalized source — either a Blob in memory or a URL we'll stream via Range requests. */
41
+ source: import("../../util/source.js").NormalizedSource;
42
+ filename: string;
43
+ context: MediaContext;
44
+ renderer: VideoRenderer;
45
+ audio: AudioOutput;
46
+ }
47
+
48
+ export async function startDecoder(opts: StartDecoderOptions): Promise<DecoderHandles> {
49
+ const variant: LibavVariant = pickLibavVariant(opts.context);
50
+ const libav = (await loadLibav(variant)) as unknown as LibavRuntime;
51
+ const bridge = await loadBridge();
52
+
53
+ // For URL sources, prepareLibavInput attaches an HTTP block reader so
54
+ // libav demuxes via Range requests. For Blob sources, it falls back to
55
+ // mkreadaheadfile (in-memory). The returned handle owns cleanup.
56
+ const { prepareLibavInput } = await import("../../util/libav-http-reader.js");
57
+ const inputHandle = await prepareLibavInput(libav as unknown as Parameters<typeof prepareLibavInput>[0], opts.filename, opts.source);
58
+
59
+ // Pre-allocate one AVPacket for ff_read_frame_multi to reuse.
60
+ const readPkt = await libav.av_packet_alloc();
61
+
62
+ const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
63
+ const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
64
+ const audioStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO) ?? null;
65
+
66
+ if (!videoStream && !audioStream) {
67
+ throw new Error("fallback decoder: file has no decodable streams");
68
+ }
69
+
70
+ // ── Set up software decoders ─────────────────────────────────────────
71
+ let videoDec: SoftDecoder | null = null;
72
+ let audioDec: SoftDecoder | null = null;
73
+ let videoTimeBase: [number, number] | undefined;
74
+ let audioTimeBase: [number, number] | undefined;
75
+
76
+ if (videoStream) {
77
+ try {
78
+ const [, c, pkt, frame] = await libav.ff_init_decoder(videoStream.codec_id, {
79
+ codecpar: videoStream.codecpar,
80
+ });
81
+ videoDec = { c, pkt, frame };
82
+ if (videoStream.time_base_num && videoStream.time_base_den) {
83
+ videoTimeBase = [videoStream.time_base_num, videoStream.time_base_den];
84
+ }
85
+ } catch (err) {
86
+ console.error("[avbridge] failed to init video decoder:", err);
87
+ }
88
+ }
89
+
90
+ if (audioStream) {
91
+ try {
92
+ const [, c, pkt, frame] = await libav.ff_init_decoder(audioStream.codec_id, {
93
+ codecpar: audioStream.codecpar,
94
+ });
95
+ audioDec = { c, pkt, frame };
96
+ if (audioStream.time_base_num && audioStream.time_base_den) {
97
+ audioTimeBase = [audioStream.time_base_num, audioStream.time_base_den];
98
+ }
99
+ } catch (err) {
100
+ console.warn(
101
+ "[avbridge] fallback: audio decoder unavailable — playing video with wall-clock timing:",
102
+ (err as Error).message,
103
+ );
104
+ }
105
+ }
106
+
107
+ // No audio decoder? Switch audio output into wall-clock mode so video can
108
+ // play even when the audio codec isn't supported by the loaded libav variant.
109
+ if (!audioDec) {
110
+ opts.audio.setNoAudio();
111
+ }
112
+
113
+ if (!videoDec && !audioDec) {
114
+ await inputHandle.detach().catch(() => {});
115
+ const codecs = [
116
+ videoStream ? `video: ${opts.context.videoTracks[0]?.codec ?? "unknown"}` : null,
117
+ audioStream ? `audio: ${opts.context.audioTracks[0]?.codec ?? "unknown"}` : null,
118
+ ].filter(Boolean).join(", ");
119
+ const hint = variant === "webcodecs"
120
+ ? ` The "${variant}" libav variant does not include software decoders for these codecs. ` +
121
+ `Try the custom "avbridge" variant (scripts/build-libav.sh) for broader codec support, ` +
122
+ `or use a lighter strategy (native, remux, hybrid) instead.`
123
+ : "";
124
+ throw new Error(
125
+ `fallback decoder: could not initialize any libav decoders (${codecs}).${hint}`,
126
+ );
127
+ }
128
+
129
+ // ── Mutable state shared across pump loops ───────────────────────────
130
+ let destroyed = false;
131
+ let pumpToken = 0; // bumped on seek; pump loops bail when token changes
132
+ let pumpRunning: Promise<void> | null = null;
133
+
134
+ let packetsRead = 0;
135
+ let videoFramesDecoded = 0;
136
+ let audioFramesDecoded = 0;
137
+
138
+ // Synthetic timestamp counters. Reset on seek.
139
+ let syntheticVideoUs = 0;
140
+ let syntheticAudioUs = 0;
141
+
142
+ const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
143
+ const videoFps = videoTrackInfo?.fps && videoTrackInfo.fps > 0 ? videoTrackInfo.fps : 30;
144
+ const videoFrameStepUs = Math.max(1, Math.round(1_000_000 / videoFps));
145
+
146
+ // ── Pump loop ─────────────────────────────────────────────────────────
147
+
148
+ async function pumpLoop(myToken: number): Promise<void> {
149
+ while (!destroyed && myToken === pumpToken) {
150
+ let readErr: number;
151
+ let packets: Record<number, LibavPacket[]>;
152
+ try {
153
+ // Smaller batch = fewer frames per decode round = less queue burst.
154
+ // 16 KB ≈ 4 video packets + ~12 audio packets at typical DivX
155
+ // bitrates. The renderer drains ~1 frame per 33ms rAF tick, so
156
+ // keeping bursts ≤ 4-6 frames prevents queue overflow.
157
+ [readErr, packets] = await libav.ff_read_frame_multi(fmt_ctx, readPkt, {
158
+ limit: 16 * 1024,
159
+ });
160
+ } catch (err) {
161
+ console.error("[avbridge] ff_read_frame_multi failed:", err);
162
+ return;
163
+ }
164
+
165
+ if (myToken !== pumpToken || destroyed) return;
166
+
167
+ const videoPackets = videoStream ? packets[videoStream.index] : undefined;
168
+ const audioPackets = audioStream ? packets[audioStream.index] : undefined;
169
+
170
+ if (videoDec && videoPackets && videoPackets.length > 0) {
171
+ await decodeVideoBatch(videoPackets, myToken);
172
+ }
173
+ if (myToken !== pumpToken || destroyed) return;
174
+ if (audioDec && audioPackets && audioPackets.length > 0) {
175
+ await decodeAudioBatch(audioPackets, myToken);
176
+ }
177
+
178
+ packetsRead += (videoPackets?.length ?? 0) + (audioPackets?.length ?? 0);
179
+
180
+ // Throttle: don't run too far ahead of playback. Two backpressure
181
+ // signals:
182
+ // - Audio buffer (mediaTimeOfNext - now()) > 2 sec — we have
183
+ // plenty of audio scheduled.
184
+ // - Renderer queue depth >= queueHighWater — the canvas can't
185
+ // drain fast enough. Without this, fast software decode of
186
+ // small frames piles up in the renderer and overflows.
187
+ while (
188
+ !destroyed &&
189
+ myToken === pumpToken &&
190
+ (opts.audio.bufferAhead() > 2.0 ||
191
+ opts.renderer.queueDepth() >= opts.renderer.queueHighWater)
192
+ ) {
193
+ await new Promise((r) => setTimeout(r, 50));
194
+ }
195
+
196
+ if (readErr === libav.AVERROR_EOF) {
197
+ if (videoDec) await decodeVideoBatch([], myToken, /*flush*/ true);
198
+ if (audioDec) await decodeAudioBatch([], myToken, /*flush*/ true);
199
+ return;
200
+ }
201
+ if (readErr && readErr !== 0 && readErr !== -libav.EAGAIN) {
202
+ console.warn("[avbridge] ff_read_frame_multi returned", readErr);
203
+ return;
204
+ }
205
+ }
206
+ }
207
+
208
+ async function decodeVideoBatch(pkts: LibavPacket[], myToken: number, flush = false) {
209
+ if (!videoDec || destroyed || myToken !== pumpToken) return;
210
+ let frames: LibavFrame[];
211
+ try {
212
+ frames = await libav.ff_decode_multi(
213
+ videoDec.c,
214
+ videoDec.pkt,
215
+ videoDec.frame,
216
+ pkts,
217
+ flush ? { fin: true, ignoreErrors: true } : { ignoreErrors: true },
218
+ );
219
+ } catch (err) {
220
+ console.error("[avbridge] video decode batch failed:", err);
221
+ return;
222
+ }
223
+ if (myToken !== pumpToken || destroyed) return;
224
+
225
+ for (const f of frames) {
226
+ if (myToken !== pumpToken || destroyed) return;
227
+ const bridgeOpts = sanitizeFrameTimestamp(
228
+ f,
229
+ () => {
230
+ const ts = syntheticVideoUs;
231
+ syntheticVideoUs += videoFrameStepUs;
232
+ return ts;
233
+ },
234
+ videoTimeBase,
235
+ );
236
+ try {
237
+ const vf = bridge.laFrameToVideoFrame(f, bridgeOpts);
238
+ opts.renderer.enqueue(vf);
239
+ videoFramesDecoded++;
240
+ } catch (err) {
241
+ if (videoFramesDecoded === 0) {
242
+ console.warn("[avbridge] laFrameToVideoFrame failed:", err);
243
+ }
244
+ }
245
+ }
246
+ }
247
+
248
+ async function decodeAudioBatch(pkts: LibavPacket[], myToken: number, flush = false) {
249
+ if (!audioDec || destroyed || myToken !== pumpToken) return;
250
+ let frames: LibavFrame[];
251
+ try {
252
+ frames = await libav.ff_decode_multi(
253
+ audioDec.c,
254
+ audioDec.pkt,
255
+ audioDec.frame,
256
+ pkts,
257
+ flush ? { fin: true, ignoreErrors: true } : { ignoreErrors: true },
258
+ );
259
+ } catch (err) {
260
+ console.error("[avbridge] audio decode batch failed:", err);
261
+ return;
262
+ }
263
+ if (myToken !== pumpToken || destroyed) return;
264
+
265
+ for (const f of frames) {
266
+ if (myToken !== pumpToken || destroyed) return;
267
+ sanitizeFrameTimestamp(
268
+ f,
269
+ () => {
270
+ const ts = syntheticAudioUs;
271
+ const samples = f.nb_samples ?? 1024;
272
+ const sampleRate = f.sample_rate ?? 44100;
273
+ syntheticAudioUs += Math.round((samples * 1_000_000) / sampleRate);
274
+ return ts;
275
+ },
276
+ audioTimeBase,
277
+ );
278
+ const samples = libavFrameToInterleavedFloat32(f);
279
+ if (samples) {
280
+ opts.audio.schedule(samples.data, samples.channels, samples.sampleRate);
281
+ audioFramesDecoded++;
282
+ }
283
+ }
284
+ }
285
+
286
+ // Kick off the initial pump.
287
+ pumpToken = 1;
288
+ pumpRunning = pumpLoop(pumpToken).catch((err) =>
289
+ console.error("[avbridge] decoder pump failed:", err),
290
+ );
291
+
292
+ return {
293
+ async destroy() {
294
+ destroyed = true;
295
+ pumpToken++;
296
+ try { await pumpRunning; } catch { /* ignore */ }
297
+ try { if (videoDec) await libav.ff_free_decoder?.(videoDec.c, videoDec.pkt, videoDec.frame); } catch { /* ignore */ }
298
+ try { if (audioDec) await libav.ff_free_decoder?.(audioDec.c, audioDec.pkt, audioDec.frame); } catch { /* ignore */ }
299
+ try { await libav.av_packet_free?.(readPkt); } catch { /* ignore */ }
300
+ try { await libav.avformat_close_input_js(fmt_ctx); } catch { /* ignore */ }
301
+ try { await inputHandle.detach(); } catch { /* ignore */ }
302
+ },
303
+
304
+ async seek(timeSec) {
305
+ // Cancel the current pump and wait for it to actually exit before
306
+ // we start moving file pointers around — concurrent ff_decode_multi
307
+ // and av_seek_frame on the same context would be a recipe for memory
308
+ // corruption inside libav.
309
+ const newToken = ++pumpToken;
310
+ if (pumpRunning) {
311
+ try { await pumpRunning; } catch { /* ignore */ }
312
+ }
313
+ if (destroyed) return;
314
+
315
+ try {
316
+ // libav.js's `av_seek_frame` takes the timestamp as a *split*
317
+ // (lo, hi) int64 pair, NOT a single number. The function signature
318
+ // is: av_seek_frame(s, stream_index, tsLo, tsHi, flags). Passing a
319
+ // single number put AVSEEK_FLAG_BACKWARD (1) into tsHi, which
320
+ // produced a bogus int64 = 4.29e9 + tsLo ≈ 73 min for any small
321
+ // seek target — seeking past EOF and stalling the pump.
322
+ const tsUs = Math.floor(timeSec * 1_000_000);
323
+ const [tsLo, tsHi] = libav.f64toi64
324
+ ? libav.f64toi64(tsUs)
325
+ : [tsUs | 0, Math.floor(tsUs / 0x100000000)];
326
+ await libav.av_seek_frame(
327
+ fmt_ctx,
328
+ -1,
329
+ tsLo,
330
+ tsHi,
331
+ libav.AVSEEK_FLAG_BACKWARD ?? 0,
332
+ );
333
+ } catch (err) {
334
+ console.warn("[avbridge] av_seek_frame failed:", err);
335
+ }
336
+
337
+ // Reset the decoder state. After the previous pump exited via the
338
+ // EOF path it called ff_decode_multi with `fin: true`, which sends a
339
+ // NULL packet to the decoder and puts it in drain mode — meaning all
340
+ // subsequent decode calls return EOF. `avcodec_flush_buffers` clears
341
+ // that state so a fresh stream of post-seek packets is accepted.
342
+ // Also clears any internal frame reordering buffer, which is what we
343
+ // want anyway since we just changed positions.
344
+ try {
345
+ if (videoDec) await libav.avcodec_flush_buffers?.(videoDec.c);
346
+ } catch { /* ignore */ }
347
+ try {
348
+ if (audioDec) await libav.avcodec_flush_buffers?.(audioDec.c);
349
+ } catch { /* ignore */ }
350
+
351
+ // Reset synthetic timestamp counters to the seek target so newly
352
+ // decoded frames start at the right media time.
353
+ syntheticVideoUs = Math.round(timeSec * 1_000_000);
354
+ syntheticAudioUs = Math.round(timeSec * 1_000_000);
355
+
356
+ // The renderer & audio output are reset by the fallback session
357
+ // wrapper that called us — see strategies/fallback/index.ts.
358
+
359
+ // Start a fresh pump for the new token.
360
+ pumpRunning = pumpLoop(newToken).catch((err) =>
361
+ console.error("[avbridge] decoder pump failed (post-seek):", err),
362
+ );
363
+ },
364
+
365
+ stats() {
366
+ return {
367
+ decoderType: "libav-wasm",
368
+ packetsRead,
369
+ videoFramesDecoded,
370
+ audioFramesDecoded,
371
+ ...opts.renderer.stats(),
372
+ ...opts.audio.stats(),
373
+ };
374
+ },
375
+ };
376
+ }
377
+
378
+ // ─────────────────────────────────────────────────────────────────────────────
379
+ // Frame timestamp sanitizer.
380
+ //
381
+ // libav can hand back decoded frames with `pts = AV_NOPTS_VALUE` (encoded as
382
+ // ptshi = -2147483648, pts = 0) for inputs whose demuxer can't determine
383
+ // presentation times. AVI is the canonical example. The bridge's
384
+ // `laFrameToVideoFrame` then multiplies pts × 1e6 × tbNum / tbDen and
385
+ // overflows int64, throwing "Value is outside the 'long long' value range".
386
+ //
387
+ // Fix: replace any invalid pts with a synthetic microsecond counter, force
388
+ // the frame's pts/ptshi to that value, and tell the bridge to use a 1/1e6
389
+ // timebase so it does an identity conversion.
390
+ // ─────────────────────────────────────────────────────────────────────────────
391
+
392
+ interface BridgeOpts {
393
+ timeBase?: [number, number];
394
+ transfer?: boolean;
395
+ }
396
+
397
+ function sanitizeFrameTimestamp(
398
+ frame: LibavFrame,
399
+ nextUs: () => number,
400
+ fallbackTimeBase?: [number, number],
401
+ ): BridgeOpts {
402
+ const lo = frame.pts ?? 0;
403
+ const hi = frame.ptshi ?? 0;
404
+ const isInvalid = (hi === -2147483648 && lo === 0) || !Number.isFinite(lo);
405
+ if (isInvalid) {
406
+ const us = nextUs();
407
+ frame.pts = us;
408
+ frame.ptshi = 0;
409
+ return { timeBase: [1, 1_000_000] };
410
+ }
411
+ const tb = fallbackTimeBase ?? [1, 1_000_000];
412
+ const pts64 = hi * 0x100000000 + lo;
413
+ const us = Math.round((pts64 * 1_000_000 * tb[0]) / tb[1]);
414
+ if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
415
+ frame.pts = us;
416
+ frame.ptshi = us < 0 ? -1 : 0;
417
+ return { timeBase: [1, 1_000_000] };
418
+ }
419
+ const fallback = nextUs();
420
+ frame.pts = fallback;
421
+ frame.ptshi = 0;
422
+ return { timeBase: [1, 1_000_000] };
423
+ }
424
+
425
+ // ─────────────────────────────────────────────────────────────────────────────
426
+ // libav decoded `Frame` → interleaved Float32Array (the format AudioOutput
427
+ // schedules).
428
+ // ─────────────────────────────────────────────────────────────────────────────
429
+
430
+ const AV_SAMPLE_FMT_U8 = 0;
431
+ const AV_SAMPLE_FMT_S16 = 1;
432
+ const AV_SAMPLE_FMT_S32 = 2;
433
+ const AV_SAMPLE_FMT_FLT = 3;
434
+ const AV_SAMPLE_FMT_U8P = 5;
435
+ const AV_SAMPLE_FMT_S16P = 6;
436
+ const AV_SAMPLE_FMT_S32P = 7;
437
+ const AV_SAMPLE_FMT_FLTP = 8;
438
+
439
+ interface InterleavedSamples {
440
+ data: Float32Array;
441
+ channels: number;
442
+ sampleRate: number;
443
+ }
444
+
445
+ function libavFrameToInterleavedFloat32(frame: LibavFrame): InterleavedSamples | null {
446
+ const channels = frame.channels ?? frame.ch_layout_nb_channels ?? 1;
447
+ const sampleRate = frame.sample_rate ?? 44100;
448
+ const nbSamples = frame.nb_samples ?? 0;
449
+ if (nbSamples === 0) return null;
450
+
451
+ const out = new Float32Array(nbSamples * channels);
452
+
453
+ switch (frame.format) {
454
+ case AV_SAMPLE_FMT_FLTP: {
455
+ const planes = ensurePlanes(frame.data, channels);
456
+ for (let ch = 0; ch < channels; ch++) {
457
+ const plane = asFloat32(planes[ch]);
458
+ for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i];
459
+ }
460
+ return { data: out, channels, sampleRate };
461
+ }
462
+ case AV_SAMPLE_FMT_FLT: {
463
+ const flat = asFloat32(frame.data);
464
+ for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i];
465
+ return { data: out, channels, sampleRate };
466
+ }
467
+ case AV_SAMPLE_FMT_S16P: {
468
+ const planes = ensurePlanes(frame.data, channels);
469
+ for (let ch = 0; ch < channels; ch++) {
470
+ const plane = asInt16(planes[ch]);
471
+ for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 32768;
472
+ }
473
+ return { data: out, channels, sampleRate };
474
+ }
475
+ case AV_SAMPLE_FMT_S16: {
476
+ const flat = asInt16(frame.data);
477
+ for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 32768;
478
+ return { data: out, channels, sampleRate };
479
+ }
480
+ case AV_SAMPLE_FMT_S32P: {
481
+ const planes = ensurePlanes(frame.data, channels);
482
+ for (let ch = 0; ch < channels; ch++) {
483
+ const plane = asInt32(planes[ch]);
484
+ for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 2147483648;
485
+ }
486
+ return { data: out, channels, sampleRate };
487
+ }
488
+ case AV_SAMPLE_FMT_S32: {
489
+ const flat = asInt32(frame.data);
490
+ for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 2147483648;
491
+ return { data: out, channels, sampleRate };
492
+ }
493
+ case AV_SAMPLE_FMT_U8P: {
494
+ const planes = ensurePlanes(frame.data, channels);
495
+ for (let ch = 0; ch < channels; ch++) {
496
+ const plane = asUint8(planes[ch]);
497
+ for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = (plane[i] - 128) / 128;
498
+ }
499
+ return { data: out, channels, sampleRate };
500
+ }
501
+ case AV_SAMPLE_FMT_U8: {
502
+ const flat = asUint8(frame.data);
503
+ for (let i = 0; i < nbSamples * channels; i++) out[i] = (flat[i] - 128) / 128;
504
+ return { data: out, channels, sampleRate };
505
+ }
506
+ default:
507
+ if (!(globalThis as { __avbridgeLoggedSampleFmt?: number }).__avbridgeLoggedSampleFmt) {
508
+ (globalThis as { __avbridgeLoggedSampleFmt?: number }).__avbridgeLoggedSampleFmt = frame.format;
509
+ console.warn(`[avbridge] unsupported audio sample format from libav: ${frame.format}`);
510
+ }
511
+ return null;
512
+ }
513
+ }
514
+
515
+ function ensurePlanes(data: unknown, channels: number): unknown[] {
516
+ if (Array.isArray(data)) return data;
517
+ const arr = data as { length: number; subarray?: (a: number, b: number) => unknown };
518
+ const len = arr.length;
519
+ const perChannel = Math.floor(len / channels);
520
+ const planes: unknown[] = [];
521
+ for (let ch = 0; ch < channels; ch++) {
522
+ planes.push(arr.subarray ? arr.subarray(ch * perChannel, (ch + 1) * perChannel) : arr);
523
+ }
524
+ return planes;
525
+ }
526
+
527
+ function asFloat32(x: unknown): Float32Array {
528
+ if (x instanceof Float32Array) return x;
529
+ const ta = x as { buffer: ArrayBuffer; byteOffset: number; byteLength: number };
530
+ return new Float32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
531
+ }
532
+ function asInt16(x: unknown): Int16Array {
533
+ if (x instanceof Int16Array) return x;
534
+ const ta = x as { buffer: ArrayBuffer; byteOffset: number; byteLength: number };
535
+ return new Int16Array(ta.buffer, ta.byteOffset, ta.byteLength / 2);
536
+ }
537
+ function asInt32(x: unknown): Int32Array {
538
+ if (x instanceof Int32Array) return x;
539
+ const ta = x as { buffer: ArrayBuffer; byteOffset: number; byteLength: number };
540
+ return new Int32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
541
+ }
542
+ function asUint8(x: unknown): Uint8Array {
543
+ if (x instanceof Uint8Array) return x;
544
+ const ta = x as { buffer: ArrayBuffer; byteOffset: number; byteLength: number };
545
+ return new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength);
546
+ }
547
+
548
+ // ─────────────────────────────────────────────────────────────────────────────
549
+ // Bridge loader (lazy via the static-import wrapper).
550
+ // ─────────────────────────────────────────────────────────────────────────────
551
+
552
+ async function loadBridge(): Promise<BridgeModule> {
553
+ try {
554
+ const wrapper = await import("./libav-import.js");
555
+ return wrapper.libavBridge as unknown as BridgeModule;
556
+ } catch (err) {
557
+ throw new Error(
558
+ `failed to load libavjs-webcodecs-bridge — install the optional peer deps with: ` +
559
+ `npm i libavjs-webcodecs-bridge @libav.js/variant-webcodecs. ` +
560
+ `(${(err as Error).message})`,
561
+ );
562
+ }
563
+ }
564
+
565
+ // ─────────────────────────────────────────────────────────────────────────────
566
+ // Structural types.
567
+ // ─────────────────────────────────────────────────────────────────────────────
568
+
569
+ interface SoftDecoder {
570
+ c: number;
571
+ pkt: number;
572
+ frame: number;
573
+ }
574
+
575
+ interface LibavStream {
576
+ index: number;
577
+ codec_type: number;
578
+ codec_id: number;
579
+ codecpar: number;
580
+ time_base_num?: number;
581
+ time_base_den?: number;
582
+ }
583
+
584
+ interface LibavPacket {
585
+ data: Uint8Array;
586
+ pts: number;
587
+ ptshi?: number;
588
+ duration?: number;
589
+ durationhi?: number;
590
+ flags: number;
591
+ stream_index: number;
592
+ time_base_num?: number;
593
+ time_base_den?: number;
594
+ }
595
+
596
+ interface LibavFrame {
597
+ data: unknown;
598
+ format: number;
599
+ channels?: number;
600
+ ch_layout_nb_channels?: number;
601
+ sample_rate?: number;
602
+ nb_samples?: number;
603
+ pts?: number;
604
+ ptshi?: number;
605
+ width?: number;
606
+ height?: number;
607
+ }
608
+
609
+ interface LibavRuntime {
610
+ AVMEDIA_TYPE_VIDEO: number;
611
+ AVMEDIA_TYPE_AUDIO: number;
612
+ AVERROR_EOF: number;
613
+ EAGAIN: number;
614
+ AVSEEK_FLAG_BACKWARD?: number;
615
+
616
+ mkreadaheadfile(name: string, blob: Blob): Promise<void>;
617
+ unlinkreadaheadfile(name: string): Promise<void>;
618
+ ff_init_demuxer_file(name: string): Promise<[number, LibavStream[]]>;
619
+ ff_read_frame_multi(
620
+ fmt_ctx: number,
621
+ pkt: number,
622
+ opts?: { limit?: number },
623
+ ): Promise<[number, Record<number, LibavPacket[]>]>;
624
+ ff_init_decoder(
625
+ codec: number | string,
626
+ config?: { codecpar?: number; time_base?: [number, number] },
627
+ ): Promise<[number, number, number, number]>;
628
+ ff_decode_multi(
629
+ c: number,
630
+ pkt: number,
631
+ frame: number,
632
+ packets: LibavPacket[],
633
+ opts?: { fin?: boolean; ignoreErrors?: boolean },
634
+ ): Promise<LibavFrame[]>;
635
+ ff_free_decoder?(c: number, pkt: number, frame: number): Promise<void>;
636
+ av_packet_alloc(): Promise<number>;
637
+ av_packet_free?(pkt: number): Promise<void>;
638
+ av_seek_frame(
639
+ fmt_ctx: number,
640
+ stream: number,
641
+ tsLo: number,
642
+ tsHi: number,
643
+ flags: number,
644
+ ): Promise<number>;
645
+ avcodec_flush_buffers?(c: number): Promise<void>;
646
+ avformat_close_input_js(ctx: number): Promise<void>;
647
+ /** Sync helper exposed by libav.js: split a JS number into (lo, hi) int64. */
648
+ f64toi64?(val: number): [number, number];
649
+ }
650
+
651
+ interface BridgeModule {
652
+ laFrameToVideoFrame(
653
+ frame: LibavFrame,
654
+ opts?: { VideoFrame?: unknown; timeBase?: [number, number]; transfer?: boolean },
655
+ ): VideoFrame;
656
+ laFrameToAudioData(
657
+ frame: LibavFrame,
658
+ opts?: { AudioData?: unknown; timeBase?: [number, number] },
659
+ ): AudioData;
660
+ }