avbridge 2.2.1 → 2.5.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 (165) hide show
  1. package/CHANGELOG.md +153 -1
  2. package/NOTICE.md +2 -2
  3. package/README.md +2 -3
  4. package/THIRD_PARTY_LICENSES.md +2 -2
  5. package/dist/avi-2JPBSHGA.js +183 -0
  6. package/dist/avi-2JPBSHGA.js.map +1 -0
  7. package/dist/avi-F6WZJK5T.cjs +185 -0
  8. package/dist/avi-F6WZJK5T.cjs.map +1 -0
  9. package/dist/{avi-GCGM7OJI.js → avi-NJXAXUXK.js} +9 -3
  10. package/dist/avi-NJXAXUXK.js.map +1 -0
  11. package/dist/{avi-6SJLWIWW.cjs → avi-W6L3BTWU.cjs} +10 -4
  12. package/dist/avi-W6L3BTWU.cjs.map +1 -0
  13. package/dist/chunk-2IJ66NTD.cjs +212 -0
  14. package/dist/chunk-2IJ66NTD.cjs.map +1 -0
  15. package/dist/{chunk-ILKDNBSE.js → chunk-2XW2O3YI.cjs} +55 -10
  16. package/dist/chunk-2XW2O3YI.cjs.map +1 -0
  17. package/dist/chunk-5KVLE6YI.js +167 -0
  18. package/dist/chunk-5KVLE6YI.js.map +1 -0
  19. package/dist/chunk-5YAWWKA3.js +18 -0
  20. package/dist/chunk-5YAWWKA3.js.map +1 -0
  21. package/dist/chunk-CPJLFFCC.js +189 -0
  22. package/dist/chunk-CPJLFFCC.js.map +1 -0
  23. package/dist/chunk-CPZ7PXAM.cjs +240 -0
  24. package/dist/chunk-CPZ7PXAM.cjs.map +1 -0
  25. package/dist/{chunk-WD2ZNQA7.js → chunk-DCSOQH2N.js} +7 -4
  26. package/dist/chunk-DCSOQH2N.js.map +1 -0
  27. package/dist/{chunk-HZLQNKFN.cjs → chunk-E76AMWI4.js} +40 -15
  28. package/dist/chunk-E76AMWI4.js.map +1 -0
  29. package/dist/chunk-F3LQJKXK.cjs +20 -0
  30. package/dist/chunk-F3LQJKXK.cjs.map +1 -0
  31. package/dist/chunk-IAYKFGFG.js +200 -0
  32. package/dist/chunk-IAYKFGFG.js.map +1 -0
  33. package/dist/{chunk-DMWARSEF.js → chunk-KY2GPCT7.js} +788 -697
  34. package/dist/chunk-KY2GPCT7.js.map +1 -0
  35. package/dist/chunk-LUFA47FP.js +19 -0
  36. package/dist/chunk-LUFA47FP.js.map +1 -0
  37. package/dist/chunk-NNVOHKXJ.cjs +204 -0
  38. package/dist/chunk-NNVOHKXJ.cjs.map +1 -0
  39. package/dist/chunk-Q2VUO52Z.cjs +374 -0
  40. package/dist/chunk-Q2VUO52Z.cjs.map +1 -0
  41. package/dist/chunk-QDJLQR53.cjs +22 -0
  42. package/dist/chunk-QDJLQR53.cjs.map +1 -0
  43. package/dist/chunk-S4WAZC2T.cjs +173 -0
  44. package/dist/chunk-S4WAZC2T.cjs.map +1 -0
  45. package/dist/chunk-SMH6IOP2.js +368 -0
  46. package/dist/chunk-SMH6IOP2.js.map +1 -0
  47. package/dist/chunk-SR3MPV4D.js +237 -0
  48. package/dist/chunk-SR3MPV4D.js.map +1 -0
  49. package/dist/{chunk-UF2N5L63.cjs → chunk-TBW26OPP.cjs} +800 -710
  50. package/dist/chunk-TBW26OPP.cjs.map +1 -0
  51. package/dist/chunk-X2K3GIWE.js +235 -0
  52. package/dist/chunk-X2K3GIWE.js.map +1 -0
  53. package/dist/{chunk-L4NPOJ36.cjs → chunk-Z33SBWL5.cjs} +7 -4
  54. package/dist/chunk-Z33SBWL5.cjs.map +1 -0
  55. package/dist/chunk-ZCUXHW55.cjs +242 -0
  56. package/dist/chunk-ZCUXHW55.cjs.map +1 -0
  57. package/dist/element-browser.js +1282 -503
  58. package/dist/element-browser.js.map +1 -1
  59. package/dist/element.cjs +59 -5
  60. package/dist/element.cjs.map +1 -1
  61. package/dist/element.d.cts +39 -1
  62. package/dist/element.d.ts +39 -1
  63. package/dist/element.js +58 -4
  64. package/dist/element.js.map +1 -1
  65. package/dist/index.cjs +605 -327
  66. package/dist/index.cjs.map +1 -1
  67. package/dist/index.d.cts +48 -4
  68. package/dist/index.d.ts +48 -4
  69. package/dist/index.js +528 -319
  70. package/dist/index.js.map +1 -1
  71. package/dist/libav-demux-H2GS46GH.cjs +27 -0
  72. package/dist/{libav-http-reader-NQJVY273.js.map → libav-demux-H2GS46GH.cjs.map} +1 -1
  73. package/dist/libav-demux-OWZ4T2YW.js +6 -0
  74. package/dist/{libav-http-reader-FPYDBMYK.cjs.map → libav-demux-OWZ4T2YW.js.map} +1 -1
  75. package/dist/libav-http-reader-AZLE7YFS.cjs +16 -0
  76. package/dist/libav-http-reader-AZLE7YFS.cjs.map +1 -0
  77. package/dist/libav-http-reader-WXG3Z7AI.js +3 -0
  78. package/dist/libav-http-reader-WXG3Z7AI.js.map +1 -0
  79. package/dist/{libav-import-GST2AMPL.cjs → libav-import-2ZVKV2E7.cjs} +2 -2
  80. package/dist/{libav-import-GST2AMPL.cjs.map → libav-import-2ZVKV2E7.cjs.map} +1 -1
  81. package/dist/{libav-import-2JURFHEW.js → libav-import-6MGLCXVQ.js} +2 -2
  82. package/dist/{libav-import-2JURFHEW.js.map → libav-import-6MGLCXVQ.js.map} +1 -1
  83. package/dist/{player-U2NPmFvA.d.cts → player-B6WB74RD.d.cts} +62 -3
  84. package/dist/{player-U2NPmFvA.d.ts → player-B6WB74RD.d.ts} +62 -3
  85. package/dist/player.cjs +5631 -0
  86. package/dist/player.cjs.map +1 -0
  87. package/dist/player.d.cts +699 -0
  88. package/dist/player.d.ts +699 -0
  89. package/dist/player.js +5629 -0
  90. package/dist/player.js.map +1 -0
  91. package/dist/remux-OBSMIENG.cjs +35 -0
  92. package/dist/remux-OBSMIENG.cjs.map +1 -0
  93. package/dist/remux-WBYIZBBX.js +10 -0
  94. package/dist/remux-WBYIZBBX.js.map +1 -0
  95. package/dist/source-4TZ6KMNV.js +4 -0
  96. package/dist/{source-FFZ7TW2B.js.map → source-4TZ6KMNV.js.map} +1 -1
  97. package/dist/source-7YLO6E7X.cjs +29 -0
  98. package/dist/{source-CN43EI7Z.cjs.map → source-7YLO6E7X.cjs.map} +1 -1
  99. package/dist/source-MTX5ELUZ.js +4 -0
  100. package/dist/source-MTX5ELUZ.js.map +1 -0
  101. package/dist/source-VFLXLOCN.cjs +29 -0
  102. package/dist/source-VFLXLOCN.cjs.map +1 -0
  103. package/dist/subtitles-4T74JRGT.js +4 -0
  104. package/dist/subtitles-4T74JRGT.js.map +1 -0
  105. package/dist/subtitles-QUH4LPI4.cjs +29 -0
  106. package/dist/subtitles-QUH4LPI4.cjs.map +1 -0
  107. package/dist/variant-routing-434STYAB.js +3 -0
  108. package/dist/{variant-routing-JOBWXYKD.js.map → variant-routing-434STYAB.js.map} +1 -1
  109. package/dist/variant-routing-HONNAA6R.cjs +12 -0
  110. package/dist/{variant-routing-GOHB2RZN.cjs.map → variant-routing-HONNAA6R.cjs.map} +1 -1
  111. package/package.json +9 -1
  112. package/src/classify/rules.ts +27 -5
  113. package/src/convert/remux.ts +9 -35
  114. package/src/convert/transcode-libav.ts +691 -0
  115. package/src/convert/transcode.ts +53 -12
  116. package/src/element/avbridge-player.ts +861 -0
  117. package/src/element/avbridge-video.ts +54 -0
  118. package/src/element/player-icons.ts +25 -0
  119. package/src/element/player-styles.ts +472 -0
  120. package/src/errors.ts +53 -0
  121. package/src/index.ts +23 -0
  122. package/src/player-element.ts +18 -0
  123. package/src/player.ts +118 -27
  124. package/src/plugins/builtin.ts +2 -2
  125. package/src/probe/avi.ts +4 -0
  126. package/src/probe/index.ts +40 -10
  127. package/src/strategies/fallback/audio-output.ts +31 -0
  128. package/src/strategies/fallback/decoder.ts +179 -175
  129. package/src/strategies/fallback/index.ts +48 -6
  130. package/src/strategies/fallback/libav-import.ts +9 -1
  131. package/src/strategies/fallback/variant-routing.ts +7 -13
  132. package/src/strategies/fallback/video-renderer.ts +231 -32
  133. package/src/strategies/hybrid/decoder.ts +219 -200
  134. package/src/strategies/hybrid/index.ts +48 -7
  135. package/src/strategies/native.ts +6 -3
  136. package/src/strategies/remux/index.ts +14 -2
  137. package/src/strategies/remux/mse.ts +12 -2
  138. package/src/strategies/remux/pipeline.ts +72 -12
  139. package/src/subtitles/index.ts +7 -3
  140. package/src/subtitles/render.ts +8 -0
  141. package/src/types.ts +53 -1
  142. package/src/util/libav-demux.ts +405 -0
  143. package/src/util/libav-http-reader.ts +5 -1
  144. package/src/util/source.ts +28 -8
  145. package/src/util/transport.ts +26 -0
  146. package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.mjs +1 -1
  147. package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.wasm +0 -0
  148. package/dist/avi-6SJLWIWW.cjs.map +0 -1
  149. package/dist/avi-GCGM7OJI.js.map +0 -1
  150. package/dist/chunk-DMWARSEF.js.map +0 -1
  151. package/dist/chunk-HZLQNKFN.cjs.map +0 -1
  152. package/dist/chunk-ILKDNBSE.js.map +0 -1
  153. package/dist/chunk-J5MCMN3S.js +0 -27
  154. package/dist/chunk-J5MCMN3S.js.map +0 -1
  155. package/dist/chunk-L4NPOJ36.cjs.map +0 -1
  156. package/dist/chunk-NZU7W256.cjs +0 -29
  157. package/dist/chunk-NZU7W256.cjs.map +0 -1
  158. package/dist/chunk-UF2N5L63.cjs.map +0 -1
  159. package/dist/chunk-WD2ZNQA7.js.map +0 -1
  160. package/dist/libav-http-reader-FPYDBMYK.cjs +0 -16
  161. package/dist/libav-http-reader-NQJVY273.js +0 -3
  162. package/dist/source-CN43EI7Z.cjs +0 -28
  163. package/dist/source-FFZ7TW2B.js +0 -3
  164. package/dist/variant-routing-GOHB2RZN.cjs +0 -12
  165. package/dist/variant-routing-JOBWXYKD.js +0 -3
@@ -1,219 +1,11 @@
1
1
  'use strict';
2
2
 
3
- var chunkHZLQNKFN_cjs = require('./chunk-HZLQNKFN.cjs');
3
+ var chunkS4WAZC2T_cjs = require('./chunk-S4WAZC2T.cjs');
4
+ var chunkZCUXHW55_cjs = require('./chunk-ZCUXHW55.cjs');
5
+ var chunk2IJ66NTD_cjs = require('./chunk-2IJ66NTD.cjs');
6
+ var chunkCPZ7PXAM_cjs = require('./chunk-CPZ7PXAM.cjs');
4
7
  var chunkG4APZMCP_cjs = require('./chunk-G4APZMCP.cjs');
