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