5
- var chunkNZU7W256_cjs = require('./chunk-NZU7W256.cjs');
6
-
7
- // src/probe/mediabunny.ts
8
- async function probeWithMediabunny(source, sniffedContainer) {
9
- const mb = await import('mediabunny');
10
- const input = new mb.Input({
11
- source: await buildMediabunnySource(mb, source),
12
- formats: mb.ALL_FORMATS
13
- });
14
- const allTracks = await input.getTracks();
15
- const duration = await safeNumber(() => input.computeDuration());
16
- const videoTracks = [];
17
- const audioTracks = [];
18
- for (const track of allTracks) {
19
- if (track.isVideoTrack()) {
20
- const codecParam = await safe(() => track.getCodecParameterString());
21
- videoTracks.push({
22
- id: track.id,
23
- codec: mediabunnyVideoToAvbridge(track.codec),
24
- width: track.displayWidth ?? track.codedWidth ?? 0,
25
- height: track.displayHeight ?? track.codedHeight ?? 0,
26
- codecString: codecParam ?? void 0
27
- });
28
- } else if (track.isAudioTrack()) {
29
- const codecParam = await safe(() => track.getCodecParameterString());
30
- audioTracks.push({
31
- id: track.id,
32
- codec: mediabunnyAudioToAvbridge(track.codec),
33
- channels: track.numberOfChannels ?? 0,
34
- sampleRate: track.sampleRate ?? 0,
35
- language: track.languageCode,
36
- codecString: codecParam ?? void 0
37
- });
38
- }
39
- }
40
- const format = await safe(() => input.getFormat());
41
- const container = resolveContainer(format?.name, sniffedContainer);
42
- return {
43
- source: source.original,
44
- name: source.name,
45
- byteLength: source.byteLength,
46
- container,
47
- videoTracks,
48
- audioTracks,
49
- subtitleTracks: [],
50
- probedBy: "mediabunny",
51
- duration
52
- };
53
- }
54
- async function buildMediabunnySource(mb, source) {
55
- if (source.kind === "url") {
56
- return new mb.UrlSource(source.url);
57
- }
58
- return new mb.BlobSource(source.blob);
59
- }
60
- async function buildMediabunnySourceFromInput(mb, source) {
61
- if (typeof source === "string") return new mb.UrlSource(source);
62
- if (source instanceof URL) return new mb.UrlSource(source.toString());
63
- if (source instanceof Blob) return new mb.BlobSource(source);
64
- if (source instanceof ArrayBuffer) return new mb.BlobSource(new Blob([source]));
65
- if (source instanceof Uint8Array) return new mb.BlobSource(new Blob([source]));
66
- throw new TypeError("unsupported source type for mediabunny");
67
- }
68
- function resolveContainer(formatName, sniffed) {
69
- const name = (formatName ?? "").toLowerCase();
70
- if (name.includes("matroska") || name.includes("mkv")) return "mkv";
71
- if (name.includes("webm")) return "webm";
72
- if (name.includes("mp4") || name.includes("isom")) return "mp4";
73
- if (name.includes("mov") || name.includes("quicktime")) return "mov";
74
- if (name.includes("ogg")) return "ogg";
75
- if (name.includes("wav")) return "wav";
76
- if (name.includes("flac")) return "flac";
77
- if (name.includes("mp3")) return "mp3";
78
- if (name.includes("adts") || name.includes("aac")) return "adts";
79
- if (name.includes("mpegts") || name.includes("mpeg-ts") || name.includes("transport")) return "mpegts";
80
- return sniffed;
81
- }
82
- function mediabunnyVideoToAvbridge(c) {
83
- switch (c) {
84
- case "avc":
85
- return "h264";
86
- case "hevc":
87
- return "h265";
88
- case "vp8":
89
- return "vp8";
90
- case "vp9":
91
- return "vp9";
92
- case "av1":
93
- return "av1";
94
- default:
95
- return c ? c : "unknown";
96
- }
97
- }
98
- function avbridgeVideoToMediabunny(c) {
99
- switch (c) {
100
- case "h264":
101
- return "avc";
102
- case "h265":
103
- return "hevc";
104
- case "vp8":
105
- return "vp8";
106
- case "vp9":
107
- return "vp9";
108
- case "av1":
109
- return "av1";
110
- default:
111
- return null;
112
- }
113
- }
114
- function mediabunnyAudioToAvbridge(c) {
115
- switch (c) {
116
- case "aac":
117
- return "aac";
118
- case "mp3":
119
- return "mp3";
120
- case "opus":
121
- return "opus";
122
- case "vorbis":
123
- return "vorbis";
124
- case "flac":
125
- return "flac";
126
- case "ac3":
127
- return "ac3";
128
- case "eac3":
129
- return "eac3";
130
- default:
131
- return c ? c : "unknown";
132
- }
133
- }
134
- function avbridgeAudioToMediabunny(c) {
135
- switch (c) {
136
- case "aac":
137
- return "aac";
138
- case "mp3":
139
- return "mp3";
140
- case "opus":
141
- return "opus";
142
- case "vorbis":
143
- return "vorbis";
144
- case "flac":
145
- return "flac";
146
- case "ac3":
147
- return "ac3";
148
- case "eac3":
149
- return "eac3";
150
- default:
151
- return null;
152
- }
153
- }
154
- async function safeNumber(fn) {
155
- try {
156
- const v = await fn();
157
- return typeof v === "number" && Number.isFinite(v) ? v : void 0;
158
- } catch {
159
- return void 0;
160
- }
161
- }
162
- async function safe(fn) {
163
- try {
164
- return await fn();
165
- } catch {
166
- return void 0;
167
- }
168
- }
169
-
170
- // src/probe/index.ts
171
- var MEDIABUNNY_CONTAINERS = /* @__PURE__ */ new Set([
172
- "mp4",
173
- "mov",
174
- "mkv",
175
- "webm",
176
- "ogg",
177
- "wav",
178
- "mp3",
179
- "flac",
180
- "adts",
181
- "mpegts"
182
- ]);
183
- async function probe(source) {
184
- const normalized = await chunkHZLQNKFN_cjs.normalizeSource(source);
185
- const sniffed = await chunkHZLQNKFN_cjs.sniffNormalizedSource(normalized);
186
- if (MEDIABUNNY_CONTAINERS.has(sniffed)) {
187
- try {
188
- return await probeWithMediabunny(normalized, sniffed);
189
- } catch (mediabunnyErr) {
190
- console.warn(
191
- `[avbridge] mediabunny rejected ${sniffed} file, falling back to libav:`,
192
- mediabunnyErr.message
193
- );
194
- try {
195
- const { probeWithLibav } = await import('./avi-6SJLWIWW.cjs');
196
- return await probeWithLibav(normalized, sniffed);
197
- } catch (libavErr) {
198
- const mbMsg = mediabunnyErr.message || String(mediabunnyErr);
199
- const lvMsg = libavErr instanceof Error ? libavErr.message : String(libavErr);
200
- throw new Error(
201
- `failed to probe ${sniffed} file. mediabunny: ${mbMsg}. libav fallback: ${lvMsg}.`
202
- );
203
- }
204
- }
205
- }
206
- try {
207
- const { probeWithLibav } = await import('./avi-6SJLWIWW.cjs');
208
- return await probeWithLibav(normalized, sniffed);
209
- } catch (err) {
210
- const inner = err instanceof Error ? err.message : String(err);
211
- console.error("[avbridge] libav probe failed for", sniffed, "file:", err);
212
- throw new Error(
213
- sniffed === "unknown" ? `unable to probe source: container could not be identified, and the libav.js fallback also failed: ${inner || "(no message \u2014 see browser console for the original error)"}` : `${sniffed.toUpperCase()} files require libav.js, which failed to load: ${inner || "(no message \u2014 see browser console for the original error)"}`
214
- );
215
- }
216
- }
8
+ var chunkF3LQJKXK_cjs = require('./chunk-F3LQJKXK.cjs');
217
9
 
218
10
  // src/util/codec-strings.ts
219
11
  function videoCodecString(track) {
@@ -311,7 +103,9 @@ var FALLBACK_AUDIO_CODECS = /* @__PURE__ */ new Set([
311
103
  "ra_144",
312
104
  "ra_288",
313
105
  "sipr",
314
- "atrac3"
106
+ "atrac3",
107
+ "dts",
108
+ "truehd"
315
109
  ]);
316
110
  var NATIVE_CONTAINERS = /* @__PURE__ */ new Set([
317
111
  "mp4",
@@ -373,7 +167,16 @@ function classifyContext(ctx) {
373
167
  reason: `video codec "${video.codec}" has no browser decoder; WASM fallback required`
374
168
  };
375
169
  }
376
- if (audio && FALLBACK_AUDIO_CODECS.has(audio.codec)) {
170
+ const audioNeedsFallback = audio && (FALLBACK_AUDIO_CODECS.has(audio.codec) || !NATIVE_AUDIO_CODECS.has(audio.codec));
171
+ if (audioNeedsFallback) {
172
+ if (NATIVE_VIDEO_CODECS.has(video.codec) && webCodecsAvailable()) {
173
+ return {
174
+ class: "HYBRID_CANDIDATE",
175
+ strategy: "hybrid",
176
+ reason: `video "${video.codec}" is hardware-decodable via WebCodecs; audio "${audio.codec}" decoded in software by libav`,
177
+ fallbackChain: ["fallback"]
178
+ };
179
+ }
377
180
  return {
378
181
  class: "FALLBACK_REQUIRED",
379
182
  strategy: "fallback",
@@ -453,36 +256,6 @@ function isRiskyNative(video) {
453
256
  return false;
454
257
  }
455
258
 
456
- // src/subtitles/srt.ts
457
- function srtToVtt(srt) {
458
- if (srt.charCodeAt(0) === 65279) srt = srt.slice(1);
459
- const normalized = srt.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
460
- const blocks = normalized.split(/\n{2,}/);
461
- const out = ["WEBVTT", ""];
462
- for (const block of blocks) {
463
- const lines = block.split("\n");
464
- if (lines.length > 0 && /^\d+$/.test(lines[0].trim())) {
465
- lines.shift();
466
- }
467
- if (lines.length === 0) continue;
468
- const timing = lines.shift();
469
- const vttTiming = convertTiming(timing);
470
- if (!vttTiming) continue;
471
- out.push(vttTiming);
472
- for (const l of lines) out.push(l);
473
- out.push("");
474
- }
475
- return out.join("\n");
476
- }
477
- function convertTiming(line) {
478
- const m = /^(\d{1,2}):(\d{2}):(\d{2})[,.](\d{1,3})\s*-->\s*(\d{1,2}):(\d{2}):(\d{2})[,.](\d{1,3})(.*)$/.exec(
479
- line.trim()
480
- );
481
- if (!m) return null;
482
- const fmt = (h, mm, s, ms) => `${h.padStart(2, "0")}:${mm}:${s}.${ms.padEnd(3, "0").slice(0, 3)}`;
483
- return `${fmt(m[1], m[2], m[3], m[4])} --> ${fmt(m[5], m[6], m[7], m[8])}${m[9] ?? ""}`;
484
- }
485
-
486
259
  // src/events.ts
487
260
  var TypedEmitter = class {
488
261
  listeners = {};
@@ -688,7 +461,7 @@ async function createNativeSession(context, video) {
688
461
  },
689
462
  async setAudioTrack(id) {
690
463
  const tracks = video.audioTracks;
691
- if (!tracks) return;
464
+ if (!tracks || tracks.length === 0) return;
692
465
  for (let i = 0; i < tracks.length; i++) {
693
466
  tracks[i].enabled = tracks[i].id === String(id) || i === id;
694
467
  }
@@ -740,10 +513,18 @@ var MseSink = class {
740
513
  constructor(options) {
741
514
  this.options = options;
742
515
  if (typeof MediaSource === "undefined") {
743
- throw new Error("MSE not supported in this environment");
516
+ throw new chunk2IJ66NTD_cjs.AvbridgeError(
517
+ chunk2IJ66NTD_cjs.ERR_MSE_NOT_SUPPORTED,
518
+ "MediaSource Extensions (MSE) are not supported in this environment.",
519
+ "MSE is required for the remux strategy. Use a browser that supports MSE, or try the fallback strategy."
520
+ );
744
521
  }
745
522
  if (!MediaSource.isTypeSupported(options.mime)) {
746
- throw new Error(`MSE does not support MIME "${options.mime}" \u2014 cannot remux`);
523
+ throw new chunk2IJ66NTD_cjs.AvbridgeError(
524
+ chunk2IJ66NTD_cjs.ERR_MSE_CODEC_NOT_SUPPORTED,
525
+ `This browser's MSE does not support "${options.mime}".`,
526
+ "The codec combination can't be played via remux in this browser. The player will try the next strategy automatically."
527
+ );
747
528
  }
748
529
  this.mediaSource = new MediaSource();
749
530
  this.objectUrl = URL.createObjectURL(this.mediaSource);
@@ -928,30 +709,49 @@ var MseSink = class {
928
709
  async function createRemuxPipeline(ctx, video) {
929
710
  const mb = await import('mediabunny');
930
711
  const videoTrackInfo = ctx.videoTracks[0];
931
- const audioTrackInfo = ctx.audioTracks[0];
932
712
  if (!videoTrackInfo) throw new Error("remux: source has no video track");
933
- const mbVideoCodec = avbridgeVideoToMediabunny(videoTrackInfo.codec);
713
+ const mbVideoCodec = chunkZCUXHW55_cjs.avbridgeVideoToMediabunny(videoTrackInfo.codec);
934
714
  if (!mbVideoCodec) {
935
715
  throw new Error(`remux: video codec "${videoTrackInfo.codec}" is not supported by mediabunny output`);
936
716
  }
937
- const mbAudioCodec = audioTrackInfo ? avbridgeAudioToMediabunny(audioTrackInfo.codec) : null;
938
717
  const input = new mb.Input({
939
- source: await buildMediabunnySourceFromInput(mb, ctx.source),
718
+ source: await chunkZCUXHW55_cjs.buildMediabunnySourceFromInput(mb, ctx.source),
940
719
  formats: mb.ALL_FORMATS
941
720
  });
942
721
  const allTracks = await input.getTracks();
943
722
  const inputVideo = allTracks.find((t) => t.id === videoTrackInfo.id && t.isVideoTrack());
944
- const inputAudio = audioTrackInfo ? allTracks.find((t) => t.id === audioTrackInfo.id && t.isAudioTrack()) : null;
945
723
  if (!inputVideo || !inputVideo.isVideoTrack()) {
946
724
  throw new Error("remux: video track not found in input");
947
725
  }
948
- if (audioTrackInfo && (!inputAudio || !inputAudio.isAudioTrack())) {
949
- throw new Error("remux: audio track not found in input");
950
- }
951
726
  const videoConfig = await inputVideo.getDecoderConfig();
952
- const audioConfig = inputAudio && inputAudio.isAudioTrack() ? await inputAudio.getDecoderConfig() : null;
953
727
  const videoSink = new mb.EncodedPacketSink(inputVideo);
954
- const audioSink = inputAudio?.isAudioTrack() ? new mb.EncodedPacketSink(inputAudio) : null;
728
+ let selectedAudioTrackId = ctx.audioTracks[0]?.id ?? null;
729
+ let inputAudio = null;
730
+ let mbAudioCodec = null;
731
+ let audioSink = null;
732
+ let audioConfig = null;
733
+ async function rebuildAudio() {
734
+ if (selectedAudioTrackId == null) {
735
+ inputAudio = null;
736
+ mbAudioCodec = null;
737
+ audioSink = null;
738
+ audioConfig = null;
739
+ return;
740
+ }
741
+ const trackInfo = ctx.audioTracks.find((t) => t.id === selectedAudioTrackId);
742
+ if (!trackInfo) {
743
+ throw new Error(`remux: no audio track with id ${selectedAudioTrackId}`);
744
+ }
745
+ const newInput = allTracks.find((t) => t.id === trackInfo.id && t.isAudioTrack());
746
+ if (!newInput || !newInput.isAudioTrack()) {
747
+ throw new Error("remux: audio track not found in input");
748
+ }
749
+ inputAudio = newInput;
750
+ mbAudioCodec = chunkZCUXHW55_cjs.avbridgeAudioToMediabunny(trackInfo.codec);
751
+ audioSink = new mb.EncodedPacketSink(newInput);
752
+ audioConfig = await newInput.getDecoderConfig();
753
+ }
754
+ await rebuildAudio();
955
755
  let sink = null;
956
756
  const stats = { videoPackets: 0, audioPackets: 0, bytesWritten: 0, fragments: 0 };
957
757
  let destroyed = false;
@@ -1076,6 +876,30 @@ async function createRemuxPipeline(ctx, video) {
1076
876
  pendingAutoPlay = autoPlay;
1077
877
  if (sink) sink.setPlayOnSeek(autoPlay);
1078
878
  },
879
+ async setAudioTrack(trackId, time, autoPlay) {
880
+ if (selectedAudioTrackId === trackId) return;
881
+ if (!ctx.audioTracks.some((t) => t.id === trackId)) {
882
+ console.warn("[avbridge] remux: setAudioTrack \u2014 unknown track id", trackId);
883
+ return;
884
+ }
885
+ pumpToken++;
886
+ selectedAudioTrackId = trackId;
887
+ await rebuildAudio().catch((err) => {
888
+ console.warn("[avbridge] remux: rebuildAudio failed:", err.message);
889
+ });
890
+ if (sink) {
891
+ try {
892
+ sink.destroy();
893
+ } catch {
894
+ }
895
+ sink = null;
896
+ }
897
+ pendingAutoPlay = autoPlay;
898
+ pendingStartTime = time;
899
+ pumpLoop(++pumpToken, time).catch((err) => {
900
+ console.error("[avbridge] remux pipeline setAudioTrack pump failed:", err);
901
+ });
902
+ },
1079
903
  async destroy() {
1080
904
  destroyed = true;
1081
905
  pumpToken++;
@@ -1135,7 +959,19 @@ async function createRemuxSession(context, video) {
1135
959
  const wasPlaying = !video.paused;
1136
960
  await pipeline.seek(time, wasPlaying || wantPlay);
1137
961
  },
1138
- async setAudioTrack(_id) {
962
+ async setAudioTrack(id) {
963
+ if (!context.audioTracks.some((t) => t.id === id)) {
964
+ console.warn("[avbridge] remux: setAudioTrack \u2014 unknown track id", id);
965
+ return;
966
+ }
967
+ const wasPlaying = !video.paused;
968
+ const time = video.currentTime || 0;
969
+ if (!started) {
970
+ started = true;
971
+ await pipeline.setAudioTrack(id, time, wantPlay || wasPlaying);
972
+ return;
973
+ }
974
+ await pipeline.setAudioTrack(id, time, wasPlaying || wantPlay);
1139
975
  },
1140
976
  async setSubtitleTrack(id) {
1141
977
  const tracks = video.textTracks;
@@ -1159,6 +995,10 @@ async function createRemuxSession(context, video) {
1159
995
  }
1160
996
 
1161
997
  // src/strategies/fallback/video-renderer.ts
998
+ function isDebug() {
999
+ return typeof globalThis !== "undefined" && !!globalThis.AVBRIDGE_DEBUG;
1000
+ }
1001
+ var lastDebugLog = 0;
1162
1002
  var VideoRenderer = class {
1163
1003
  constructor(target, clock, fps = 30) {
1164
1004
  this.target = target;
@@ -1184,6 +1024,9 @@ var VideoRenderer = class {
1184
1024
  document.body.appendChild(this.canvas);
1185
1025
  }
1186
1026
  target.style.visibility = "hidden";
1027
+ const overlayParent = parent instanceof HTMLElement ? parent : document.body;
1028
+ this.subtitleOverlay = new chunkS4WAZC2T_cjs.SubtitleOverlay(overlayParent);
1029
+ this.watchTextTracks(target);
1187
1030
  const ctx = this.canvas.getContext("2d");
1188
1031
  if (!ctx) throw new Error("video renderer: failed to acquire 2D context");
1189
1032
  this.ctx = ctx;
@@ -1205,6 +1048,29 @@ var VideoRenderer = class {
1205
1048
  lastPaintWall = 0;
1206
1049
  /** Minimum ms between paints — paces video at roughly source fps. */
1207
1050
  paintIntervalMs;
1051
+ /** Cumulative count of frames skipped because all PTS are in the future. */
1052
+ ticksWaiting = 0;
1053
+ /** Cumulative count of ticks where PTS mode painted a frame. */
1054
+ ticksPainted = 0;
1055
+ /**
1056
+ * Subtitle overlay div attached to the stage wrapper alongside the
1057
+ * canvas. Created lazily when subtitle tracks are attached via the
1058
+ * target's `<track>` children. Canvas strategies (hybrid, fallback)
1059
+ * hide the <video>, so we can't rely on the browser's native cue
1060
+ * rendering; we read TextTrack.cues and render into this overlay.
1061
+ */
1062
+ subtitleOverlay = null;
1063
+ subtitleTrack = null;
1064
+ /**
1065
+ * Calibration offset (microseconds) between video PTS and audio clock.
1066
+ * Video PTS and AudioContext.currentTime can drift ~0.1% relative to
1067
+ * each other (different clock domains). Over 45 minutes that's 2.6s.
1068
+ * We measure the offset on the first painted frame and update it
1069
+ * periodically so the PTS comparison stays calibrated.
1070
+ */
1071
+ ptsCalibrationUs = 0;
1072
+ ptsCalibrated = false;
1073
+ lastCalibrationWall = 0;
1208
1074
  /** Resolves once the first decoded frame has been enqueued. */
1209
1075
  firstFrameReady;
1210
1076
  resolveFirstFrame;
@@ -1238,9 +1104,80 @@ var VideoRenderer = class {
1238
1104
  this.framesDroppedOverflow++;
1239
1105
  }
1240
1106
  }
1107
+ /**
1108
+ * Watch the target <video>'s textTracks list. When a track is added,
1109
+ * grab it and start polling cues on each render tick. Existing tracks
1110
+ * (if any) are picked up immediately.
1111
+ */
1112
+ watchTextTracks(target) {
1113
+ const pick = () => {
1114
+ if (this.subtitleTrack) return;
1115
+ const tracks = target.textTracks;
1116
+ if (isDebug()) {
1117
+ console.log(`[avbridge:subs] watchTextTracks pick() \u2014 ${tracks.length} tracks`);
1118
+ }
1119
+ for (let i = 0; i < tracks.length; i++) {
1120
+ const t = tracks[i];
1121
+ if (isDebug()) {
1122
+ console.log(`[avbridge:subs] track ${i}: kind=${t.kind} mode=${t.mode} cues=${t.cues?.length ?? 0}`);
1123
+ }
1124
+ if (t.kind === "subtitles" || t.kind === "captions") {
1125
+ this.subtitleTrack = t;
1126
+ t.mode = "hidden";
1127
+ if (isDebug()) {
1128
+ console.log(`[avbridge:subs] picked track, mode=hidden`);
1129
+ }
1130
+ const trackEl = target.querySelector(`track[srclang="${t.language}"]`);
1131
+ if (trackEl) {
1132
+ trackEl.addEventListener("load", () => {
1133
+ if (isDebug()) {
1134
+ console.log(`[avbridge:subs] track element loaded, cues=${t.cues?.length ?? 0}`);
1135
+ }
1136
+ });
1137
+ trackEl.addEventListener("error", (ev) => {
1138
+ console.warn(`[avbridge:subs] track element error:`, ev);
1139
+ });
1140
+ }
1141
+ break;
1142
+ }
1143
+ }
1144
+ };
1145
+ pick();
1146
+ if (typeof target.textTracks.addEventListener === "function") {
1147
+ target.textTracks.addEventListener("addtrack", (e) => {
1148
+ if (isDebug()) {
1149
+ console.log("[avbridge:subs] addtrack event fired");
1150
+ }
1151
+ pick();
1152
+ });
1153
+ }
1154
+ }
1155
+ _loggedCues = false;
1156
+ /** Find the active cue (if any) for the given media time. */
1157
+ updateSubtitles() {
1158
+ if (!this.subtitleOverlay || !this.subtitleTrack) return;
1159
+ const cues = this.subtitleTrack.cues;
1160
+ if (!cues || cues.length === 0) return;
1161
+ if (isDebug() && !this._loggedCues) {
1162
+ this._loggedCues = true;
1163
+ console.log(`[avbridge:subs] cues available: ${cues.length}, first start=${cues[0].startTime}, last end=${cues[cues.length - 1].endTime}`);
1164
+ }
1165
+ const t = this.clock.now();
1166
+ let activeText = "";
1167
+ for (let i = 0; i < cues.length; i++) {
1168
+ const c = cues[i];
1169
+ if (t >= c.startTime && t <= c.endTime) {
1170
+ const vttCue = c;
1171
+ activeText = vttCue.text ?? "";
1172
+ break;
1173
+ }
1174
+ }
1175
+ this.subtitleOverlay.setText(activeText.replace(/<[^>]+>/g, ""));
1176
+ }
1241
1177
  tick() {
1242
1178
  if (this.destroyed) return;
1243
1179
  this.rafHandle = requestAnimationFrame(this.tick);
1180
+ this.updateSubtitles();
1244
1181
  if (this.queue.length === 0) return;
1245
1182
  const playing = this.clock.isPlaying();
1246
1183
  if (!playing) {
@@ -1253,21 +1190,81 @@ var VideoRenderer = class {
1253
1190
  }
1254
1191
  return;
1255
1192
  }
1256
- const wallNow = performance.now();
1257
- if (wallNow - this.lastPaintWall < this.paintIntervalMs - 2) return;
1258
- if (this.queue.length === 0) return;
1259
- if (this.framesPainted > 0 && this.framesPainted % 30 === 0) {
1260
- const audioNowUs = this.clock.now() * 1e6;
1261
- const headTs = this.queue[0].timestamp ?? 0;
1262
- const driftUs = headTs - audioNowUs;
1263
- if (driftUs < -15e4) {
1264
- this.queue.shift()?.close();
1265
- this.framesDroppedLate++;
1266
- if (this.queue.length === 0) return;
1267
- } else if (driftUs > 15e4) {
1193
+ const rawAudioNowUs = this.clock.now() * 1e6;
1194
+ const headTs = this.queue[0].timestamp ?? 0;
1195
+ const hasPts = headTs > 0 || this.queue.length > 1;
1196
+ if (hasPts) {
1197
+ const wallNow2 = performance.now();
1198
+ if (!this.ptsCalibrated || wallNow2 - this.lastCalibrationWall > 1e4) {
1199
+ this.ptsCalibrationUs = headTs - rawAudioNowUs;
1200
+ this.ptsCalibrated = true;
1201
+ this.lastCalibrationWall = wallNow2;
1202
+ }
1203
+ const audioNowUs = rawAudioNowUs + this.ptsCalibrationUs;
1204
+ const frameDurationUs = this.paintIntervalMs * 1e3;
1205
+ const deadlineUs = audioNowUs + frameDurationUs;
1206
+ let bestIdx = -1;
1207
+ for (let i = 0; i < this.queue.length; i++) {
1208
+ const ts = this.queue[i].timestamp ?? 0;
1209
+ if (ts <= deadlineUs) {
1210
+ bestIdx = i;
1211
+ } else {
1212
+ break;
1213
+ }
1214
+ }
1215
+ if (bestIdx < 0) {
1216
+ this.ticksWaiting++;
1217
+ if (isDebug()) {
1218
+ const now = performance.now();
1219
+ if (now - lastDebugLog > 1e3) {
1220
+ const headPtsMs = (headTs / 1e3).toFixed(1);
1221
+ const audioMs = (audioNowUs / 1e3).toFixed(1);
1222
+ const rawDriftMs = ((headTs - rawAudioNowUs) / 1e3).toFixed(1);
1223
+ const calibMs = (this.ptsCalibrationUs / 1e3).toFixed(1);
1224
+ console.log(
1225
+ `[avbridge:renderer] WAIT q=${this.queue.length} headPTS=${headPtsMs}ms calibAudio=${audioMs}ms rawDrift=${rawDriftMs}ms calib=${calibMs}ms painted=${this.framesPainted} dropped=${this.framesDroppedLate}`
1226
+ );
1227
+ lastDebugLog = now;
1228
+ }
1229
+ }
1268
1230
  return;
1269
1231
  }
1232
+ const dropThresholdUs = audioNowUs - frameDurationUs * 2;
1233
+ let dropped = 0;
1234
+ while (bestIdx > 0) {
1235
+ const ts = this.queue[0].timestamp ?? 0;
1236
+ if (ts < dropThresholdUs) {
1237
+ this.queue.shift()?.close();
1238
+ this.framesDroppedLate++;
1239
+ bestIdx--;
1240
+ dropped++;
1241
+ } else {
1242
+ break;
1243
+ }
1244
+ }
1245
+ this.ticksPainted++;
1246
+ if (isDebug()) {
1247
+ const now = performance.now();
1248
+ if (now - lastDebugLog > 1e3) {
1249
+ const paintedTs = this.queue[0]?.timestamp ?? 0;
1250
+ const audioMs = (audioNowUs / 1e3).toFixed(1);
1251
+ const ptsMs = (paintedTs / 1e3).toFixed(1);
1252
+ const rawDriftMs = ((paintedTs - rawAudioNowUs) / 1e3).toFixed(1);
1253
+ const calibMs = (this.ptsCalibrationUs / 1e3).toFixed(1);
1254
+ console.log(
1255
+ `[avbridge:renderer] PAINT q=${this.queue.length} calibAudio=${audioMs}ms nextPTS=${ptsMs}ms rawDrift=${rawDriftMs}ms calib=${calibMs}ms dropped=${dropped} total_drops=${this.framesDroppedLate} painted=${this.framesPainted}`
1256
+ );
1257
+ lastDebugLog = now;
1258
+ }
1259
+ }
1260
+ const frame2 = this.queue.shift();
1261
+ this.paint(frame2);
1262
+ frame2.close();
1263
+ this.lastPaintWall = performance.now();
1264
+ return;
1270
1265
  }
1266
+ const wallNow = performance.now();
1267
+ if (wallNow - this.lastPaintWall < this.paintIntervalMs - 2) return;
1271
1268
  const frame = this.queue.shift();
1272
1269
  this.paint(frame);
1273
1270
  frame.close();
@@ -1289,8 +1286,13 @@ var VideoRenderer = class {
1289
1286
  }
1290
1287
  /** Discard all queued frames. Used by seek to drop stale buffers. */
1291
1288
  flush() {
1289
+ const count = this.queue.length;
1292
1290
  while (this.queue.length > 0) this.queue.shift()?.close();
1293
1291
  this.prerolled = false;
1292
+ this.ptsCalibrated = false;
1293
+ if (isDebug() && count > 0) {
1294
+ console.log(`[avbridge:renderer] FLUSH discarded=${count} painted=${this.framesPainted} drops=${this.framesDroppedLate}`);
1295
+ }
1294
1296
  }
1295
1297
  stats() {
1296
1298
  return {
@@ -1304,6 +1306,11 @@ var VideoRenderer = class {
1304
1306
  this.destroyed = true;
1305
1307
  if (this.rafHandle != null) cancelAnimationFrame(this.rafHandle);
1306
1308
  this.flush();
1309
+ if (this.subtitleOverlay) {
1310
+ this.subtitleOverlay.destroy();
1311
+ this.subtitleOverlay = null;
1312
+ }
1313
+ this.subtitleTrack = null;
1307
1314
  this.canvas.remove();
1308
1315
  this.target.style.visibility = "";
1309
1316
  }
@@ -1335,11 +1342,38 @@ var AudioOutput = class {
1335
1342
  pendingQueue = [];
1336
1343
  framesScheduled = 0;
1337
1344
  destroyed = false;
1345
+ /** User-set volume (0..1). Applied to the gain node. */
1346
+ _volume = 1;
1347
+ /** User-set muted flag. When true, gain is forced to 0. */
1348
+ _muted = false;
1338
1349
  constructor() {
1339
1350
  this.ctx = new AudioContext();
1340
1351
  this.gain = this.ctx.createGain();
1341
1352
  this.gain.connect(this.ctx.destination);
1342
1353
  }
1354
+ /** Set volume (0..1). Applied immediately to the gain node. */
1355
+ setVolume(v) {
1356
+ this._volume = Math.max(0, Math.min(1, v));
1357
+ this.applyGain();
1358
+ }
1359
+ getVolume() {
1360
+ return this._volume;
1361
+ }
1362
+ /** Set muted. When true, output is silenced regardless of volume. */
1363
+ setMuted(m) {
1364
+ this._muted = m;
1365
+ this.applyGain();
1366
+ }
1367
+ getMuted() {
1368
+ return this._muted;
1369
+ }
1370
+ applyGain() {
1371
+ const target = this._muted ? 0 : this._volume;
1372
+ try {
1373
+ this.gain.gain.value = target;
1374
+ } catch {
1375
+ }
1376
+ }
1343
1377
  /**
1344
1378
  * Switch into wall-clock fallback mode. Called by the decoder when no
1345
1379
  * audio decoder could be initialized for the source. Once set, this
@@ -1489,6 +1523,7 @@ var AudioOutput = class {
1489
1523
  }
1490
1524
  this.gain = this.ctx.createGain();
1491
1525
  this.gain.connect(this.ctx.destination);
1526
+ this.applyGain();
1492
1527
  this.pendingQueue = [];
1493
1528
  this.mediaTimeOfAnchor = newMediaTime;
1494
1529
  this.mediaTimeOfNext = newMediaTime;
@@ -1517,15 +1552,16 @@ var AudioOutput = class {
1517
1552
 
1518
1553
  // src/strategies/hybrid/decoder.ts
1519
1554
  async function startHybridDecoder(opts) {
1520
- const variant = chunkNZU7W256_cjs.pickLibavVariant(opts.context);
1555
+ const variant = chunkF3LQJKXK_cjs.pickLibavVariant(opts.context);
1521
1556
  const libav = await chunkG4APZMCP_cjs.loadLibav(variant);
1522
1557
  const bridge = await loadBridge();
1523
- const { prepareLibavInput } = await import('./libav-http-reader-FPYDBMYK.cjs');
1524
- const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source);
1558
+ const { prepareLibavInput } = await import('./libav-http-reader-AZLE7YFS.cjs');
1559
+ const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source, opts.transport);
1525
1560
  const readPkt = await libav.av_packet_alloc();
1526
1561
  const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
1527
1562
  const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
1528
- const audioStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO) ?? null;
1563
+ const firstAudioTrackId = opts.context.audioTracks[0]?.id;
1564
+ let audioStream = (firstAudioTrackId != null ? streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO && s.index === firstAudioTrackId) : void 0) ?? streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO) ?? null;
1529
1565
  if (!videoStream && !audioStream) {
1530
1566
  throw new Error("hybrid decoder: file has no decodable streams");
1531
1567
  }
@@ -1592,6 +1628,56 @@ async function startHybridDecoder(opts) {
1592
1628
  });
1593
1629
  throw new Error("hybrid decoder: could not initialize any decoders");
1594
1630
  }
1631
+ let bsfCtx = null;
1632
+ let bsfPkt = null;
1633
+ if (videoStream && opts.context.videoTracks[0]?.codec === "mpeg4") {
1634
+ try {
1635
+ bsfCtx = await libav.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
1636
+ if (bsfCtx != null && bsfCtx >= 0) {
1637
+ const parIn = await libav.AVBSFContext_par_in(bsfCtx);
1638
+ await libav.avcodec_parameters_copy(parIn, videoStream.codecpar);
1639
+ await libav.av_bsf_init(bsfCtx);
1640
+ bsfPkt = await libav.av_packet_alloc();
1641
+ chunkG4APZMCP_cjs.dbg.info("bsf", "mpeg4_unpack_bframes BSF active (hybrid)");
1642
+ } else {
1643
+ console.warn("[avbridge] mpeg4_unpack_bframes BSF not available in hybrid decoder");
1644
+ bsfCtx = null;
1645
+ }
1646
+ } catch (err) {
1647
+ console.warn("[avbridge] hybrid: failed to init BSF:", err.message);
1648
+ bsfCtx = null;
1649
+ bsfPkt = null;
1650
+ }
1651
+ }
1652
+ async function applyBSF(packets) {
1653
+ if (!bsfCtx || !bsfPkt) return packets;
1654
+ const out = [];
1655
+ for (const pkt of packets) {
1656
+ await libav.ff_copyin_packet(bsfPkt, pkt);
1657
+ const sendErr = await libav.av_bsf_send_packet(bsfCtx, bsfPkt);
1658
+ if (sendErr < 0) {
1659
+ out.push(pkt);
1660
+ continue;
1661
+ }
1662
+ while (true) {
1663
+ const recvErr = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
1664
+ if (recvErr < 0) break;
1665
+ out.push(await libav.ff_copyout_packet(bsfPkt));
1666
+ }
1667
+ }
1668
+ return out;
1669
+ }
1670
+ async function flushBSF() {
1671
+ if (!bsfCtx || !bsfPkt) return;
1672
+ try {
1673
+ await libav.av_bsf_send_packet(bsfCtx, 0);
1674
+ while (true) {
1675
+ const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
1676
+ if (err < 0) break;
1677
+ }
1678
+ } catch {
1679
+ }
1680
+ }
1595
1681
  let destroyed = false;
1596
1682
  let pumpToken = 0;
1597
1683
  let pumpRunning = null;
@@ -1619,10 +1705,17 @@ async function startHybridDecoder(opts) {
1619
1705
  if (myToken !== pumpToken || destroyed) return;
1620
1706
  const videoPackets = videoStream ? packets[videoStream.index] : void 0;
1621
1707
  const audioPackets = audioStream ? packets[audioStream.index] : void 0;
1708
+ if (audioDec && audioPackets && audioPackets.length > 0) {
1709
+ await decodeAudioBatch(audioPackets, myToken);
1710
+ }
1711
+ if (myToken !== pumpToken || destroyed) return;
1712
+ await new Promise((r) => setTimeout(r, 0));
1713
+ if (myToken !== pumpToken || destroyed) return;
1622
1714
  if (videoDecoder && videoPackets && videoPackets.length > 0) {
1623
- for (const pkt of videoPackets) {
1715
+ const processed = await applyBSF(videoPackets);
1716
+ for (const pkt of processed) {
1624
1717
  if (myToken !== pumpToken || destroyed) return;
1625
- sanitizePacketTimestamp(pkt, () => {
1718
+ chunkCPZ7PXAM_cjs.sanitizePacketTimestamp(pkt, () => {
1626
1719
  const ts = syntheticVideoUs;
1627
1720
  syntheticVideoUs += videoFrameStepUs;
1628
1721
  return ts;
@@ -1640,9 +1733,6 @@ async function startHybridDecoder(opts) {
1640
1733
  }
1641
1734
  }
1642
1735
  }
1643
- if (audioDec && audioPackets && audioPackets.length > 0) {
1644
- await decodeAudioBatch(audioPackets, myToken);
1645
- }
1646
1736
  packetsRead += (videoPackets?.length ?? 0) + (audioPackets?.length ?? 0);
1647
1737
  while (!destroyed && myToken === pumpToken && (videoDecoder && videoDecoder.decodeQueueSize > 10 || opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
1648
1738
  await new Promise((r) => setTimeout(r, 50));
@@ -1665,23 +1755,46 @@ async function startHybridDecoder(opts) {
1665
1755
  }
1666
1756
  async function decodeAudioBatch(pkts, myToken, flush = false) {
1667
1757
  if (!audioDec || destroyed || myToken !== pumpToken) return;
1668
- let frames;
1669
- try {
1670
- frames = await libav.ff_decode_multi(
1671
- audioDec.c,
1672
- audioDec.pkt,
1673
- audioDec.frame,
1674
- pkts,
1675
- flush ? { fin: true, ignoreErrors: true } : { ignoreErrors: true }
1676
- );
1677
- } catch (err) {
1678
- console.error("[avbridge] hybrid audio decode failed:", err);
1679
- return;
1758
+ const AUDIO_SUB_BATCH = 4;
1759
+ let allFrames = [];
1760
+ for (let i = 0; i < pkts.length; i += AUDIO_SUB_BATCH) {
1761
+ if (myToken !== pumpToken || destroyed) return;
1762
+ const slice = pkts.slice(i, i + AUDIO_SUB_BATCH);
1763
+ const isLast = i + AUDIO_SUB_BATCH >= pkts.length;
1764
+ try {
1765
+ const frames2 = await libav.ff_decode_multi(
1766
+ audioDec.c,
1767
+ audioDec.pkt,
1768
+ audioDec.frame,
1769
+ slice,
1770
+ isLast && flush ? { fin: true, ignoreErrors: true } : { ignoreErrors: true }
1771
+ );
1772
+ allFrames = allFrames.concat(frames2);
1773
+ } catch (err) {
1774
+ console.error("[avbridge] hybrid audio decode failed:", err);
1775
+ return;
1776
+ }
1777
+ if (!isLast) await new Promise((r) => setTimeout(r, 0));
1778
+ }
1779
+ if (pkts.length === 0 && flush) {
1780
+ try {
1781
+ allFrames = await libav.ff_decode_multi(
1782
+ audioDec.c,
1783
+ audioDec.pkt,
1784
+ audioDec.frame,
1785
+ [],
1786
+ { fin: true, ignoreErrors: true }
1787
+ );
1788
+ } catch (err) {
1789
+ console.error("[avbridge] hybrid audio flush failed:", err);
1790
+ return;
1791
+ }
1680
1792
  }
1681
1793
  if (myToken !== pumpToken || destroyed) return;
1794
+ const frames = allFrames;
1682
1795
  for (const f of frames) {
1683
1796
  if (myToken !== pumpToken || destroyed) return;
1684
- sanitizeFrameTimestamp(
1797
+ chunkCPZ7PXAM_cjs.sanitizeFrameTimestamp(
1685
1798
  f,
1686
1799
  () => {
1687
1800
  const ts = syntheticAudioUs;
@@ -1692,7 +1805,7 @@ async function startHybridDecoder(opts) {
1692
1805
  },
1693
1806
  audioTimeBase
1694
1807
  );
1695
- const samples = libavFrameToInterleavedFloat32(f);
1808
+ const samples = chunkCPZ7PXAM_cjs.libavFrameToInterleavedFloat32(f);
1696
1809
  if (samples) {
1697
1810
  opts.audio.schedule(samples.data, samples.channels, samples.sampleRate);
1698
1811
  audioFramesDecoded++;
@@ -1715,6 +1828,14 @@ async function startHybridDecoder(opts) {
1715
1828
  await pumpRunning;
1716
1829
  } catch {
1717
1830
  }
1831
+ try {
1832
+ if (bsfCtx) await libav.av_bsf_free(bsfCtx);
1833
+ } catch {
1834
+ }
1835
+ try {
1836
+ if (bsfPkt) await libav.av_packet_free?.(bsfPkt);
1837
+ } catch {
1838
+ }
1718
1839
  try {
1719
1840
  if (videoDecoder && videoDecoder.state !== "closed") videoDecoder.close();
1720
1841
  } catch {
@@ -1736,6 +1857,71 @@ async function startHybridDecoder(opts) {
1736
1857
  } catch {
1737
1858
  }
1738
1859
  },
1860
+ async setAudioTrack(trackId, timeSec) {
1861
+ if (audioStream && audioStream.index === trackId) return;
1862
+ const newStream = streams.find(
1863
+ (s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO && s.index === trackId
1864
+ );
1865
+ if (!newStream) {
1866
+ console.warn("[avbridge] hybrid: setAudioTrack \u2014 no stream with id", trackId);
1867
+ return;
1868
+ }
1869
+ const newToken = ++pumpToken;
1870
+ if (pumpRunning) {
1871
+ try {
1872
+ await pumpRunning;
1873
+ } catch {
1874
+ }
1875
+ }
1876
+ if (destroyed) return;
1877
+ if (audioDec) {
1878
+ try {
1879
+ await libav.ff_free_decoder?.(audioDec.c, audioDec.pkt, audioDec.frame);
1880
+ } catch {
1881
+ }
1882
+ audioDec = null;
1883
+ }
1884
+ try {
1885
+ const [, c, pkt, frame] = await libav.ff_init_decoder(newStream.codec_id, {
1886
+ codecpar: newStream.codecpar
1887
+ });
1888
+ audioDec = { c, pkt, frame };
1889
+ audioTimeBase = newStream.time_base_num && newStream.time_base_den ? [newStream.time_base_num, newStream.time_base_den] : void 0;
1890
+ } catch (err) {
1891
+ console.warn(
1892
+ "[avbridge] hybrid: setAudioTrack init failed \u2014 switching to no-audio:",
1893
+ err.message
1894
+ );
1895
+ audioDec = null;
1896
+ opts.audio.setNoAudio();
1897
+ }
1898
+ audioStream = newStream;
1899
+ try {
1900
+ const tsUs = Math.floor(timeSec * 1e6);
1901
+ const [tsLo, tsHi] = libav.f64toi64 ? libav.f64toi64(tsUs) : [tsUs | 0, Math.floor(tsUs / 4294967296)];
1902
+ await libav.av_seek_frame(
1903
+ fmt_ctx,
1904
+ -1,
1905
+ tsLo,
1906
+ tsHi,
1907
+ libav.AVSEEK_FLAG_BACKWARD ?? 0
1908
+ );
1909
+ } catch (err) {
1910
+ console.warn("[avbridge] hybrid: setAudioTrack seek failed:", err);
1911
+ }
1912
+ try {
1913
+ if (videoDecoder && videoDecoder.state === "configured") {
1914
+ await videoDecoder.flush();
1915
+ }
1916
+ } catch {
1917
+ }
1918
+ await flushBSF();
1919
+ syntheticVideoUs = Math.round(timeSec * 1e6);
1920
+ syntheticAudioUs = Math.round(timeSec * 1e6);
1921
+ pumpRunning = pumpLoop(newToken).catch(
1922
+ (err) => console.error("[avbridge] hybrid pump failed (post-setAudioTrack):", err)
1923
+ );
1924
+ },
1739
1925
  async seek(timeSec) {
1740
1926
  const newToken = ++pumpToken;
1741
1927
  if (pumpRunning) {
@@ -1768,6 +1954,7 @@ async function startHybridDecoder(opts) {
1768
1954
  if (audioDec) await libav.avcodec_flush_buffers?.(audioDec.c);
1769
1955
  } catch {
1770
1956
  }
1957
+ await flushBSF();
1771
1958
  syntheticVideoUs = Math.round(timeSec * 1e6);
1772
1959
  syntheticAudioUs = Math.round(timeSec * 1e6);
1773
1960
  pumpRunning = pumpLoop(newToken).catch(
@@ -1781,6 +1968,7 @@ async function startHybridDecoder(opts) {
1781
1968
  videoFramesDecoded,
1782
1969
  videoChunksFed,
1783
1970
  audioFramesDecoded,
1971
+ bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
1784
1972
  videoDecodeQueueSize: videoDecoder?.decodeQueueSize ?? 0,
1785
1973
  // Confirmed transport info — see fallback decoder for the pattern.
1786
1974
  _transport: inputHandle.transport === "http-range" ? "http-range" : "memory",
@@ -1791,161 +1979,9 @@ async function startHybridDecoder(opts) {
1791
1979
  }
1792
1980
  };
1793
1981
  }
1794
- function sanitizePacketTimestamp(pkt, nextUs, fallbackTimeBase) {
1795
- const lo = pkt.pts ?? 0;
1796
- const hi = pkt.ptshi ?? 0;
1797
- const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
1798
- if (isInvalid) {
1799
- const us2 = nextUs();
1800
- pkt.pts = us2;
1801
- pkt.ptshi = 0;
1802
- pkt.time_base_num = 1;
1803
- pkt.time_base_den = 1e6;
1804
- return;
1805
- }
1806
- const tb = fallbackTimeBase ?? [1, 1e6];
1807
- const pts64 = hi * 4294967296 + lo;
1808
- const us = Math.round(pts64 * 1e6 * tb[0] / tb[1]);
1809
- if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
1810
- pkt.pts = us;
1811
- pkt.ptshi = us < 0 ? -1 : 0;
1812
- pkt.time_base_num = 1;
1813
- pkt.time_base_den = 1e6;
1814
- return;
1815
- }
1816
- const fallback = nextUs();
1817
- pkt.pts = fallback;
1818
- pkt.ptshi = 0;
1819
- pkt.time_base_num = 1;
1820
- pkt.time_base_den = 1e6;
1821
- }
1822
- function sanitizeFrameTimestamp(frame, nextUs, fallbackTimeBase) {
1823
- const lo = frame.pts ?? 0;
1824
- const hi = frame.ptshi ?? 0;
1825
- const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
1826
- if (isInvalid) {
1827
- const us2 = nextUs();
1828
- frame.pts = us2;
1829
- frame.ptshi = 0;
1830
- return;
1831
- }
1832
- const tb = fallbackTimeBase ?? [1, 1e6];
1833
- const pts64 = hi * 4294967296 + lo;
1834
- const us = Math.round(pts64 * 1e6 * tb[0] / tb[1]);
1835
- if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
1836
- frame.pts = us;
1837
- frame.ptshi = us < 0 ? -1 : 0;
1838
- return;
1839
- }
1840
- const fallback = nextUs();
1841
- frame.pts = fallback;
1842
- frame.ptshi = 0;
1843
- }
1844
- var AV_SAMPLE_FMT_U8 = 0;
1845
- var AV_SAMPLE_FMT_S16 = 1;
1846
- var AV_SAMPLE_FMT_S32 = 2;
1847
- var AV_SAMPLE_FMT_FLT = 3;
1848
- var AV_SAMPLE_FMT_U8P = 5;
1849
- var AV_SAMPLE_FMT_S16P = 6;
1850
- var AV_SAMPLE_FMT_S32P = 7;
1851
- var AV_SAMPLE_FMT_FLTP = 8;
1852
- function libavFrameToInterleavedFloat32(frame) {
1853
- const channels = frame.channels ?? frame.ch_layout_nb_channels ?? 1;
1854
- const sampleRate = frame.sample_rate ?? 44100;
1855
- const nbSamples = frame.nb_samples ?? 0;
1856
- if (nbSamples === 0) return null;
1857
- const out = new Float32Array(nbSamples * channels);
1858
- switch (frame.format) {
1859
- case AV_SAMPLE_FMT_FLTP: {
1860
- const planes = ensurePlanes(frame.data, channels);
1861
- for (let ch = 0; ch < channels; ch++) {
1862
- const plane = asFloat32(planes[ch]);
1863
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i];
1864
- }
1865
- return { data: out, channels, sampleRate };
1866
- }
1867
- case AV_SAMPLE_FMT_FLT: {
1868
- const flat = asFloat32(frame.data);
1869
- for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i];
1870
- return { data: out, channels, sampleRate };
1871
- }
1872
- case AV_SAMPLE_FMT_S16P: {
1873
- const planes = ensurePlanes(frame.data, channels);
1874
- for (let ch = 0; ch < channels; ch++) {
1875
- const plane = asInt16(planes[ch]);
1876
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 32768;
1877
- }
1878
- return { data: out, channels, sampleRate };
1879
- }
1880
- case AV_SAMPLE_FMT_S16: {
1881
- const flat = asInt16(frame.data);
1882
- for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 32768;
1883
- return { data: out, channels, sampleRate };
1884
- }
1885
- case AV_SAMPLE_FMT_S32P: {
1886
- const planes = ensurePlanes(frame.data, channels);
1887
- for (let ch = 0; ch < channels; ch++) {
1888
- const plane = asInt32(planes[ch]);
1889
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 2147483648;
1890
- }
1891
- return { data: out, channels, sampleRate };
1892
- }
1893
- case AV_SAMPLE_FMT_S32: {
1894
- const flat = asInt32(frame.data);
1895
- for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 2147483648;
1896
- return { data: out, channels, sampleRate };
1897
- }
1898
- case AV_SAMPLE_FMT_U8P: {
1899
- const planes = ensurePlanes(frame.data, channels);
1900
- for (let ch = 0; ch < channels; ch++) {
1901
- const plane = asUint8(planes[ch]);
1902
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = (plane[i] - 128) / 128;
1903
- }
1904
- return { data: out, channels, sampleRate };
1905
- }
1906
- case AV_SAMPLE_FMT_U8: {
1907
- const flat = asUint8(frame.data);
1908
- for (let i = 0; i < nbSamples * channels; i++) out[i] = (flat[i] - 128) / 128;
1909
- return { data: out, channels, sampleRate };
1910
- }
1911
- default:
1912
- return null;
1913
- }
1914
- }
1915
- function ensurePlanes(data, channels) {
1916
- if (Array.isArray(data)) return data;
1917
- const arr = data;
1918
- const len = arr.length;
1919
- const perChannel = Math.floor(len / channels);
1920
- const planes = [];
1921
- for (let ch = 0; ch < channels; ch++) {
1922
- planes.push(arr.subarray ? arr.subarray(ch * perChannel, (ch + 1) * perChannel) : arr);
1923
- }
1924
- return planes;
1925
- }
1926
- function asFloat32(x) {
1927
- if (x instanceof Float32Array) return x;
1928
- const ta = x;
1929
- return new Float32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
1930
- }
1931
- function asInt16(x) {
1932
- if (x instanceof Int16Array) return x;
1933
- const ta = x;
1934
- return new Int16Array(ta.buffer, ta.byteOffset, ta.byteLength / 2);
1935
- }
1936
- function asInt32(x) {
1937
- if (x instanceof Int32Array) return x;
1938
- const ta = x;
1939
- return new Int32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
1940
- }
1941
- function asUint8(x) {
1942
- if (x instanceof Uint8Array) return x;
1943
- const ta = x;
1944
- return new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength);
1945
- }
1946
1982
  async function loadBridge() {
1947
1983
  try {
1948
- const wrapper = await import('./libav-import-GST2AMPL.cjs');
1984
+ const wrapper = await import('./libav-import-2ZVKV2E7.cjs');
1949
1985
  return wrapper.libavBridge;
1950
1986
  } catch (err) {
1951
1987
  throw new Error(
@@ -1957,9 +1993,9 @@ async function loadBridge() {
1957
1993
  // src/strategies/hybrid/index.ts
1958
1994
  var READY_AUDIO_BUFFER_SECONDS = 0.3;
1959
1995
  var READY_TIMEOUT_SECONDS = 10;
1960
- async function createHybridSession(ctx, target) {
1961
- const { normalizeSource: normalizeSource2 } = await import('./source-CN43EI7Z.cjs');
1962
- const source = await normalizeSource2(ctx.source);
1996
+ async function createHybridSession(ctx, target, transport) {
1997
+ const { normalizeSource } = await import('./source-VFLXLOCN.cjs');
1998
+ const source = await normalizeSource(ctx.source);
1963
1999
  const fps = ctx.videoTracks[0]?.fps ?? 30;
1964
2000
  const audio = new AudioOutput();
1965
2001
  const renderer = new VideoRenderer(target, audio, fps);
@@ -1970,7 +2006,8 @@ async function createHybridSession(ctx, target) {
1970
2006
  filename: ctx.name ?? "input.bin",
1971
2007
  context: ctx,
1972
2008
  renderer,
1973
- audio
2009
+ audio,
2010
+ transport
1974
2011
  });
1975
2012
  } catch (err) {
1976
2013
  audio.destroy();
@@ -1988,6 +2025,22 @@ async function createHybridSession(ctx, target) {
1988
2025
  configurable: true,
1989
2026
  get: () => !audio.isPlaying()
1990
2027
  });
2028
+ Object.defineProperty(target, "volume", {
2029
+ configurable: true,
2030
+ get: () => audio.getVolume(),
2031
+ set: (v) => {
2032
+ audio.setVolume(v);
2033
+ target.dispatchEvent(new Event("volumechange"));
2034
+ }
2035
+ });
2036
+ Object.defineProperty(target, "muted", {
2037
+ configurable: true,
2038
+ get: () => audio.getMuted(),
2039
+ set: (m) => {
2040
+ audio.setMuted(m);
2041
+ target.dispatchEvent(new Event("volumechange"));
2042
+ }
2043
+ });
1991
2044
  if (ctx.duration && Number.isFinite(ctx.duration)) {
1992
2045
  Object.defineProperty(target, "duration", {
1993
2046
  configurable: true,
@@ -2027,15 +2080,35 @@ async function createHybridSession(ctx, target) {
2027
2080
  if (!audio.isPlaying()) {
2028
2081
  await waitForBuffer();
2029
2082
  await audio.start();
2083
+ target.dispatchEvent(new Event("play"));
2084
+ target.dispatchEvent(new Event("playing"));
2030
2085
  }
2031
2086
  },
2032
2087
  pause() {
2033
2088
  void audio.pause();
2089
+ target.dispatchEvent(new Event("pause"));
2034
2090
  },
2035
2091
  async seek(time) {
2036
2092
  await doSeek(time);
2037
2093
  },
2038
- async setAudioTrack(_id) {
2094
+ async setAudioTrack(id) {
2095
+ if (!ctx.audioTracks.some((t) => t.id === id)) {
2096
+ console.warn("[avbridge] hybrid: setAudioTrack \u2014 unknown track id", id);
2097
+ return;
2098
+ }
2099
+ const wasPlaying = audio.isPlaying();
2100
+ const currentTime = audio.now();
2101
+ await audio.pause().catch(() => {
2102
+ });
2103
+ await handles.setAudioTrack(id, currentTime).catch(
2104
+ (err) => console.warn("[avbridge] hybrid: handles.setAudioTrack failed:", err)
2105
+ );
2106
+ await audio.reset(currentTime);
2107
+ renderer.flush();
2108
+ if (wasPlaying) {
2109
+ await waitForBuffer();
2110
+ await audio.start();
2111
+ }
2039
2112
  },
2040
2113
  async setSubtitleTrack(_id) {
2041
2114
  },
@@ -2053,6 +2126,8 @@ async function createHybridSession(ctx, target) {
2053
2126
  delete target.currentTime;
2054
2127
  delete target.duration;
2055
2128
  delete target.paused;
2129
+ delete target.volume;
2130
+ delete target.muted;
2056
2131
  } catch {
2057
2132
  }
2058
2133
  },
@@ -2064,15 +2139,16 @@ async function createHybridSession(ctx, target) {
2064
2139
 
2065
2140
  // src/strategies/fallback/decoder.ts
2066
2141
  async function startDecoder(opts) {
2067
- const variant = chunkNZU7W256_cjs.pickLibavVariant(opts.context);
2142
+ const variant = chunkF3LQJKXK_cjs.pickLibavVariant(opts.context);
2068
2143
  const libav = await chunkG4APZMCP_cjs.loadLibav(variant);
2069
2144
  const bridge = await loadBridge2();
2070
- const { prepareLibavInput } = await import('./libav-http-reader-FPYDBMYK.cjs');
2071
- const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source);
2145
+ const { prepareLibavInput } = await import('./libav-http-reader-AZLE7YFS.cjs');
2146
+ const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source, opts.transport);
2072
2147
  const readPkt = await libav.av_packet_alloc();
2073
2148
  const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
2074
2149
  const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
2075
- const audioStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO) ?? null;
2150
+ const firstAudioTrackId = opts.context.audioTracks[0]?.id;
2151
+ let audioStream = (firstAudioTrackId != null ? streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO && s.index === firstAudioTrackId) : void 0) ?? streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO) ?? null;
2076
2152
  if (!videoStream && !audioStream) {
2077
2153
  throw new Error("fallback decoder: file has no decodable streams");
2078
2154
  }
@@ -2124,6 +2200,56 @@ async function startDecoder(opts) {
2124
2200
  `fallback decoder: could not initialize any libav decoders (${codecs}).${hint}`
2125
2201
  );
2126
2202
  }
2203
+ let bsfCtx = null;
2204
+ let bsfPkt = null;
2205
+ if (videoStream && opts.context.videoTracks[0]?.codec === "mpeg4") {
2206
+ try {
2207
+ bsfCtx = await libav.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
2208
+ if (bsfCtx != null && bsfCtx >= 0) {
2209
+ const parIn = await libav.AVBSFContext_par_in(bsfCtx);
2210
+ await libav.avcodec_parameters_copy(parIn, videoStream.codecpar);
2211
+ await libav.av_bsf_init(bsfCtx);
2212
+ bsfPkt = await libav.av_packet_alloc();
2213
+ chunkG4APZMCP_cjs.dbg.info("bsf", "mpeg4_unpack_bframes BSF active");
2214
+ } else {
2215
+ console.warn("[avbridge] mpeg4_unpack_bframes BSF not available \u2014 decoding without it");
2216
+ bsfCtx = null;
2217
+ }
2218
+ } catch (err) {
2219
+ console.warn("[avbridge] failed to init mpeg4_unpack_bframes BSF:", err.message);
2220
+ bsfCtx = null;
2221
+ bsfPkt = null;
2222
+ }
2223
+ }
2224
+ async function applyBSF(packets) {
2225
+ if (!bsfCtx || !bsfPkt) return packets;
2226
+ const out = [];
2227
+ for (const pkt of packets) {
2228
+ await libav.ff_copyin_packet(bsfPkt, pkt);
2229
+ const sendErr = await libav.av_bsf_send_packet(bsfCtx, bsfPkt);
2230
+ if (sendErr < 0) {
2231
+ out.push(pkt);
2232
+ continue;
2233
+ }
2234
+ while (true) {
2235
+ const recvErr = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
2236
+ if (recvErr < 0) break;
2237
+ out.push(await libav.ff_copyout_packet(bsfPkt));
2238
+ }
2239
+ }
2240
+ return out;
2241
+ }
2242
+ async function flushBSF() {
2243
+ if (!bsfCtx || !bsfPkt) return;
2244
+ try {
2245
+ await libav.av_bsf_send_packet(bsfCtx, 0);
2246
+ while (true) {
2247
+ const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
2248
+ if (err < 0) break;
2249
+ }
2250
+ } catch {
2251
+ }
2252
+ }
2127
2253
  let destroyed = false;
2128
2254
  let pumpToken = 0;
2129
2255
  let pumpRunning = null;
@@ -2159,7 +2285,8 @@ async function startDecoder(opts) {
2159
2285
  }
2160
2286
  if (myToken !== pumpToken || destroyed) return;
2161
2287
  if (videoDec && videoPackets && videoPackets.length > 0) {
2162
- await decodeVideoBatch(videoPackets, myToken);
2288
+ const processed = await applyBSF(videoPackets);
2289
+ await decodeVideoBatch(processed, myToken);
2163
2290
  }
2164
2291
  packetsRead += (videoPackets?.length ?? 0) + (audioPackets?.length ?? 0);
2165
2292
  if (videoFramesDecoded > 0) {
@@ -2237,7 +2364,7 @@ async function startDecoder(opts) {
2237
2364
  if (myToken !== pumpToken || destroyed) return;
2238
2365
  for (const f of frames) {
2239
2366
  if (myToken !== pumpToken || destroyed) return;
2240
- const bridgeOpts = sanitizeFrameTimestamp2(
2367
+ chunkCPZ7PXAM_cjs.sanitizeFrameTimestamp(
2241
2368
  f,
2242
2369
  () => {
2243
2370
  const ts = syntheticVideoUs;
@@ -2247,7 +2374,7 @@ async function startDecoder(opts) {
2247
2374
  videoTimeBase
2248
2375
  );
2249
2376
  try {
2250
- const vf = bridge.laFrameToVideoFrame(f, bridgeOpts);
2377
+ const vf = bridge.laFrameToVideoFrame(f, { timeBase: [1, 1e6] });
2251
2378
  opts.renderer.enqueue(vf);
2252
2379
  videoFramesDecoded++;
2253
2380
  } catch (err) {
@@ -2275,7 +2402,7 @@ async function startDecoder(opts) {
2275
2402
  if (myToken !== pumpToken || destroyed) return;
2276
2403
  for (const f of frames) {
2277
2404
  if (myToken !== pumpToken || destroyed) return;
2278
- sanitizeFrameTimestamp2(
2405
+ chunkCPZ7PXAM_cjs.sanitizeFrameTimestamp(
2279
2406
  f,
2280
2407
  () => {
2281
2408
  const ts = syntheticAudioUs;
@@ -2286,7 +2413,7 @@ async function startDecoder(opts) {
2286
2413
  },
2287
2414
  audioTimeBase
2288
2415
  );
2289
- const samples = libavFrameToInterleavedFloat322(f);
2416
+ const samples = chunkCPZ7PXAM_cjs.libavFrameToInterleavedFloat32(f);
2290
2417
  if (samples) {
2291
2418
  opts.audio.schedule(samples.data, samples.channels, samples.sampleRate);
2292
2419
  audioFramesDecoded++;
@@ -2305,6 +2432,14 @@ async function startDecoder(opts) {
2305
2432
  await pumpRunning;
2306
2433
  } catch {
2307
2434
  }
2435
+ try {
2436
+ if (bsfCtx) await libav.av_bsf_free(bsfCtx);
2437
+ } catch {
2438
+ }
2439
+ try {
2440
+ if (bsfPkt) await libav.av_packet_free?.(bsfPkt);
2441
+ } catch {
2442
+ }
2308
2443
  try {
2309
2444
  if (videoDec) await libav.ff_free_decoder?.(videoDec.c, videoDec.pkt, videoDec.frame);
2310
2445
  } catch {
@@ -2326,6 +2461,69 @@ async function startDecoder(opts) {
2326
2461
  } catch {
2327
2462
  }
2328
2463
  },
2464
+ async setAudioTrack(trackId, timeSec) {
2465
+ if (audioStream && audioStream.index === trackId) return;
2466
+ const newStream = streams.find(
2467
+ (s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO && s.index === trackId
2468
+ );
2469
+ if (!newStream) {
2470
+ console.warn("[avbridge] fallback: setAudioTrack \u2014 no stream with id", trackId);
2471
+ return;
2472
+ }
2473
+ const newToken = ++pumpToken;
2474
+ if (pumpRunning) {
2475
+ try {
2476
+ await pumpRunning;
2477
+ } catch {
2478
+ }
2479
+ }
2480
+ if (destroyed) return;
2481
+ if (audioDec) {
2482
+ try {
2483
+ await libav.ff_free_decoder?.(audioDec.c, audioDec.pkt, audioDec.frame);
2484
+ } catch {
2485
+ }
2486
+ audioDec = null;
2487
+ }
2488
+ try {
2489
+ const [, c, pkt, frame] = await libav.ff_init_decoder(newStream.codec_id, {
2490
+ codecpar: newStream.codecpar
2491
+ });
2492
+ audioDec = { c, pkt, frame };
2493
+ audioTimeBase = newStream.time_base_num && newStream.time_base_den ? [newStream.time_base_num, newStream.time_base_den] : void 0;
2494
+ } catch (err) {
2495
+ console.warn(
2496
+ "[avbridge] fallback: setAudioTrack init failed \u2014 falling back to no-audio mode:",
2497
+ err.message
2498
+ );
2499
+ audioDec = null;
2500
+ opts.audio.setNoAudio();
2501
+ }
2502
+ audioStream = newStream;
2503
+ try {
2504
+ const tsUs = Math.floor(timeSec * 1e6);
2505
+ const [tsLo, tsHi] = libav.f64toi64 ? libav.f64toi64(tsUs) : [tsUs | 0, Math.floor(tsUs / 4294967296)];
2506
+ await libav.av_seek_frame(
2507
+ fmt_ctx,
2508
+ -1,
2509
+ tsLo,
2510
+ tsHi,
2511
+ libav.AVSEEK_FLAG_BACKWARD ?? 0
2512
+ );
2513
+ } catch (err) {
2514
+ console.warn("[avbridge] fallback: setAudioTrack seek failed:", err);
2515
+ }
2516
+ try {
2517
+ if (videoDec) await libav.avcodec_flush_buffers?.(videoDec.c);
2518
+ } catch {
2519
+ }
2520
+ await flushBSF();
2521
+ syntheticVideoUs = Math.round(timeSec * 1e6);
2522
+ syntheticAudioUs = Math.round(timeSec * 1e6);
2523
+ pumpRunning = pumpLoop(newToken).catch(
2524
+ (err) => console.error("[avbridge] fallback pump failed (post-setAudioTrack):", err)
2525
+ );
2526
+ },
2329
2527
  async seek(timeSec) {
2330
2528
  const newToken = ++pumpToken;
2331
2529
  if (pumpRunning) {
@@ -2356,6 +2554,7 @@ async function startDecoder(opts) {
2356
2554
  if (audioDec) await libav.avcodec_flush_buffers?.(audioDec.c);
2357
2555
  } catch {
2358
2556
  }
2557
+ await flushBSF();
2359
2558
  syntheticVideoUs = Math.round(timeSec * 1e6);
2360
2559
  syntheticAudioUs = Math.round(timeSec * 1e6);
2361
2560
  pumpRunning = pumpLoop(newToken).catch(
@@ -2368,6 +2567,7 @@ async function startDecoder(opts) {
2368
2567
  packetsRead,
2369
2568
  videoFramesDecoded,
2370
2569
  audioFramesDecoded,
2570
+ bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
2371
2571
  // Confirmed transport info: once prepareLibavInput returns
2372
2572
  // successfully, we *know* whether the source is http-range (probe
2373
2573
  // succeeded and returned 206) or in-memory blob. Diagnostics hoists
@@ -2380,138 +2580,9 @@ async function startDecoder(opts) {
2380
2580
  }
2381
2581
  };
2382
2582
  }
2383
- function sanitizeFrameTimestamp2(frame, nextUs, fallbackTimeBase) {
2384
- const lo = frame.pts ?? 0;
2385
- const hi = frame.ptshi ?? 0;
2386
- const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
2387
- if (isInvalid) {
2388
- const us2 = nextUs();
2389
- frame.pts = us2;
2390
- frame.ptshi = 0;
2391
- return { timeBase: [1, 1e6] };
2392
- }
2393
- const tb = fallbackTimeBase ?? [1, 1e6];
2394
- const pts64 = hi * 4294967296 + lo;
2395
- const us = Math.round(pts64 * 1e6 * tb[0] / tb[1]);
2396
- if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
2397
- frame.pts = us;
2398
- frame.ptshi = us < 0 ? -1 : 0;
2399
- return { timeBase: [1, 1e6] };
2400
- }
2401
- const fallback = nextUs();
2402
- frame.pts = fallback;
2403
- frame.ptshi = 0;
2404
- return { timeBase: [1, 1e6] };
2405
- }
2406
- var AV_SAMPLE_FMT_U82 = 0;
2407
- var AV_SAMPLE_FMT_S162 = 1;
2408
- var AV_SAMPLE_FMT_S322 = 2;
2409
- var AV_SAMPLE_FMT_FLT2 = 3;
2410
- var AV_SAMPLE_FMT_U8P2 = 5;
2411
- var AV_SAMPLE_FMT_S16P2 = 6;
2412
- var AV_SAMPLE_FMT_S32P2 = 7;
2413
- var AV_SAMPLE_FMT_FLTP2 = 8;
2414
- function libavFrameToInterleavedFloat322(frame) {
2415
- const channels = frame.channels ?? frame.ch_layout_nb_channels ?? 1;
2416
- const sampleRate = frame.sample_rate ?? 44100;
2417
- const nbSamples = frame.nb_samples ?? 0;
2418
- if (nbSamples === 0) return null;
2419
- const out = new Float32Array(nbSamples * channels);
2420
- switch (frame.format) {
2421
- case AV_SAMPLE_FMT_FLTP2: {
2422
- const planes = ensurePlanes2(frame.data, channels);
2423
- for (let ch = 0; ch < channels; ch++) {
2424
- const plane = asFloat322(planes[ch]);
2425
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i];
2426
- }
2427
- return { data: out, channels, sampleRate };
2428
- }
2429
- case AV_SAMPLE_FMT_FLT2: {
2430
- const flat = asFloat322(frame.data);
2431
- for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i];
2432
- return { data: out, channels, sampleRate };
2433
- }
2434
- case AV_SAMPLE_FMT_S16P2: {
2435
- const planes = ensurePlanes2(frame.data, channels);
2436
- for (let ch = 0; ch < channels; ch++) {
2437
- const plane = asInt162(planes[ch]);
2438
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 32768;
2439
- }
2440
- return { data: out, channels, sampleRate };
2441
- }
2442
- case AV_SAMPLE_FMT_S162: {
2443
- const flat = asInt162(frame.data);
2444
- for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 32768;
2445
- return { data: out, channels, sampleRate };
2446
- }
2447
- case AV_SAMPLE_FMT_S32P2: {
2448
- const planes = ensurePlanes2(frame.data, channels);
2449
- for (let ch = 0; ch < channels; ch++) {
2450
- const plane = asInt322(planes[ch]);
2451
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 2147483648;
2452
- }
2453
- return { data: out, channels, sampleRate };
2454
- }
2455
- case AV_SAMPLE_FMT_S322: {
2456
- const flat = asInt322(frame.data);
2457
- for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 2147483648;
2458
- return { data: out, channels, sampleRate };
2459
- }
2460
- case AV_SAMPLE_FMT_U8P2: {
2461
- const planes = ensurePlanes2(frame.data, channels);
2462
- for (let ch = 0; ch < channels; ch++) {
2463
- const plane = asUint82(planes[ch]);
2464
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = (plane[i] - 128) / 128;
2465
- }
2466
- return { data: out, channels, sampleRate };
2467
- }
2468
- case AV_SAMPLE_FMT_U82: {
2469
- const flat = asUint82(frame.data);
2470
- for (let i = 0; i < nbSamples * channels; i++) out[i] = (flat[i] - 128) / 128;
2471
- return { data: out, channels, sampleRate };
2472
- }
2473
- default:
2474
- if (!globalThis.__avbridgeLoggedSampleFmt) {
2475
- globalThis.__avbridgeLoggedSampleFmt = frame.format;
2476
- console.warn(`[avbridge] unsupported audio sample format from libav: ${frame.format}`);
2477
- }
2478
- return null;
2479
- }
2480
- }
2481
- function ensurePlanes2(data, channels) {
2482
- if (Array.isArray(data)) return data;
2483
- const arr = data;
2484
- const len = arr.length;
2485
- const perChannel = Math.floor(len / channels);
2486
- const planes = [];
2487
- for (let ch = 0; ch < channels; ch++) {
2488
- planes.push(arr.subarray ? arr.subarray(ch * perChannel, (ch + 1) * perChannel) : arr);
2489
- }
2490
- return planes;
2491
- }
2492
- function asFloat322(x) {
2493
- if (x instanceof Float32Array) return x;
2494
- const ta = x;
2495
- return new Float32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
2496
- }
2497
- function asInt162(x) {
2498
- if (x instanceof Int16Array) return x;
2499
- const ta = x;
2500
- return new Int16Array(ta.buffer, ta.byteOffset, ta.byteLength / 2);
2501
- }
2502
- function asInt322(x) {
2503
- if (x instanceof Int32Array) return x;
2504
- const ta = x;
2505
- return new Int32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
2506
- }
2507
- function asUint82(x) {
2508
- if (x instanceof Uint8Array) return x;
2509
- const ta = x;
2510
- return new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength);
2511
- }
2512
2583
  async function loadBridge2() {
2513
2584
  try {
2514
- const wrapper = await import('./libav-import-GST2AMPL.cjs');
2585
+ const wrapper = await import('./libav-import-2ZVKV2E7.cjs');
2515
2586
  return wrapper.libavBridge;
2516
2587
  } catch (err) {
2517
2588
  throw new Error(
@@ -2523,9 +2594,9 @@ async function loadBridge2() {
2523
2594
  // src/strategies/fallback/index.ts
2524
2595
  var READY_AUDIO_BUFFER_SECONDS2 = 0.04;
2525
2596
  var READY_TIMEOUT_SECONDS2 = 3;
2526
- async function createFallbackSession(ctx, target) {
2527
- const { normalizeSource: normalizeSource2 } = await import('./source-CN43EI7Z.cjs');
2528
- const source = await normalizeSource2(ctx.source);
2597
+ async function createFallbackSession(ctx, target, transport) {
2598
+ const { normalizeSource } = await import('./source-VFLXLOCN.cjs');
2599
+ const source = await normalizeSource(ctx.source);
2529
2600
  const fps = ctx.videoTracks[0]?.fps ?? 30;
2530
2601
  const audio = new AudioOutput();
2531
2602
  const renderer = new VideoRenderer(target, audio, fps);
@@ -2536,7 +2607,8 @@ async function createFallbackSession(ctx, target) {
2536
2607
  filename: ctx.name ?? "input.bin",
2537
2608
  context: ctx,
2538
2609
  renderer,
2539
- audio
2610
+ audio,
2611
+ transport
2540
2612
  });
2541
2613
  } catch (err) {
2542
2614
  audio.destroy();
@@ -2554,6 +2626,22 @@ async function createFallbackSession(ctx, target) {
2554
2626
  configurable: true,
2555
2627
  get: () => !audio.isPlaying()
2556
2628
  });
2629
+ Object.defineProperty(target, "volume", {
2630
+ configurable: true,
2631
+ get: () => audio.getVolume(),
2632
+ set: (v) => {
2633
+ audio.setVolume(v);
2634
+ target.dispatchEvent(new Event("volumechange"));
2635
+ }
2636
+ });
2637
+ Object.defineProperty(target, "muted", {
2638
+ configurable: true,
2639
+ get: () => audio.getMuted(),
2640
+ set: (m) => {
2641
+ audio.setMuted(m);
2642
+ target.dispatchEvent(new Event("volumechange"));
2643
+ }
2644
+ });
2557
2645
  if (ctx.duration && Number.isFinite(ctx.duration)) {
2558
2646
  Object.defineProperty(target, "duration", {
2559
2647
  configurable: true,
@@ -2617,15 +2705,35 @@ async function createFallbackSession(ctx, target) {
2617
2705
  if (!audio.isPlaying()) {
2618
2706
  await waitForBuffer();
2619
2707
  await audio.start();
2708
+ target.dispatchEvent(new Event("play"));
2709
+ target.dispatchEvent(new Event("playing"));
2620
2710
  }
2621
2711
  },
2622
2712
  pause() {
2623
2713
  void audio.pause();
2714
+ target.dispatchEvent(new Event("pause"));
2624
2715
  },
2625
2716
  async seek(time) {
2626
2717
  await doSeek(time);
2627
2718
  },
2628
- async setAudioTrack(_id) {
2719
+ async setAudioTrack(id) {
2720
+ if (!ctx.audioTracks.some((t) => t.id === id)) {
2721
+ console.warn("[avbridge] fallback: setAudioTrack \u2014 unknown track id", id);
2722
+ return;
2723
+ }
2724
+ const wasPlaying = audio.isPlaying();
2725
+ const currentTime = audio.now();
2726
+ await audio.pause().catch(() => {
2727
+ });
2728
+ await handles.setAudioTrack(id, currentTime).catch(
2729
+ (err) => console.warn("[avbridge] fallback: handles.setAudioTrack failed:", err)
2730
+ );
2731
+ await audio.reset(currentTime);
2732
+ renderer.flush();
2733
+ if (wasPlaying) {
2734
+ await waitForBuffer();
2735
+ await audio.start();
2736
+ }
2629
2737
  },
2630
2738
  async setSubtitleTrack(_id) {
2631
2739
  },
@@ -2640,6 +2748,8 @@ async function createFallbackSession(ctx, target) {
2640
2748
  delete target.currentTime;
2641
2749
  delete target.duration;
2642
2750
  delete target.paused;
2751
+ delete target.volume;
2752
+ delete target.muted;
2643
2753
  } catch {
2644
2754
  }
2645
2755
  },
@@ -2663,12 +2773,12 @@ var remuxPlugin = {
2663
2773
  var hybridPlugin = {
2664
2774
  name: "hybrid",
2665
2775
  canHandle: () => typeof VideoDecoder !== "undefined",
2666
- execute: (ctx, video) => createHybridSession(ctx, video)
2776
+ execute: (ctx, video, transport) => createHybridSession(ctx, video, transport)
2667
2777
  };
2668
2778
  var fallbackPlugin = {
2669
2779
  name: "fallback",
2670
2780
  canHandle: () => true,
2671
- execute: (ctx, video) => createFallbackSession(ctx, video)
2781
+ execute: (ctx, video, transport) => createFallbackSession(ctx, video, transport)
2672
2782
  };
2673
2783
  function registerBuiltins(registry) {
2674
2784
  registry.register(nativePlugin);
@@ -2677,88 +2787,6 @@ function registerBuiltins(registry) {
2677
2787
  registry.register(fallbackPlugin);
2678
2788
  }
2679
2789
 
2680
- // src/subtitles/vtt.ts
2681
- function isVtt(text) {
2682
- const trimmed = text.replace(/^\ufeff/, "").trimStart();
2683
- return trimmed.startsWith("WEBVTT");
2684
- }
2685
-
2686
- // src/subtitles/index.ts
2687
- async function discoverSidecars(file, directory) {
2688
- const baseName = file.name.replace(/\.[^.]+$/, "");
2689
- const found = [];
2690
- for await (const [name, handle] of directory) {
2691
- if (handle.kind !== "file") continue;
2692
- if (!name.startsWith(baseName)) continue;
2693
- const lower = name.toLowerCase();
2694
- let format = null;
2695
- if (lower.endsWith(".srt")) format = "srt";
2696
- else if (lower.endsWith(".vtt")) format = "vtt";
2697
- if (!format) continue;
2698
- const sidecarFile = await handle.getFile();
2699
- const url = URL.createObjectURL(sidecarFile);
2700
- const langMatch = name.slice(baseName.length).match(/[._-]([a-z]{2,3})(?:[._-]|\.)/i);
2701
- found.push({
2702
- url,
2703
- format,
2704
- language: langMatch?.[1]
2705
- });
2706
- }
2707
- return found;
2708
- }
2709
- var SubtitleResourceBag = class {
2710
- urls = /* @__PURE__ */ new Set();
2711
- /** Track an externally-created blob URL (e.g. from `discoverSidecars`). */
2712
- track(url) {
2713
- this.urls.add(url);
2714
- }
2715
- /** Convenience: create a blob URL and track it in one call. */
2716
- createObjectURL(blob) {
2717
- const url = URL.createObjectURL(blob);
2718
- this.urls.add(url);
2719
- return url;
2720
- }
2721
- /** Revoke every tracked URL. Idempotent — safe to call multiple times. */
2722
- revokeAll() {
2723
- for (const u of this.urls) URL.revokeObjectURL(u);
2724
- this.urls.clear();
2725
- }
2726
- };
2727
- async function attachSubtitleTracks(video, tracks, bag, onError) {
2728
- for (const t of Array.from(video.querySelectorAll("track[data-avbridge]"))) {
2729
- t.remove();
2730
- }
2731
- for (const t of tracks) {
2732
- if (!t.sidecarUrl) continue;
2733
- try {
2734
- let url = t.sidecarUrl;
2735
- if (t.format === "srt") {
2736
- const res = await fetch(t.sidecarUrl);
2737
- const text = await res.text();
2738
- const vtt = srtToVtt(text);
2739
- const blob = new Blob([vtt], { type: "text/vtt" });
2740
- url = bag ? bag.createObjectURL(blob) : URL.createObjectURL(blob);
2741
- } else if (t.format === "vtt") {
2742
- const res = await fetch(t.sidecarUrl);
2743
- const text = await res.text();
2744
- if (!isVtt(text)) {
2745
- console.warn("[avbridge] subtitle missing WEBVTT header:", t.sidecarUrl);
2746
- }
2747
- }
2748
- const trackEl = document.createElement("track");
2749
- trackEl.kind = "subtitles";
2750
- trackEl.src = url;
2751
- trackEl.srclang = t.language ?? "und";
2752
- trackEl.label = t.language ?? `Subtitle ${t.id}`;
2753
- trackEl.dataset.avbridge = "true";
2754
- video.appendChild(trackEl);
2755
- } catch (err) {
2756
- const e = err instanceof Error ? err : new Error(String(err));
2757
- onError?.(e, t);
2758
- }
2759
- }
2760
- }
2761
-
2762
2790
  // src/player.ts
2763
2791
  var UnifiedPlayer = class _UnifiedPlayer {
2764
2792
  /**
@@ -2767,6 +2795,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
2767
2795
  constructor(options, registry) {
2768
2796
  this.options = options;
2769
2797
  this.registry = registry;
2798
+ const { requestInit, fetchFn } = options;
2799
+ if (requestInit || fetchFn) {
2800
+ this.transport = { requestInit, fetchFn };
2801
+ }
2770
2802
  }
2771
2803
  options;
2772
2804
  registry;
@@ -2786,11 +2818,23 @@ var UnifiedPlayer = class _UnifiedPlayer {
2786
2818
  // listener outlives the player and accumulates on elements that swap
2787
2819
  // source (e.g. <avbridge-video>).
2788
2820
  endedListener = null;
2821
+ // Background tab handling. userIntent is what the user last asked for
2822
+ // (play vs pause) — used to decide whether to auto-resume on visibility
2823
+ // return. autoPausedForVisibility tracks whether we paused because the
2824
+ // tab was hidden, so we don't resume playback the user deliberately
2825
+ // paused (e.g. via media keys while hidden).
2826
+ userIntent = "pause";
2827
+ autoPausedForVisibility = false;
2828
+ visibilityListener = null;
2789
2829
  // Serializes escalation / setStrategy calls
2790
2830
  switchingPromise = Promise.resolve();
2791
2831
  // Owns blob URLs created during sidecar discovery + SRT->VTT conversion.
2792
2832
  // Revoked at destroy() so repeated source swaps don't leak.
2793
- subtitleResources = new SubtitleResourceBag();
2833
+ subtitleResources = new chunkS4WAZC2T_cjs.SubtitleResourceBag();
2834
+ // Transport config extracted from CreatePlayerOptions. Threaded to probe,
2835
+ // subtitle fetches, and strategy session creators. Not stored on MediaContext
2836
+ // because it's runtime config, not media analysis.
2837
+ transport;
2794
2838
  static async create(options) {
2795
2839
  const registry = new PluginRegistry();
2796
2840
  registerBuiltins(registry);
@@ -2814,7 +2858,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
2814
2858
  const bootstrapStart = performance.now();
2815
2859
  try {
2816
2860
  chunkG4APZMCP_cjs.dbg.info("bootstrap", "start");
2817
- const ctx = await chunkG4APZMCP_cjs.dbg.timed("probe", "probe", 3e3, () => probe(this.options.source));
2861
+ const ctx = await chunkG4APZMCP_cjs.dbg.timed("probe", "probe", 3e3, () => chunkZCUXHW55_cjs.probe(this.options.source, this.transport));
2818
2862
  chunkG4APZMCP_cjs.dbg.info(
2819
2863
  "probe",
2820
2864
  `container=${ctx.container} video=${ctx.videoTracks[0]?.codec ?? "-"} audio=${ctx.audioTracks[0]?.codec ?? "-"} probedBy=${ctx.probedBy}`
@@ -2832,7 +2876,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
2832
2876
  }
2833
2877
  }
2834
2878
  if (this.options.directory && this.options.source instanceof File) {
2835
- const found = await discoverSidecars(this.options.source, this.options.directory);
2879
+ const found = await chunkS4WAZC2T_cjs.discoverSidecars(this.options.source, this.options.directory);
2836
2880
  for (const s of found) {
2837
2881
  this.subtitleResources.track(s.url);
2838
2882
  ctx.subtitleTracks.push({
@@ -2855,16 +2899,15 @@ var UnifiedPlayer = class _UnifiedPlayer {
2855
2899
  reason: decision.reason
2856
2900
  });
2857
2901
  await this.startSession(decision.strategy, decision.reason);
2858
- if (this.session.strategy !== "fallback" && this.session.strategy !== "hybrid") {
2859
- await attachSubtitleTracks(
2860
- this.options.target,
2861
- ctx.subtitleTracks,
2862
- this.subtitleResources,
2863
- (err, track) => {
2864
- console.warn(`[avbridge] subtitle ${track.id} failed: ${err.message}`);
2865
- }
2866
- );
2867
- }
2902
+ await chunkS4WAZC2T_cjs.attachSubtitleTracks(
2903
+ this.options.target,
2904
+ ctx.subtitleTracks,
2905
+ this.subtitleResources,
2906
+ (err, track) => {
2907
+ console.warn(`[avbridge] subtitle ${track.id} failed: ${err.message}`);
2908
+ },
2909
+ this.transport
2910
+ );
2868
2911
  this.emitter.emitSticky("tracks", {
2869
2912
  video: ctx.videoTracks,
2870
2913
  audio: ctx.audioTracks,
@@ -2873,6 +2916,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
2873
2916
  this.startTimeupdateLoop();
2874
2917
  this.endedListener = () => this.emitter.emit("ended", void 0);
2875
2918
  this.options.target.addEventListener("ended", this.endedListener);
2919
+ if (this.options.backgroundBehavior !== "continue" && typeof document !== "undefined") {
2920
+ this.visibilityListener = () => this.onVisibilityChange();
2921
+ document.addEventListener("visibilitychange", this.visibilityListener);
2922
+ }
2876
2923
  this.emitter.emitSticky("ready", void 0);
2877
2924
  const bootstrapElapsed = performance.now() - bootstrapStart;
2878
2925
  chunkG4APZMCP_cjs.dbg.info("bootstrap", `ready in ${bootstrapElapsed.toFixed(0)}ms`);
@@ -2899,7 +2946,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
2899
2946
  throw new Error(`no plugin available for strategy "${strategy}"`);
2900
2947
  }
2901
2948
  try {
2902
- this.session = await plugin.execute(this.mediaContext, this.options.target);
2949
+ this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
2903
2950
  } catch (err) {
2904
2951
  const chain = this.classification?.fallbackChain;
2905
2952
  if (chain && chain.length > 0) {
@@ -2972,7 +3019,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
2972
3019
  continue;
2973
3020
  }
2974
3021
  try {
2975
- this.session = await plugin.execute(this.mediaContext, this.options.target);
3022
+ this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
2976
3023
  } catch (err) {
2977
3024
  const msg = err instanceof Error ? err.message : String(err);
2978
3025
  errors.push(`${nextStrategy}: ${msg}`);
@@ -2995,8 +3042,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
2995
3042
  }
2996
3043
  return;
2997
3044
  }
2998
- this.emitter.emit("error", new Error(
2999
- `all fallback strategies failed: ${errors.join("; ")}`
3045
+ this.emitter.emit("error", new chunk2IJ66NTD_cjs.AvbridgeError(
3046
+ chunk2IJ66NTD_cjs.ERR_ALL_STRATEGIES_EXHAUSTED,
3047
+ `All playback strategies failed: ${errors.join("; ")}`,
3048
+ "This file may require a codec or container that isn't available in this browser. Try the fallback strategy or check browser codec support."
3000
3049
  ));
3001
3050
  }
3002
3051
  // ── Stall supervision ─────────────────────────────────────────────────
@@ -3048,7 +3097,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
3048
3097
  // ── Public: manual strategy switch ────────────────────────────────────
3049
3098
  /** Manually switch to a different playback strategy. Preserves current position and play/pause state. Concurrent calls are serialized. */
3050
3099
  async setStrategy(strategy, reason) {
3051
- if (!this.mediaContext) throw new Error("player not ready");
3100
+ if (!this.mediaContext) throw new chunk2IJ66NTD_cjs.AvbridgeError(chunk2IJ66NTD_cjs.ERR_PLAYER_NOT_READY, "Player not ready \u2014 wait for the 'ready' event before calling playback methods.", "Await the 'ready' event or check player.readyState before calling play/pause/seek.");
3052
3101
  if (this.session?.strategy === strategy) return;
3053
3102
  this.switchingPromise = this.switchingPromise.then(
3054
3103
  () => this.doSetStrategy(strategy, reason)
@@ -3077,7 +3126,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
3077
3126
  }
3078
3127
  const plugin = this.registry.findFor(this.mediaContext, strategy);
3079
3128
  if (!plugin) throw new Error(`no plugin available for strategy "${strategy}"`);
3080
- this.session = await plugin.execute(this.mediaContext, this.options.target);
3129
+ this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
3081
3130
  this.emitter.emitSticky("strategy", {
3082
3131
  strategy,
3083
3132
  reason: switchReason
@@ -3111,26 +3160,56 @@ var UnifiedPlayer = class _UnifiedPlayer {
3111
3160
  }
3112
3161
  /** Begin or resume playback. Throws if the player is not ready. */
3113
3162
  async play() {
3114
- if (!this.session) throw new Error("player not ready");
3163
+ if (!this.session) throw new chunk2IJ66NTD_cjs.AvbridgeError(chunk2IJ66NTD_cjs.ERR_PLAYER_NOT_READY, "Player not ready \u2014 wait for the 'ready' event before calling playback methods.", "Await the 'ready' event or check player.readyState before calling play/pause/seek.");
3164
+ this.userIntent = "play";
3165
+ this.autoPausedForVisibility = false;
3115
3166
  await this.session.play();
3116
3167
  }
3117
3168
  /** Pause playback. No-op if the player is not ready or already paused. */
3118
3169
  pause() {
3170
+ this.userIntent = "pause";
3171
+ this.autoPausedForVisibility = false;
3119
3172
  this.session?.pause();
3120
3173
  }
3174
+ /**
3175
+ * Handle browser tab visibility changes. On hide: pause if the user
3176
+ * had been playing. On show: resume if we were the one who paused.
3177
+ * Skips when `backgroundBehavior: "continue"` is set (listener isn't
3178
+ * installed in that case).
3179
+ */
3180
+ onVisibilityChange() {
3181
+ if (!this.session) return;
3182
+ const action = decideVisibilityAction({
3183
+ hidden: document.hidden,
3184
+ userIntent: this.userIntent,
3185
+ sessionIsPlaying: !this.options.target.paused,
3186
+ autoPausedForVisibility: this.autoPausedForVisibility
3187
+ });
3188
+ if (action === "pause") {
3189
+ this.autoPausedForVisibility = true;
3190
+ chunkG4APZMCP_cjs.dbg.info("visibility", "tab hidden \u2014 auto-paused");
3191
+ this.session.pause();
3192
+ } else if (action === "resume") {
3193
+ this.autoPausedForVisibility = false;
3194
+ chunkG4APZMCP_cjs.dbg.info("visibility", "tab visible \u2014 auto-resuming");
3195
+ void this.session.play().catch((err) => {
3196
+ console.warn("[avbridge] auto-resume after tab return failed:", err);
3197
+ });
3198
+ }
3199
+ }
3121
3200
  /** Seek to the given time in seconds. Throws if the player is not ready. */
3122
3201
  async seek(time) {
3123
- if (!this.session) throw new Error("player not ready");
3202
+ if (!this.session) throw new chunk2IJ66NTD_cjs.AvbridgeError(chunk2IJ66NTD_cjs.ERR_PLAYER_NOT_READY, "Player not ready \u2014 wait for the 'ready' event before calling playback methods.", "Await the 'ready' event or check player.readyState before calling play/pause/seek.");
3124
3203
  await this.session.seek(time);
3125
3204
  }
3126
3205
  /** Switch the active audio track by track ID. Throws if the player is not ready. */
3127
3206
  async setAudioTrack(id) {
3128
- if (!this.session) throw new Error("player not ready");
3207
+ if (!this.session) throw new chunk2IJ66NTD_cjs.AvbridgeError(chunk2IJ66NTD_cjs.ERR_PLAYER_NOT_READY, "Player not ready \u2014 wait for the 'ready' event before calling playback methods.", "Await the 'ready' event or check player.readyState before calling play/pause/seek.");
3129
3208
  await this.session.setAudioTrack(id);
3130
3209
  }
3131
3210
  /** Switch the active subtitle track by track ID, or pass `null` to disable subtitles. */
3132
3211
  async setSubtitleTrack(id) {
3133
- if (!this.session) throw new Error("player not ready");
3212
+ if (!this.session) throw new chunk2IJ66NTD_cjs.AvbridgeError(chunk2IJ66NTD_cjs.ERR_PLAYER_NOT_READY, "Player not ready \u2014 wait for the 'ready' event before calling playback methods.", "Await the 'ready' event or check player.readyState before calling play/pause/seek.");
3134
3213
  await this.session.setSubtitleTrack(id);
3135
3214
  }
3136
3215
  /** Return a snapshot of current diagnostics: container, codecs, strategy, runtime stats, and strategy history. */
@@ -3162,6 +3241,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
3162
3241
  this.options.target.removeEventListener("ended", this.endedListener);
3163
3242
  this.endedListener = null;
3164
3243
  }
3244
+ if (this.visibilityListener) {
3245
+ document.removeEventListener("visibilitychange", this.visibilityListener);
3246
+ this.visibilityListener = null;
3247
+ }
3165
3248
  if (this.session) {
3166
3249
  await this.session.destroy();
3167
3250
  this.session = null;
@@ -3173,6 +3256,14 @@ var UnifiedPlayer = class _UnifiedPlayer {
3173
3256
  async function createPlayer(options) {
3174
3257
  return UnifiedPlayer.create(options);
3175
3258
  }
3259
+ function decideVisibilityAction(state) {
3260
+ if (state.hidden) {
3261
+ if (state.userIntent === "play" && state.sessionIsPlaying) return "pause";
3262
+ return "noop";
3263
+ }
3264
+ if (state.autoPausedForVisibility) return "resume";
3265
+ return "noop";
3266
+ }
3176
3267
  function buildInitialDecision(initial, ctx) {
3177
3268
  const natural = classifyContext(ctx);
3178
3269
  const cls = strategyToClass(initial, natural);
@@ -3211,13 +3302,12 @@ function defaultFallbackChain(strategy) {
3211
3302
  }
3212
3303
  }
3213
3304
 
3305
+ exports.FALLBACK_AUDIO_CODECS = FALLBACK_AUDIO_CODECS;
3306
+ exports.FALLBACK_VIDEO_CODECS = FALLBACK_VIDEO_CODECS;
3307
+ exports.NATIVE_AUDIO_CODECS = NATIVE_AUDIO_CODECS;
3308
+ exports.NATIVE_VIDEO_CODECS = NATIVE_VIDEO_CODECS;
3214
3309
  exports.UnifiedPlayer = UnifiedPlayer;
3215
- exports.avbridgeAudioToMediabunny = avbridgeAudioToMediabunny;
3216
- exports.avbridgeVideoToMediabunny = avbridgeVideoToMediabunny;
3217
- exports.buildMediabunnySourceFromInput = buildMediabunnySourceFromInput;
3218
3310
  exports.classifyContext = classifyContext;
3219
3311
  exports.createPlayer = createPlayer;
3220
- exports.probe = probe;
3221
- exports.srtToVtt = srtToVtt;
3222
- //# sourceMappingURL=chunk-UF2N5L63.cjs.map
3223
- //# sourceMappingURL=chunk-UF2N5L63.cjs.map
3312
+ //# sourceMappingURL=chunk-TBW26OPP.cjs.map
3313
+ //# sourceMappingURL=chunk-TBW26OPP.cjs.map