avbridge 2.2.0 → 2.3.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 (121) hide show
  1. package/CHANGELOG.md +125 -1
  2. package/NOTICE.md +2 -2
  3. package/README.md +100 -74
  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-ILKDNBSE.js → chunk-2PGRFCWB.js} +59 -10
  14. package/dist/chunk-2PGRFCWB.js.map +1 -0
  15. package/dist/chunk-5YAWWKA3.js +18 -0
  16. package/dist/chunk-5YAWWKA3.js.map +1 -0
  17. package/dist/chunk-6UUT4BEA.cjs +219 -0
  18. package/dist/chunk-6UUT4BEA.cjs.map +1 -0
  19. package/dist/{chunk-OE66B34H.cjs → chunk-7RGG6ME7.cjs} +562 -94
  20. package/dist/chunk-7RGG6ME7.cjs.map +1 -0
  21. package/dist/{chunk-WD2ZNQA7.js → chunk-DCSOQH2N.js} +7 -4
  22. package/dist/chunk-DCSOQH2N.js.map +1 -0
  23. package/dist/chunk-F3LQJKXK.cjs +20 -0
  24. package/dist/chunk-F3LQJKXK.cjs.map +1 -0
  25. package/dist/chunk-IAYKFGFG.js +200 -0
  26. package/dist/chunk-IAYKFGFG.js.map +1 -0
  27. package/dist/chunk-NNVOHKXJ.cjs +204 -0
  28. package/dist/chunk-NNVOHKXJ.cjs.map +1 -0
  29. package/dist/{chunk-C5VA5U5O.js → chunk-NV7ILLWH.js} +556 -92
  30. package/dist/chunk-NV7ILLWH.js.map +1 -0
  31. package/dist/{chunk-HZLQNKFN.cjs → chunk-QQXBPW72.js} +54 -15
  32. package/dist/chunk-QQXBPW72.js.map +1 -0
  33. package/dist/chunk-XKPSTC34.cjs +210 -0
  34. package/dist/chunk-XKPSTC34.cjs.map +1 -0
  35. package/dist/{chunk-L4NPOJ36.cjs → chunk-Z33SBWL5.cjs} +7 -4
  36. package/dist/chunk-Z33SBWL5.cjs.map +1 -0
  37. package/dist/element-browser.js +631 -103
  38. package/dist/element-browser.js.map +1 -1
  39. package/dist/element.cjs +4 -4
  40. package/dist/element.d.cts +1 -1
  41. package/dist/element.d.ts +1 -1
  42. package/dist/element.js +3 -3
  43. package/dist/index.cjs +174 -26
  44. package/dist/index.cjs.map +1 -1
  45. package/dist/index.d.cts +48 -4
  46. package/dist/index.d.ts +48 -4
  47. package/dist/index.js +93 -12
  48. package/dist/index.js.map +1 -1
  49. package/dist/libav-http-reader-AZLE7YFS.cjs +16 -0
  50. package/dist/{libav-http-reader-FPYDBMYK.cjs.map → libav-http-reader-AZLE7YFS.cjs.map} +1 -1
  51. package/dist/libav-http-reader-WXG3Z7AI.js +3 -0
  52. package/dist/{libav-http-reader-NQJVY273.js.map → libav-http-reader-WXG3Z7AI.js.map} +1 -1
  53. package/dist/{player-DUyvltvy.d.cts → player-B6WB74RD.d.cts} +63 -3
  54. package/dist/{player-DUyvltvy.d.ts → player-B6WB74RD.d.ts} +63 -3
  55. package/dist/player.cjs +5500 -0
  56. package/dist/player.cjs.map +1 -0
  57. package/dist/player.d.cts +649 -0
  58. package/dist/player.d.ts +649 -0
  59. package/dist/player.js +5498 -0
  60. package/dist/player.js.map +1 -0
  61. package/dist/source-73CAH6HW.cjs +28 -0
  62. package/dist/{source-CN43EI7Z.cjs.map → source-73CAH6HW.cjs.map} +1 -1
  63. package/dist/source-F656KYYV.js +3 -0
  64. package/dist/{source-FFZ7TW2B.js.map → source-F656KYYV.js.map} +1 -1
  65. package/dist/source-QJR3OHTW.js +3 -0
  66. package/dist/source-QJR3OHTW.js.map +1 -0
  67. package/dist/source-VB74JQ7Z.cjs +28 -0
  68. package/dist/source-VB74JQ7Z.cjs.map +1 -0
  69. package/dist/variant-routing-434STYAB.js +3 -0
  70. package/dist/{variant-routing-JOBWXYKD.js.map → variant-routing-434STYAB.js.map} +1 -1
  71. package/dist/variant-routing-HONNAA6R.cjs +12 -0
  72. package/dist/{variant-routing-GOHB2RZN.cjs.map → variant-routing-HONNAA6R.cjs.map} +1 -1
  73. package/package.json +9 -1
  74. package/src/classify/rules.ts +27 -5
  75. package/src/convert/remux.ts +8 -0
  76. package/src/convert/transcode.ts +41 -8
  77. package/src/element/avbridge-player.ts +845 -0
  78. package/src/element/player-icons.ts +25 -0
  79. package/src/element/player-styles.ts +472 -0
  80. package/src/errors.ts +47 -0
  81. package/src/index.ts +23 -0
  82. package/src/player-element.ts +18 -0
  83. package/src/player.ts +127 -27
  84. package/src/plugins/builtin.ts +2 -2
  85. package/src/probe/avi.ts +4 -0
  86. package/src/probe/index.ts +40 -10
  87. package/src/strategies/fallback/audio-output.ts +31 -0
  88. package/src/strategies/fallback/decoder.ts +83 -2
  89. package/src/strategies/fallback/index.ts +34 -1
  90. package/src/strategies/fallback/variant-routing.ts +7 -13
  91. package/src/strategies/fallback/video-renderer.ts +129 -33
  92. package/src/strategies/hybrid/decoder.ts +131 -20
  93. package/src/strategies/hybrid/index.ts +36 -2
  94. package/src/strategies/remux/index.ts +13 -1
  95. package/src/strategies/remux/mse.ts +12 -2
  96. package/src/strategies/remux/pipeline.ts +6 -0
  97. package/src/subtitles/index.ts +7 -3
  98. package/src/types.ts +53 -1
  99. package/src/util/libav-http-reader.ts +5 -1
  100. package/src/util/source.ts +28 -8
  101. package/src/util/transport.ts +26 -0
  102. package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.mjs +1 -1
  103. package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.wasm +0 -0
  104. package/dist/avi-6SJLWIWW.cjs.map +0 -1
  105. package/dist/avi-GCGM7OJI.js.map +0 -1
  106. package/dist/chunk-C5VA5U5O.js.map +0 -1
  107. package/dist/chunk-HZLQNKFN.cjs.map +0 -1
  108. package/dist/chunk-ILKDNBSE.js.map +0 -1
  109. package/dist/chunk-J5MCMN3S.js +0 -27
  110. package/dist/chunk-J5MCMN3S.js.map +0 -1
  111. package/dist/chunk-L4NPOJ36.cjs.map +0 -1
  112. package/dist/chunk-NZU7W256.cjs +0 -29
  113. package/dist/chunk-NZU7W256.cjs.map +0 -1
  114. package/dist/chunk-OE66B34H.cjs.map +0 -1
  115. package/dist/chunk-WD2ZNQA7.js.map +0 -1
  116. package/dist/libav-http-reader-FPYDBMYK.cjs +0 -16
  117. package/dist/libav-http-reader-NQJVY273.js +0 -3
  118. package/dist/source-CN43EI7Z.cjs +0 -28
  119. package/dist/source-FFZ7TW2B.js +0 -3
  120. package/dist/variant-routing-GOHB2RZN.cjs +0 -12
  121. package/dist/variant-routing-JOBWXYKD.js +0 -3
@@ -1,6 +1,6 @@
1
- import { normalizeSource, sniffNormalizedSource } from './chunk-ILKDNBSE.js';
1
+ import { normalizeSource, sniffNormalizedSource, AvbridgeError, ERR_PROBE_FAILED, ERR_PROBE_UNKNOWN_CONTAINER, ERR_LIBAV_NOT_REACHABLE, ERR_ALL_STRATEGIES_EXHAUSTED, ERR_PLAYER_NOT_READY, fetchWith, ERR_MSE_NOT_SUPPORTED, ERR_MSE_CODEC_NOT_SUPPORTED } from './chunk-2PGRFCWB.js';
2
2
  import { dbg, loadLibav } from './chunk-5DMTJVIU.js';
3
- import { pickLibavVariant } from './chunk-J5MCMN3S.js';
3
+ import { pickLibavVariant } from './chunk-5YAWWKA3.js';
4
4
 
5
5
  // src/probe/mediabunny.ts
6
6
  async function probeWithMediabunny(source, sniffedContainer) {
@@ -178,37 +178,58 @@ var MEDIABUNNY_CONTAINERS = /* @__PURE__ */ new Set([
178
178
  "adts",
179
179
  "mpegts"
180
180
  ]);
181
- async function probe(source) {
182
- const normalized = await normalizeSource(source);
181
+ async function probe(source, transport) {
182
+ const normalized = await normalizeSource(source, transport);
183
183
  const sniffed = await sniffNormalizedSource(normalized);
184
184
  if (MEDIABUNNY_CONTAINERS.has(sniffed)) {
185
185
  try {
186
- return await probeWithMediabunny(normalized, sniffed);
186
+ const result = await probeWithMediabunny(normalized, sniffed);
187
+ const hasUnknownCodec = result.videoTracks.some((t) => t.codec === "unknown") || result.audioTracks.some((t) => t.codec === "unknown");
188
+ if (hasUnknownCodec) {
189
+ try {
190
+ const { probeWithLibav } = await import('./avi-NJXAXUXK.js');
191
+ return await probeWithLibav(normalized, sniffed);
192
+ } catch {
193
+ return result;
194
+ }
195
+ }
196
+ return result;
187
197
  } catch (mediabunnyErr) {
188
198
  console.warn(
189
199
  `[avbridge] mediabunny rejected ${sniffed} file, falling back to libav:`,
190
200
  mediabunnyErr.message
191
201
  );
192
202
  try {
193
- const { probeWithLibav } = await import('./avi-GCGM7OJI.js');
203
+ const { probeWithLibav } = await import('./avi-NJXAXUXK.js');
194
204
  return await probeWithLibav(normalized, sniffed);
195
205
  } catch (libavErr) {
196
206
  const mbMsg = mediabunnyErr.message || String(mediabunnyErr);
197
207
  const lvMsg = libavErr instanceof Error ? libavErr.message : String(libavErr);
198
- throw new Error(
199
- `failed to probe ${sniffed} file. mediabunny: ${mbMsg}. libav fallback: ${lvMsg}.`
208
+ throw new AvbridgeError(
209
+ ERR_PROBE_FAILED,
210
+ `Failed to probe ${sniffed.toUpperCase()} file. mediabunny: ${mbMsg}. libav: ${lvMsg}.`,
211
+ "The file may be corrupt, truncated, or in an unsupported format. Enable AVBRIDGE_DEBUG for detailed logs."
200
212
  );
201
213
  }
202
214
  }
203
215
  }
204
216
  try {
205
- const { probeWithLibav } = await import('./avi-GCGM7OJI.js');
217
+ const { probeWithLibav } = await import('./avi-NJXAXUXK.js');
206
218
  return await probeWithLibav(normalized, sniffed);
207
219
  } catch (err) {
208
220
  const inner = err instanceof Error ? err.message : String(err);
209
221
  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)"}`
222
+ if (sniffed === "unknown") {
223
+ throw new AvbridgeError(
224
+ ERR_PROBE_UNKNOWN_CONTAINER,
225
+ `Unable to probe source: container format could not be identified. libav fallback: ${inner || "(no details)"}`,
226
+ "The file may be corrupt or in a format avbridge doesn't recognize. Check the file plays in VLC or ffprobe."
227
+ );
228
+ }
229
+ throw new AvbridgeError(
230
+ ERR_LIBAV_NOT_REACHABLE,
231
+ `${sniffed.toUpperCase()} files require libav.js, which failed to load: ${inner || "(no details)"}`,
232
+ "Install @libav.js/variant-webcodecs, or check that AVBRIDGE_LIBAV_BASE points to the correct path."
212
233
  );
213
234
  }
214
235
  }
@@ -309,7 +330,9 @@ var FALLBACK_AUDIO_CODECS = /* @__PURE__ */ new Set([
309
330
  "ra_144",
310
331
  "ra_288",
311
332
  "sipr",
312
- "atrac3"
333
+ "atrac3",
334
+ "dts",
335
+ "truehd"
313
336
  ]);
314
337
  var NATIVE_CONTAINERS = /* @__PURE__ */ new Set([
315
338
  "mp4",
@@ -371,7 +394,16 @@ function classifyContext(ctx) {
371
394
  reason: `video codec "${video.codec}" has no browser decoder; WASM fallback required`
372
395
  };
373
396
  }
374
- if (audio && FALLBACK_AUDIO_CODECS.has(audio.codec)) {
397
+ const audioNeedsFallback = audio && (FALLBACK_AUDIO_CODECS.has(audio.codec) || !NATIVE_AUDIO_CODECS.has(audio.codec));
398
+ if (audioNeedsFallback) {
399
+ if (NATIVE_VIDEO_CODECS.has(video.codec) && webCodecsAvailable()) {
400
+ return {
401
+ class: "HYBRID_CANDIDATE",
402
+ strategy: "hybrid",
403
+ reason: `video "${video.codec}" is hardware-decodable via WebCodecs; audio "${audio.codec}" decoded in software by libav`,
404
+ fallbackChain: ["fallback"]
405
+ };
406
+ }
375
407
  return {
376
408
  class: "FALLBACK_REQUIRED",
377
409
  strategy: "fallback",
@@ -738,10 +770,18 @@ var MseSink = class {
738
770
  constructor(options) {
739
771
  this.options = options;
740
772
  if (typeof MediaSource === "undefined") {
741
- throw new Error("MSE not supported in this environment");
773
+ throw new AvbridgeError(
774
+ ERR_MSE_NOT_SUPPORTED,
775
+ "MediaSource Extensions (MSE) are not supported in this environment.",
776
+ "MSE is required for the remux strategy. Use a browser that supports MSE, or try the fallback strategy."
777
+ );
742
778
  }
743
779
  if (!MediaSource.isTypeSupported(options.mime)) {
744
- throw new Error(`MSE does not support MIME "${options.mime}" \u2014 cannot remux`);
780
+ throw new AvbridgeError(
781
+ ERR_MSE_CODEC_NOT_SUPPORTED,
782
+ `This browser's MSE does not support "${options.mime}".`,
783
+ "The codec combination can't be played via remux in this browser. The player will try the next strategy automatically."
784
+ );
745
785
  }
746
786
  this.mediaSource = new MediaSource();
747
787
  this.objectUrl = URL.createObjectURL(this.mediaSource);
@@ -1070,6 +1110,10 @@ async function createRemuxPipeline(ctx, video) {
1070
1110
  console.error("[avbridge] remux pipeline reseek failed:", err);
1071
1111
  });
1072
1112
  },
1113
+ setAutoPlay(autoPlay) {
1114
+ pendingAutoPlay = autoPlay;
1115
+ if (sink) sink.setPlayOnSeek(autoPlay);
1116
+ },
1073
1117
  async destroy() {
1074
1118
  destroyed = true;
1075
1119
  pumpToken++;
@@ -1110,7 +1154,11 @@ async function createRemuxSession(context, video) {
1110
1154
  await pipeline.start(video.currentTime || 0, true);
1111
1155
  return;
1112
1156
  }
1113
- await video.play();
1157
+ pipeline.setAutoPlay(true);
1158
+ try {
1159
+ await video.play();
1160
+ } catch {
1161
+ }
1114
1162
  },
1115
1163
  pause() {
1116
1164
  wantPlay = false;
@@ -1149,6 +1197,10 @@ async function createRemuxSession(context, video) {
1149
1197
  }
1150
1198
 
1151
1199
  // src/strategies/fallback/video-renderer.ts
1200
+ function isDebug() {
1201
+ return typeof globalThis !== "undefined" && !!globalThis.AVBRIDGE_DEBUG;
1202
+ }
1203
+ var lastDebugLog = 0;
1152
1204
  var VideoRenderer = class {
1153
1205
  constructor(target, clock, fps = 30) {
1154
1206
  this.target = target;
@@ -1158,7 +1210,7 @@ var VideoRenderer = class {
1158
1210
  this.resolveFirstFrame = resolve;
1159
1211
  });
1160
1212
  this.canvas = document.createElement("canvas");
1161
- this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;";
1213
+ this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;object-fit:contain;";
1162
1214
  const parent = target.parentElement ?? target.parentNode;
1163
1215
  if (parent && parent instanceof HTMLElement) {
1164
1216
  if (getComputedStyle(parent).position === "static") {
@@ -1195,6 +1247,20 @@ var VideoRenderer = class {
1195
1247
  lastPaintWall = 0;
1196
1248
  /** Minimum ms between paints — paces video at roughly source fps. */
1197
1249
  paintIntervalMs;
1250
+ /** Cumulative count of frames skipped because all PTS are in the future. */
1251
+ ticksWaiting = 0;
1252
+ /** Cumulative count of ticks where PTS mode painted a frame. */
1253
+ ticksPainted = 0;
1254
+ /**
1255
+ * Calibration offset (microseconds) between video PTS and audio clock.
1256
+ * Video PTS and AudioContext.currentTime can drift ~0.1% relative to
1257
+ * each other (different clock domains). Over 45 minutes that's 2.6s.
1258
+ * We measure the offset on the first painted frame and update it
1259
+ * periodically so the PTS comparison stays calibrated.
1260
+ */
1261
+ ptsCalibrationUs = 0;
1262
+ ptsCalibrated = false;
1263
+ lastCalibrationWall = 0;
1198
1264
  /** Resolves once the first decoded frame has been enqueued. */
1199
1265
  firstFrameReady;
1200
1266
  resolveFirstFrame;
@@ -1243,21 +1309,81 @@ var VideoRenderer = class {
1243
1309
  }
1244
1310
  return;
1245
1311
  }
1246
- const wallNow = performance.now();
1247
- if (wallNow - this.lastPaintWall < this.paintIntervalMs - 2) return;
1248
- if (this.queue.length === 0) return;
1249
- if (this.framesPainted > 0 && this.framesPainted % 30 === 0) {
1250
- const audioNowUs = this.clock.now() * 1e6;
1251
- const headTs = this.queue[0].timestamp ?? 0;
1252
- const driftUs = headTs - audioNowUs;
1253
- if (driftUs < -15e4) {
1254
- this.queue.shift()?.close();
1255
- this.framesDroppedLate++;
1256
- if (this.queue.length === 0) return;
1257
- } else if (driftUs > 15e4) {
1312
+ const rawAudioNowUs = this.clock.now() * 1e6;
1313
+ const headTs = this.queue[0].timestamp ?? 0;
1314
+ const hasPts = headTs > 0 || this.queue.length > 1;
1315
+ if (hasPts) {
1316
+ const wallNow2 = performance.now();
1317
+ if (!this.ptsCalibrated || wallNow2 - this.lastCalibrationWall > 1e4) {
1318
+ this.ptsCalibrationUs = headTs - rawAudioNowUs;
1319
+ this.ptsCalibrated = true;
1320
+ this.lastCalibrationWall = wallNow2;
1321
+ }
1322
+ const audioNowUs = rawAudioNowUs + this.ptsCalibrationUs;
1323
+ const frameDurationUs = this.paintIntervalMs * 1e3;
1324
+ const deadlineUs = audioNowUs + frameDurationUs;
1325
+ let bestIdx = -1;
1326
+ for (let i = 0; i < this.queue.length; i++) {
1327
+ const ts = this.queue[i].timestamp ?? 0;
1328
+ if (ts <= deadlineUs) {
1329
+ bestIdx = i;
1330
+ } else {
1331
+ break;
1332
+ }
1333
+ }
1334
+ if (bestIdx < 0) {
1335
+ this.ticksWaiting++;
1336
+ if (isDebug()) {
1337
+ const now = performance.now();
1338
+ if (now - lastDebugLog > 1e3) {
1339
+ const headPtsMs = (headTs / 1e3).toFixed(1);
1340
+ const audioMs = (audioNowUs / 1e3).toFixed(1);
1341
+ const rawDriftMs = ((headTs - rawAudioNowUs) / 1e3).toFixed(1);
1342
+ const calibMs = (this.ptsCalibrationUs / 1e3).toFixed(1);
1343
+ console.log(
1344
+ `[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}`
1345
+ );
1346
+ lastDebugLog = now;
1347
+ }
1348
+ }
1258
1349
  return;
1259
1350
  }
1351
+ const dropThresholdUs = audioNowUs - frameDurationUs * 2;
1352
+ let dropped = 0;
1353
+ while (bestIdx > 0) {
1354
+ const ts = this.queue[0].timestamp ?? 0;
1355
+ if (ts < dropThresholdUs) {
1356
+ this.queue.shift()?.close();
1357
+ this.framesDroppedLate++;
1358
+ bestIdx--;
1359
+ dropped++;
1360
+ } else {
1361
+ break;
1362
+ }
1363
+ }
1364
+ this.ticksPainted++;
1365
+ if (isDebug()) {
1366
+ const now = performance.now();
1367
+ if (now - lastDebugLog > 1e3) {
1368
+ const paintedTs = this.queue[0]?.timestamp ?? 0;
1369
+ const audioMs = (audioNowUs / 1e3).toFixed(1);
1370
+ const ptsMs = (paintedTs / 1e3).toFixed(1);
1371
+ const rawDriftMs = ((paintedTs - rawAudioNowUs) / 1e3).toFixed(1);
1372
+ const calibMs = (this.ptsCalibrationUs / 1e3).toFixed(1);
1373
+ console.log(
1374
+ `[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}`
1375
+ );
1376
+ lastDebugLog = now;
1377
+ }
1378
+ }
1379
+ const frame2 = this.queue.shift();
1380
+ this.paint(frame2);
1381
+ frame2.close();
1382
+ this.lastPaintWall = performance.now();
1383
+ return;
1260
1384
  }
1385
+ const wallNow = performance.now();
1386
+ if (wallNow - this.lastPaintWall < this.paintIntervalMs - 2) return;
1261
1387
  const frame = this.queue.shift();
1262
1388
  this.paint(frame);
1263
1389
  frame.close();
@@ -1279,8 +1405,13 @@ var VideoRenderer = class {
1279
1405
  }
1280
1406
  /** Discard all queued frames. Used by seek to drop stale buffers. */
1281
1407
  flush() {
1408
+ const count = this.queue.length;
1282
1409
  while (this.queue.length > 0) this.queue.shift()?.close();
1283
1410
  this.prerolled = false;
1411
+ this.ptsCalibrated = false;
1412
+ if (isDebug() && count > 0) {
1413
+ console.log(`[avbridge:renderer] FLUSH discarded=${count} painted=${this.framesPainted} drops=${this.framesDroppedLate}`);
1414
+ }
1284
1415
  }
1285
1416
  stats() {
1286
1417
  return {
@@ -1325,11 +1456,38 @@ var AudioOutput = class {
1325
1456
  pendingQueue = [];
1326
1457
  framesScheduled = 0;
1327
1458
  destroyed = false;
1459
+ /** User-set volume (0..1). Applied to the gain node. */
1460
+ _volume = 1;
1461
+ /** User-set muted flag. When true, gain is forced to 0. */
1462
+ _muted = false;
1328
1463
  constructor() {
1329
1464
  this.ctx = new AudioContext();
1330
1465
  this.gain = this.ctx.createGain();
1331
1466
  this.gain.connect(this.ctx.destination);
1332
1467
  }
1468
+ /** Set volume (0..1). Applied immediately to the gain node. */
1469
+ setVolume(v) {
1470
+ this._volume = Math.max(0, Math.min(1, v));
1471
+ this.applyGain();
1472
+ }
1473
+ getVolume() {
1474
+ return this._volume;
1475
+ }
1476
+ /** Set muted. When true, output is silenced regardless of volume. */
1477
+ setMuted(m) {
1478
+ this._muted = m;
1479
+ this.applyGain();
1480
+ }
1481
+ getMuted() {
1482
+ return this._muted;
1483
+ }
1484
+ applyGain() {
1485
+ const target = this._muted ? 0 : this._volume;
1486
+ try {
1487
+ this.gain.gain.value = target;
1488
+ } catch {
1489
+ }
1490
+ }
1333
1491
  /**
1334
1492
  * Switch into wall-clock fallback mode. Called by the decoder when no
1335
1493
  * audio decoder could be initialized for the source. Once set, this
@@ -1400,9 +1558,13 @@ var AudioOutput = class {
1400
1558
  const node = this.ctx.createBufferSource();
1401
1559
  node.buffer = buffer;
1402
1560
  node.connect(this.gain);
1403
- const ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor);
1404
- const safeStart = Math.max(ctxStart, this.ctx.currentTime);
1405
- node.start(safeStart);
1561
+ let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor);
1562
+ if (ctxStart < this.ctx.currentTime) {
1563
+ this.ctxTimeAtAnchor = this.ctx.currentTime;
1564
+ this.mediaTimeOfAnchor = this.mediaTimeOfNext;
1565
+ ctxStart = this.ctx.currentTime;
1566
+ }
1567
+ node.start(ctxStart);
1406
1568
  this.mediaTimeOfNext += frameCount / sampleRate;
1407
1569
  this.framesScheduled++;
1408
1570
  }
@@ -1475,6 +1637,7 @@ var AudioOutput = class {
1475
1637
  }
1476
1638
  this.gain = this.ctx.createGain();
1477
1639
  this.gain.connect(this.ctx.destination);
1640
+ this.applyGain();
1478
1641
  this.pendingQueue = [];
1479
1642
  this.mediaTimeOfAnchor = newMediaTime;
1480
1643
  this.mediaTimeOfNext = newMediaTime;
@@ -1506,8 +1669,8 @@ async function startHybridDecoder(opts) {
1506
1669
  const variant = pickLibavVariant(opts.context);
1507
1670
  const libav = await loadLibav(variant);
1508
1671
  const bridge = await loadBridge();
1509
- const { prepareLibavInput } = await import('./libav-http-reader-NQJVY273.js');
1510
- const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source);
1672
+ const { prepareLibavInput } = await import('./libav-http-reader-WXG3Z7AI.js');
1673
+ const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source, opts.transport);
1511
1674
  const readPkt = await libav.av_packet_alloc();
1512
1675
  const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
1513
1676
  const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
@@ -1578,6 +1741,56 @@ async function startHybridDecoder(opts) {
1578
1741
  });
1579
1742
  throw new Error("hybrid decoder: could not initialize any decoders");
1580
1743
  }
1744
+ let bsfCtx = null;
1745
+ let bsfPkt = null;
1746
+ if (videoStream && opts.context.videoTracks[0]?.codec === "mpeg4") {
1747
+ try {
1748
+ bsfCtx = await libav.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
1749
+ if (bsfCtx != null && bsfCtx >= 0) {
1750
+ const parIn = await libav.AVBSFContext_par_in(bsfCtx);
1751
+ await libav.avcodec_parameters_copy(parIn, videoStream.codecpar);
1752
+ await libav.av_bsf_init(bsfCtx);
1753
+ bsfPkt = await libav.av_packet_alloc();
1754
+ dbg.info("bsf", "mpeg4_unpack_bframes BSF active (hybrid)");
1755
+ } else {
1756
+ console.warn("[avbridge] mpeg4_unpack_bframes BSF not available in hybrid decoder");
1757
+ bsfCtx = null;
1758
+ }
1759
+ } catch (err) {
1760
+ console.warn("[avbridge] hybrid: failed to init BSF:", err.message);
1761
+ bsfCtx = null;
1762
+ bsfPkt = null;
1763
+ }
1764
+ }
1765
+ async function applyBSF(packets) {
1766
+ if (!bsfCtx || !bsfPkt) return packets;
1767
+ const out = [];
1768
+ for (const pkt of packets) {
1769
+ await libav.ff_copyin_packet(bsfPkt, pkt);
1770
+ const sendErr = await libav.av_bsf_send_packet(bsfCtx, bsfPkt);
1771
+ if (sendErr < 0) {
1772
+ out.push(pkt);
1773
+ continue;
1774
+ }
1775
+ while (true) {
1776
+ const recvErr = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
1777
+ if (recvErr < 0) break;
1778
+ out.push(await libav.ff_copyout_packet(bsfPkt));
1779
+ }
1780
+ }
1781
+ return out;
1782
+ }
1783
+ async function flushBSF() {
1784
+ if (!bsfCtx || !bsfPkt) return;
1785
+ try {
1786
+ await libav.av_bsf_send_packet(bsfCtx, 0);
1787
+ while (true) {
1788
+ const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
1789
+ if (err < 0) break;
1790
+ }
1791
+ } catch {
1792
+ }
1793
+ }
1581
1794
  let destroyed = false;
1582
1795
  let pumpToken = 0;
1583
1796
  let pumpRunning = null;
@@ -1605,8 +1818,15 @@ async function startHybridDecoder(opts) {
1605
1818
  if (myToken !== pumpToken || destroyed) return;
1606
1819
  const videoPackets = videoStream ? packets[videoStream.index] : void 0;
1607
1820
  const audioPackets = audioStream ? packets[audioStream.index] : void 0;
1821
+ if (audioDec && audioPackets && audioPackets.length > 0) {
1822
+ await decodeAudioBatch(audioPackets, myToken);
1823
+ }
1824
+ if (myToken !== pumpToken || destroyed) return;
1825
+ await new Promise((r) => setTimeout(r, 0));
1826
+ if (myToken !== pumpToken || destroyed) return;
1608
1827
  if (videoDecoder && videoPackets && videoPackets.length > 0) {
1609
- for (const pkt of videoPackets) {
1828
+ const processed = await applyBSF(videoPackets);
1829
+ for (const pkt of processed) {
1610
1830
  if (myToken !== pumpToken || destroyed) return;
1611
1831
  sanitizePacketTimestamp(pkt, () => {
1612
1832
  const ts = syntheticVideoUs;
@@ -1626,9 +1846,6 @@ async function startHybridDecoder(opts) {
1626
1846
  }
1627
1847
  }
1628
1848
  }
1629
- if (audioDec && audioPackets && audioPackets.length > 0) {
1630
- await decodeAudioBatch(audioPackets, myToken);
1631
- }
1632
1849
  packetsRead += (videoPackets?.length ?? 0) + (audioPackets?.length ?? 0);
1633
1850
  while (!destroyed && myToken === pumpToken && (videoDecoder && videoDecoder.decodeQueueSize > 10 || opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
1634
1851
  await new Promise((r) => setTimeout(r, 50));
@@ -1651,20 +1868,43 @@ async function startHybridDecoder(opts) {
1651
1868
  }
1652
1869
  async function decodeAudioBatch(pkts, myToken, flush = false) {
1653
1870
  if (!audioDec || destroyed || myToken !== pumpToken) return;
1654
- let frames;
1655
- try {
1656
- frames = await libav.ff_decode_multi(
1657
- audioDec.c,
1658
- audioDec.pkt,
1659
- audioDec.frame,
1660
- pkts,
1661
- flush ? { fin: true, ignoreErrors: true } : { ignoreErrors: true }
1662
- );
1663
- } catch (err) {
1664
- console.error("[avbridge] hybrid audio decode failed:", err);
1665
- return;
1871
+ const AUDIO_SUB_BATCH = 4;
1872
+ let allFrames = [];
1873
+ for (let i = 0; i < pkts.length; i += AUDIO_SUB_BATCH) {
1874
+ if (myToken !== pumpToken || destroyed) return;
1875
+ const slice = pkts.slice(i, i + AUDIO_SUB_BATCH);
1876
+ const isLast = i + AUDIO_SUB_BATCH >= pkts.length;
1877
+ try {
1878
+ const frames2 = await libav.ff_decode_multi(
1879
+ audioDec.c,
1880
+ audioDec.pkt,
1881
+ audioDec.frame,
1882
+ slice,
1883
+ isLast && flush ? { fin: true, ignoreErrors: true } : { ignoreErrors: true }
1884
+ );
1885
+ allFrames = allFrames.concat(frames2);
1886
+ } catch (err) {
1887
+ console.error("[avbridge] hybrid audio decode failed:", err);
1888
+ return;
1889
+ }
1890
+ if (!isLast) await new Promise((r) => setTimeout(r, 0));
1891
+ }
1892
+ if (pkts.length === 0 && flush) {
1893
+ try {
1894
+ allFrames = await libav.ff_decode_multi(
1895
+ audioDec.c,
1896
+ audioDec.pkt,
1897
+ audioDec.frame,
1898
+ [],
1899
+ { fin: true, ignoreErrors: true }
1900
+ );
1901
+ } catch (err) {
1902
+ console.error("[avbridge] hybrid audio flush failed:", err);
1903
+ return;
1904
+ }
1666
1905
  }
1667
1906
  if (myToken !== pumpToken || destroyed) return;
1907
+ const frames = allFrames;
1668
1908
  for (const f of frames) {
1669
1909
  if (myToken !== pumpToken || destroyed) return;
1670
1910
  sanitizeFrameTimestamp(
@@ -1701,6 +1941,14 @@ async function startHybridDecoder(opts) {
1701
1941
  await pumpRunning;
1702
1942
  } catch {
1703
1943
  }
1944
+ try {
1945
+ if (bsfCtx) await libav.av_bsf_free(bsfCtx);
1946
+ } catch {
1947
+ }
1948
+ try {
1949
+ if (bsfPkt) await libav.av_packet_free?.(bsfPkt);
1950
+ } catch {
1951
+ }
1704
1952
  try {
1705
1953
  if (videoDecoder && videoDecoder.state !== "closed") videoDecoder.close();
1706
1954
  } catch {
@@ -1754,6 +2002,7 @@ async function startHybridDecoder(opts) {
1754
2002
  if (audioDec) await libav.avcodec_flush_buffers?.(audioDec.c);
1755
2003
  } catch {
1756
2004
  }
2005
+ await flushBSF();
1757
2006
  syntheticVideoUs = Math.round(timeSec * 1e6);
1758
2007
  syntheticAudioUs = Math.round(timeSec * 1e6);
1759
2008
  pumpRunning = pumpLoop(newToken).catch(
@@ -1767,6 +2016,7 @@ async function startHybridDecoder(opts) {
1767
2016
  videoFramesDecoded,
1768
2017
  videoChunksFed,
1769
2018
  audioFramesDecoded,
2019
+ bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
1770
2020
  videoDecodeQueueSize: videoDecoder?.decodeQueueSize ?? 0,
1771
2021
  // Confirmed transport info — see fallback decoder for the pattern.
1772
2022
  _transport: inputHandle.transport === "http-range" ? "http-range" : "memory",
@@ -1943,8 +2193,8 @@ async function loadBridge() {
1943
2193
  // src/strategies/hybrid/index.ts
1944
2194
  var READY_AUDIO_BUFFER_SECONDS = 0.3;
1945
2195
  var READY_TIMEOUT_SECONDS = 10;
1946
- async function createHybridSession(ctx, target) {
1947
- const { normalizeSource: normalizeSource2 } = await import('./source-FFZ7TW2B.js');
2196
+ async function createHybridSession(ctx, target, transport) {
2197
+ const { normalizeSource: normalizeSource2 } = await import('./source-QJR3OHTW.js');
1948
2198
  const source = await normalizeSource2(ctx.source);
1949
2199
  const fps = ctx.videoTracks[0]?.fps ?? 30;
1950
2200
  const audio = new AudioOutput();
@@ -1956,7 +2206,8 @@ async function createHybridSession(ctx, target) {
1956
2206
  filename: ctx.name ?? "input.bin",
1957
2207
  context: ctx,
1958
2208
  renderer,
1959
- audio
2209
+ audio,
2210
+ transport
1960
2211
  });
1961
2212
  } catch (err) {
1962
2213
  audio.destroy();
@@ -1970,6 +2221,26 @@ async function createHybridSession(ctx, target) {
1970
2221
  void doSeek(v);
1971
2222
  }
1972
2223
  });
2224
+ Object.defineProperty(target, "paused", {
2225
+ configurable: true,
2226
+ get: () => !audio.isPlaying()
2227
+ });
2228
+ Object.defineProperty(target, "volume", {
2229
+ configurable: true,
2230
+ get: () => audio.getVolume(),
2231
+ set: (v) => {
2232
+ audio.setVolume(v);
2233
+ target.dispatchEvent(new Event("volumechange"));
2234
+ }
2235
+ });
2236
+ Object.defineProperty(target, "muted", {
2237
+ configurable: true,
2238
+ get: () => audio.getMuted(),
2239
+ set: (m) => {
2240
+ audio.setMuted(m);
2241
+ target.dispatchEvent(new Event("volumechange"));
2242
+ }
2243
+ });
1973
2244
  if (ctx.duration && Number.isFinite(ctx.duration)) {
1974
2245
  Object.defineProperty(target, "duration", {
1975
2246
  configurable: true,
@@ -2009,10 +2280,13 @@ async function createHybridSession(ctx, target) {
2009
2280
  if (!audio.isPlaying()) {
2010
2281
  await waitForBuffer();
2011
2282
  await audio.start();
2283
+ target.dispatchEvent(new Event("play"));
2284
+ target.dispatchEvent(new Event("playing"));
2012
2285
  }
2013
2286
  },
2014
2287
  pause() {
2015
2288
  void audio.pause();
2289
+ target.dispatchEvent(new Event("pause"));
2016
2290
  },
2017
2291
  async seek(time) {
2018
2292
  await doSeek(time);
@@ -2034,6 +2308,9 @@ async function createHybridSession(ctx, target) {
2034
2308
  try {
2035
2309
  delete target.currentTime;
2036
2310
  delete target.duration;
2311
+ delete target.paused;
2312
+ delete target.volume;
2313
+ delete target.muted;
2037
2314
  } catch {
2038
2315
  }
2039
2316
  },
@@ -2048,8 +2325,8 @@ async function startDecoder(opts) {
2048
2325
  const variant = pickLibavVariant(opts.context);
2049
2326
  const libav = await loadLibav(variant);
2050
2327
  const bridge = await loadBridge2();
2051
- const { prepareLibavInput } = await import('./libav-http-reader-NQJVY273.js');
2052
- const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source);
2328
+ const { prepareLibavInput } = await import('./libav-http-reader-WXG3Z7AI.js');
2329
+ const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source, opts.transport);
2053
2330
  const readPkt = await libav.av_packet_alloc();
2054
2331
  const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
2055
2332
  const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
@@ -2105,6 +2382,56 @@ async function startDecoder(opts) {
2105
2382
  `fallback decoder: could not initialize any libav decoders (${codecs}).${hint}`
2106
2383
  );
2107
2384
  }
2385
+ let bsfCtx = null;
2386
+ let bsfPkt = null;
2387
+ if (videoStream && opts.context.videoTracks[0]?.codec === "mpeg4") {
2388
+ try {
2389
+ bsfCtx = await libav.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
2390
+ if (bsfCtx != null && bsfCtx >= 0) {
2391
+ const parIn = await libav.AVBSFContext_par_in(bsfCtx);
2392
+ await libav.avcodec_parameters_copy(parIn, videoStream.codecpar);
2393
+ await libav.av_bsf_init(bsfCtx);
2394
+ bsfPkt = await libav.av_packet_alloc();
2395
+ dbg.info("bsf", "mpeg4_unpack_bframes BSF active");
2396
+ } else {
2397
+ console.warn("[avbridge] mpeg4_unpack_bframes BSF not available \u2014 decoding without it");
2398
+ bsfCtx = null;
2399
+ }
2400
+ } catch (err) {
2401
+ console.warn("[avbridge] failed to init mpeg4_unpack_bframes BSF:", err.message);
2402
+ bsfCtx = null;
2403
+ bsfPkt = null;
2404
+ }
2405
+ }
2406
+ async function applyBSF(packets) {
2407
+ if (!bsfCtx || !bsfPkt) return packets;
2408
+ const out = [];
2409
+ for (const pkt of packets) {
2410
+ await libav.ff_copyin_packet(bsfPkt, pkt);
2411
+ const sendErr = await libav.av_bsf_send_packet(bsfCtx, bsfPkt);
2412
+ if (sendErr < 0) {
2413
+ out.push(pkt);
2414
+ continue;
2415
+ }
2416
+ while (true) {
2417
+ const recvErr = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
2418
+ if (recvErr < 0) break;
2419
+ out.push(await libav.ff_copyout_packet(bsfPkt));
2420
+ }
2421
+ }
2422
+ return out;
2423
+ }
2424
+ async function flushBSF() {
2425
+ if (!bsfCtx || !bsfPkt) return;
2426
+ try {
2427
+ await libav.av_bsf_send_packet(bsfCtx, 0);
2428
+ while (true) {
2429
+ const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
2430
+ if (err < 0) break;
2431
+ }
2432
+ } catch {
2433
+ }
2434
+ }
2108
2435
  let destroyed = false;
2109
2436
  let pumpToken = 0;
2110
2437
  let pumpRunning = null;
@@ -2113,7 +2440,8 @@ async function startDecoder(opts) {
2113
2440
  let audioFramesDecoded = 0;
2114
2441
  let watchdogFirstFrameMs = 0;
2115
2442
  let watchdogSlowSinceMs = 0;
2116
- let watchdogWarned = false;
2443
+ let watchdogSlowWarned = false;
2444
+ let watchdogOverflowWarned = false;
2117
2445
  let syntheticVideoUs = 0;
2118
2446
  let syntheticAudioUs = 0;
2119
2447
  const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
@@ -2125,7 +2453,7 @@ async function startDecoder(opts) {
2125
2453
  let packets;
2126
2454
  try {
2127
2455
  [readErr, packets] = await libav.ff_read_frame_multi(fmt_ctx, readPkt, {
2128
- limit: 64 * 1024
2456
+ limit: 16 * 1024
2129
2457
  });
2130
2458
  } catch (err) {
2131
2459
  console.error("[avbridge] ff_read_frame_multi failed:", err);
@@ -2134,26 +2462,27 @@ async function startDecoder(opts) {
2134
2462
  if (myToken !== pumpToken || destroyed) return;
2135
2463
  const videoPackets = videoStream ? packets[videoStream.index] : void 0;
2136
2464
  const audioPackets = audioStream ? packets[audioStream.index] : void 0;
2137
- if (videoDec && videoPackets && videoPackets.length > 0) {
2138
- await decodeVideoBatch(videoPackets, myToken);
2139
- }
2140
- if (myToken !== pumpToken || destroyed) return;
2141
2465
  if (audioDec && audioPackets && audioPackets.length > 0) {
2142
2466
  await decodeAudioBatch(audioPackets, myToken);
2143
2467
  }
2468
+ if (myToken !== pumpToken || destroyed) return;
2469
+ if (videoDec && videoPackets && videoPackets.length > 0) {
2470
+ const processed = await applyBSF(videoPackets);
2471
+ await decodeVideoBatch(processed, myToken);
2472
+ }
2144
2473
  packetsRead += (videoPackets?.length ?? 0) + (audioPackets?.length ?? 0);
2145
2474
  if (videoFramesDecoded > 0) {
2146
2475
  if (watchdogFirstFrameMs === 0) {
2147
2476
  watchdogFirstFrameMs = performance.now();
2148
2477
  }
2149
2478
  const elapsedSinceFirst = (performance.now() - watchdogFirstFrameMs) / 1e3;
2150
- if (elapsedSinceFirst > 1 && !watchdogWarned) {
2479
+ if (elapsedSinceFirst > 1 && !watchdogSlowWarned) {
2151
2480
  const expectedFrames = elapsedSinceFirst * videoFps;
2152
2481
  const ratio = videoFramesDecoded / expectedFrames;
2153
2482
  if (ratio < 0.6) {
2154
2483
  if (watchdogSlowSinceMs === 0) watchdogSlowSinceMs = performance.now();
2155
2484
  if ((performance.now() - watchdogSlowSinceMs) / 1e3 > 5) {
2156
- watchdogWarned = true;
2485
+ watchdogSlowWarned = true;
2157
2486
  console.warn(
2158
2487
  "[avbridge:decode-rate]",
2159
2488
  `decoder is running slower than realtime: ${videoFramesDecoded} frames in ${elapsedSinceFirst.toFixed(1)}s (${(videoFramesDecoded / elapsedSinceFirst).toFixed(1)} fps vs ${videoFps} fps source \u2014 ${(ratio * 100).toFixed(0)}% of realtime). Playback will stutter. Typical causes: software decode of a codec with no WebCodecs support (rv40, mpeg4 @ 720p+, wmv3), or a resolution too large for single-threaded WASM to keep up with.`
@@ -2163,6 +2492,17 @@ async function startDecoder(opts) {
2163
2492
  watchdogSlowSinceMs = 0;
2164
2493
  }
2165
2494
  }
2495
+ if (!watchdogOverflowWarned && videoFramesDecoded > 100) {
2496
+ const rendererStats = opts.renderer.stats();
2497
+ const overflow = rendererStats.framesDroppedOverflow ?? 0;
2498
+ if (overflow / videoFramesDecoded > 0.1) {
2499
+ watchdogOverflowWarned = true;
2500
+ console.warn(
2501
+ "[avbridge:overflow-drop]",
2502
+ `renderer is dropping ${overflow}/${videoFramesDecoded} frames (${(overflow / videoFramesDecoded * 100).toFixed(0)}%) because the decoder is producing bursts faster than the canvas can drain. Symptom: choppy playback despite decoder keeping up on average. Fix would be smaller read batches in the pump loop or a lower queueHighWater cap \u2014 see src/strategies/fallback/decoder.ts.`
2503
+ );
2504
+ }
2505
+ }
2166
2506
  }
2167
2507
  while (!destroyed && myToken === pumpToken && (opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
2168
2508
  await new Promise((r) => setTimeout(r, 50));
@@ -2274,6 +2614,14 @@ async function startDecoder(opts) {
2274
2614
  await pumpRunning;
2275
2615
  } catch {
2276
2616
  }
2617
+ try {
2618
+ if (bsfCtx) await libav.av_bsf_free(bsfCtx);
2619
+ } catch {
2620
+ }
2621
+ try {
2622
+ if (bsfPkt) await libav.av_packet_free?.(bsfPkt);
2623
+ } catch {
2624
+ }
2277
2625
  try {
2278
2626
  if (videoDec) await libav.ff_free_decoder?.(videoDec.c, videoDec.pkt, videoDec.frame);
2279
2627
  } catch {
@@ -2325,6 +2673,7 @@ async function startDecoder(opts) {
2325
2673
  if (audioDec) await libav.avcodec_flush_buffers?.(audioDec.c);
2326
2674
  } catch {
2327
2675
  }
2676
+ await flushBSF();
2328
2677
  syntheticVideoUs = Math.round(timeSec * 1e6);
2329
2678
  syntheticAudioUs = Math.round(timeSec * 1e6);
2330
2679
  pumpRunning = pumpLoop(newToken).catch(
@@ -2337,6 +2686,7 @@ async function startDecoder(opts) {
2337
2686
  packetsRead,
2338
2687
  videoFramesDecoded,
2339
2688
  audioFramesDecoded,
2689
+ bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
2340
2690
  // Confirmed transport info: once prepareLibavInput returns
2341
2691
  // successfully, we *know* whether the source is http-range (probe
2342
2692
  // succeeded and returned 206) or in-memory blob. Diagnostics hoists
@@ -2492,8 +2842,8 @@ async function loadBridge2() {
2492
2842
  // src/strategies/fallback/index.ts
2493
2843
  var READY_AUDIO_BUFFER_SECONDS2 = 0.04;
2494
2844
  var READY_TIMEOUT_SECONDS2 = 3;
2495
- async function createFallbackSession(ctx, target) {
2496
- const { normalizeSource: normalizeSource2 } = await import('./source-FFZ7TW2B.js');
2845
+ async function createFallbackSession(ctx, target, transport) {
2846
+ const { normalizeSource: normalizeSource2 } = await import('./source-QJR3OHTW.js');
2497
2847
  const source = await normalizeSource2(ctx.source);
2498
2848
  const fps = ctx.videoTracks[0]?.fps ?? 30;
2499
2849
  const audio = new AudioOutput();
@@ -2505,7 +2855,8 @@ async function createFallbackSession(ctx, target) {
2505
2855
  filename: ctx.name ?? "input.bin",
2506
2856
  context: ctx,
2507
2857
  renderer,
2508
- audio
2858
+ audio,
2859
+ transport
2509
2860
  });
2510
2861
  } catch (err) {
2511
2862
  audio.destroy();
@@ -2519,6 +2870,26 @@ async function createFallbackSession(ctx, target) {
2519
2870
  void doSeek(v);
2520
2871
  }
2521
2872
  });
2873
+ Object.defineProperty(target, "paused", {
2874
+ configurable: true,
2875
+ get: () => !audio.isPlaying()
2876
+ });
2877
+ Object.defineProperty(target, "volume", {
2878
+ configurable: true,
2879
+ get: () => audio.getVolume(),
2880
+ set: (v) => {
2881
+ audio.setVolume(v);
2882
+ target.dispatchEvent(new Event("volumechange"));
2883
+ }
2884
+ });
2885
+ Object.defineProperty(target, "muted", {
2886
+ configurable: true,
2887
+ get: () => audio.getMuted(),
2888
+ set: (m) => {
2889
+ audio.setMuted(m);
2890
+ target.dispatchEvent(new Event("volumechange"));
2891
+ }
2892
+ });
2522
2893
  if (ctx.duration && Number.isFinite(ctx.duration)) {
2523
2894
  Object.defineProperty(target, "duration", {
2524
2895
  configurable: true,
@@ -2527,25 +2898,35 @@ async function createFallbackSession(ctx, target) {
2527
2898
  }
2528
2899
  async function waitForBuffer() {
2529
2900
  const start = performance.now();
2901
+ let firstFrameAtMs = 0;
2530
2902
  dbg.info(
2531
2903
  "cold-start",
2532
- `gate entry: need audio >= ${READY_AUDIO_BUFFER_SECONDS2 * 1e3}ms + 1 frame`
2904
+ `gate entry: want audio \u2265 ${READY_AUDIO_BUFFER_SECONDS2 * 1e3}ms + 1 frame`
2533
2905
  );
2534
2906
  while (true) {
2535
2907
  const audioAhead = audio.isNoAudio() ? Infinity : audio.bufferAhead();
2536
2908
  const audioReady = audio.isNoAudio() || audioAhead >= READY_AUDIO_BUFFER_SECONDS2;
2537
2909
  const hasFrames = renderer.hasFrames();
2910
+ const nowMs = performance.now();
2911
+ if (hasFrames && firstFrameAtMs === 0) firstFrameAtMs = nowMs;
2538
2912
  if (audioReady && hasFrames) {
2539
2913
  dbg.info(
2540
2914
  "cold-start",
2541
- `gate satisfied in ${(performance.now() - start).toFixed(0)}ms (audio=${(audioAhead * 1e3).toFixed(0)}ms, frames=${renderer.queueDepth()})`
2915
+ `gate satisfied in ${(nowMs - start).toFixed(0)}ms (audio=${(audioAhead * 1e3).toFixed(0)}ms, frames=${renderer.queueDepth()})`
2916
+ );
2917
+ return;
2918
+ }
2919
+ if (hasFrames && firstFrameAtMs > 0 && nowMs - firstFrameAtMs >= 500) {
2920
+ dbg.info(
2921
+ "cold-start",
2922
+ `gate released on video-only grace at ${(nowMs - start).toFixed(0)}ms (frames=${renderer.queueDepth()}, audio=${(audioAhead * 1e3).toFixed(0)}ms \u2014 demuxer hasn't delivered audio packets yet, starting anyway and letting the audio scheduler catch up at its media-time anchor)`
2542
2923
  );
2543
2924
  return;
2544
2925
  }
2545
- if ((performance.now() - start) / 1e3 > READY_TIMEOUT_SECONDS2) {
2926
+ if ((nowMs - start) / 1e3 > READY_TIMEOUT_SECONDS2) {
2546
2927
  dbg.diag(
2547
2928
  "cold-start",
2548
- `gate TIMEOUT after ${READY_TIMEOUT_SECONDS2}s \u2014 audio=${(audioAhead * 1e3).toFixed(0)}ms (needed ${READY_AUDIO_BUFFER_SECONDS2 * 1e3}ms), frames=${renderer.queueDepth()} (needed \u22651). Software decoder is producing output slower than realtime \u2014 playback will stutter. Check getDiagnostics().runtime for the decode rate.`
2929
+ `gate TIMEOUT after ${READY_TIMEOUT_SECONDS2}s \u2014 audio=${(audioAhead * 1e3).toFixed(0)}ms (needed ${READY_AUDIO_BUFFER_SECONDS2 * 1e3}ms), frames=${renderer.queueDepth()} (needed \u22651). Decoder produced nothing in ${READY_TIMEOUT_SECONDS2}s \u2014 either a corrupt source, a missing codec, or WASM is catastrophically slow on this file. Check getDiagnostics().runtime for decode counters.`
2549
2930
  );
2550
2931
  return;
2551
2932
  }
@@ -2572,10 +2953,13 @@ async function createFallbackSession(ctx, target) {
2572
2953
  if (!audio.isPlaying()) {
2573
2954
  await waitForBuffer();
2574
2955
  await audio.start();
2956
+ target.dispatchEvent(new Event("play"));
2957
+ target.dispatchEvent(new Event("playing"));
2575
2958
  }
2576
2959
  },
2577
2960
  pause() {
2578
2961
  void audio.pause();
2962
+ target.dispatchEvent(new Event("pause"));
2579
2963
  },
2580
2964
  async seek(time) {
2581
2965
  await doSeek(time);
@@ -2594,6 +2978,9 @@ async function createFallbackSession(ctx, target) {
2594
2978
  try {
2595
2979
  delete target.currentTime;
2596
2980
  delete target.duration;
2981
+ delete target.paused;
2982
+ delete target.volume;
2983
+ delete target.muted;
2597
2984
  } catch {
2598
2985
  }
2599
2986
  },
@@ -2617,12 +3004,12 @@ var remuxPlugin = {
2617
3004
  var hybridPlugin = {
2618
3005
  name: "hybrid",
2619
3006
  canHandle: () => typeof VideoDecoder !== "undefined",
2620
- execute: (ctx, video) => createHybridSession(ctx, video)
3007
+ execute: (ctx, video, transport) => createHybridSession(ctx, video, transport)
2621
3008
  };
2622
3009
  var fallbackPlugin = {
2623
3010
  name: "fallback",
2624
3011
  canHandle: () => true,
2625
- execute: (ctx, video) => createFallbackSession(ctx, video)
3012
+ execute: (ctx, video, transport) => createFallbackSession(ctx, video, transport)
2626
3013
  };
2627
3014
  function registerBuiltins(registry) {
2628
3015
  registry.register(nativePlugin);
@@ -2678,7 +3065,8 @@ var SubtitleResourceBag = class {
2678
3065
  this.urls.clear();
2679
3066
  }
2680
3067
  };
2681
- async function attachSubtitleTracks(video, tracks, bag, onError) {
3068
+ async function attachSubtitleTracks(video, tracks, bag, onError, transport) {
3069
+ const doFetch = fetchWith(transport);
2682
3070
  for (const t of Array.from(video.querySelectorAll("track[data-avbridge]"))) {
2683
3071
  t.remove();
2684
3072
  }
@@ -2687,13 +3075,13 @@ async function attachSubtitleTracks(video, tracks, bag, onError) {
2687
3075
  try {
2688
3076
  let url = t.sidecarUrl;
2689
3077
  if (t.format === "srt") {
2690
- const res = await fetch(t.sidecarUrl);
3078
+ const res = await doFetch(t.sidecarUrl, transport?.requestInit);
2691
3079
  const text = await res.text();
2692
3080
  const vtt = srtToVtt(text);
2693
3081
  const blob = new Blob([vtt], { type: "text/vtt" });
2694
3082
  url = bag ? bag.createObjectURL(blob) : URL.createObjectURL(blob);
2695
3083
  } else if (t.format === "vtt") {
2696
- const res = await fetch(t.sidecarUrl);
3084
+ const res = await doFetch(t.sidecarUrl, transport?.requestInit);
2697
3085
  const text = await res.text();
2698
3086
  if (!isVtt(text)) {
2699
3087
  console.warn("[avbridge] subtitle missing WEBVTT header:", t.sidecarUrl);
@@ -2721,6 +3109,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
2721
3109
  constructor(options, registry) {
2722
3110
  this.options = options;
2723
3111
  this.registry = registry;
3112
+ const { requestInit, fetchFn } = options;
3113
+ if (requestInit || fetchFn) {
3114
+ this.transport = { requestInit, fetchFn };
3115
+ }
2724
3116
  }
2725
3117
  options;
2726
3118
  registry;
@@ -2736,11 +3128,27 @@ var UnifiedPlayer = class _UnifiedPlayer {
2736
3128
  lastProgressTime = 0;
2737
3129
  lastProgressPosition = -1;
2738
3130
  errorListener = null;
3131
+ // Bound so we can removeEventListener in destroy(); without this the
3132
+ // listener outlives the player and accumulates on elements that swap
3133
+ // source (e.g. <avbridge-video>).
3134
+ endedListener = null;
3135
+ // Background tab handling. userIntent is what the user last asked for
3136
+ // (play vs pause) — used to decide whether to auto-resume on visibility
3137
+ // return. autoPausedForVisibility tracks whether we paused because the
3138
+ // tab was hidden, so we don't resume playback the user deliberately
3139
+ // paused (e.g. via media keys while hidden).
3140
+ userIntent = "pause";
3141
+ autoPausedForVisibility = false;
3142
+ visibilityListener = null;
2739
3143
  // Serializes escalation / setStrategy calls
2740
3144
  switchingPromise = Promise.resolve();
2741
3145
  // Owns blob URLs created during sidecar discovery + SRT->VTT conversion.
2742
3146
  // Revoked at destroy() so repeated source swaps don't leak.
2743
3147
  subtitleResources = new SubtitleResourceBag();
3148
+ // Transport config extracted from CreatePlayerOptions. Threaded to probe,
3149
+ // subtitle fetches, and strategy session creators. Not stored on MediaContext
3150
+ // because it's runtime config, not media analysis.
3151
+ transport;
2744
3152
  static async create(options) {
2745
3153
  const registry = new PluginRegistry();
2746
3154
  registerBuiltins(registry);
@@ -2764,7 +3172,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
2764
3172
  const bootstrapStart = performance.now();
2765
3173
  try {
2766
3174
  dbg.info("bootstrap", "start");
2767
- const ctx = await dbg.timed("probe", "probe", 3e3, () => probe(this.options.source));
3175
+ const ctx = await dbg.timed("probe", "probe", 3e3, () => probe(this.options.source, this.transport));
2768
3176
  dbg.info(
2769
3177
  "probe",
2770
3178
  `container=${ctx.container} video=${ctx.videoTracks[0]?.codec ?? "-"} audio=${ctx.audioTracks[0]?.codec ?? "-"} probedBy=${ctx.probedBy}`
@@ -2812,7 +3220,8 @@ var UnifiedPlayer = class _UnifiedPlayer {
2812
3220
  this.subtitleResources,
2813
3221
  (err, track) => {
2814
3222
  console.warn(`[avbridge] subtitle ${track.id} failed: ${err.message}`);
2815
- }
3223
+ },
3224
+ this.transport
2816
3225
  );
2817
3226
  }
2818
3227
  this.emitter.emitSticky("tracks", {
@@ -2821,7 +3230,12 @@ var UnifiedPlayer = class _UnifiedPlayer {
2821
3230
  subtitle: ctx.subtitleTracks
2822
3231
  });
2823
3232
  this.startTimeupdateLoop();
2824
- this.options.target.addEventListener("ended", () => this.emitter.emit("ended", void 0));
3233
+ this.endedListener = () => this.emitter.emit("ended", void 0);
3234
+ this.options.target.addEventListener("ended", this.endedListener);
3235
+ if (this.options.backgroundBehavior !== "continue" && typeof document !== "undefined") {
3236
+ this.visibilityListener = () => this.onVisibilityChange();
3237
+ document.addEventListener("visibilitychange", this.visibilityListener);
3238
+ }
2825
3239
  this.emitter.emitSticky("ready", void 0);
2826
3240
  const bootstrapElapsed = performance.now() - bootstrapStart;
2827
3241
  dbg.info("bootstrap", `ready in ${bootstrapElapsed.toFixed(0)}ms`);
@@ -2848,7 +3262,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
2848
3262
  throw new Error(`no plugin available for strategy "${strategy}"`);
2849
3263
  }
2850
3264
  try {
2851
- this.session = await plugin.execute(this.mediaContext, this.options.target);
3265
+ this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
2852
3266
  } catch (err) {
2853
3267
  const chain = this.classification?.fallbackChain;
2854
3268
  if (chain && chain.length > 0) {
@@ -2921,7 +3335,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
2921
3335
  continue;
2922
3336
  }
2923
3337
  try {
2924
- this.session = await plugin.execute(this.mediaContext, this.options.target);
3338
+ this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
2925
3339
  } catch (err) {
2926
3340
  const msg = err instanceof Error ? err.message : String(err);
2927
3341
  errors.push(`${nextStrategy}: ${msg}`);
@@ -2944,8 +3358,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
2944
3358
  }
2945
3359
  return;
2946
3360
  }
2947
- this.emitter.emit("error", new Error(
2948
- `all fallback strategies failed: ${errors.join("; ")}`
3361
+ this.emitter.emit("error", new AvbridgeError(
3362
+ ERR_ALL_STRATEGIES_EXHAUSTED,
3363
+ `All playback strategies failed: ${errors.join("; ")}`,
3364
+ "This file may require a codec or container that isn't available in this browser. Try the fallback strategy or check browser codec support."
2949
3365
  ));
2950
3366
  }
2951
3367
  // ── Stall supervision ─────────────────────────────────────────────────
@@ -2997,7 +3413,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
2997
3413
  // ── Public: manual strategy switch ────────────────────────────────────
2998
3414
  /** Manually switch to a different playback strategy. Preserves current position and play/pause state. Concurrent calls are serialized. */
2999
3415
  async setStrategy(strategy, reason) {
3000
- if (!this.mediaContext) throw new Error("player not ready");
3416
+ 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.");
3001
3417
  if (this.session?.strategy === strategy) return;
3002
3418
  this.switchingPromise = this.switchingPromise.then(
3003
3419
  () => this.doSetStrategy(strategy, reason)
@@ -3026,7 +3442,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
3026
3442
  }
3027
3443
  const plugin = this.registry.findFor(this.mediaContext, strategy);
3028
3444
  if (!plugin) throw new Error(`no plugin available for strategy "${strategy}"`);
3029
- this.session = await plugin.execute(this.mediaContext, this.options.target);
3445
+ this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
3030
3446
  this.emitter.emitSticky("strategy", {
3031
3447
  strategy,
3032
3448
  reason: switchReason
@@ -3060,26 +3476,56 @@ var UnifiedPlayer = class _UnifiedPlayer {
3060
3476
  }
3061
3477
  /** Begin or resume playback. Throws if the player is not ready. */
3062
3478
  async play() {
3063
- if (!this.session) throw new Error("player not ready");
3479
+ 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.");
3480
+ this.userIntent = "play";
3481
+ this.autoPausedForVisibility = false;
3064
3482
  await this.session.play();
3065
3483
  }
3066
3484
  /** Pause playback. No-op if the player is not ready or already paused. */
3067
3485
  pause() {
3486
+ this.userIntent = "pause";
3487
+ this.autoPausedForVisibility = false;
3068
3488
  this.session?.pause();
3069
3489
  }
3490
+ /**
3491
+ * Handle browser tab visibility changes. On hide: pause if the user
3492
+ * had been playing. On show: resume if we were the one who paused.
3493
+ * Skips when `backgroundBehavior: "continue"` is set (listener isn't
3494
+ * installed in that case).
3495
+ */
3496
+ onVisibilityChange() {
3497
+ if (!this.session) return;
3498
+ const action = decideVisibilityAction({
3499
+ hidden: document.hidden,
3500
+ userIntent: this.userIntent,
3501
+ sessionIsPlaying: !this.options.target.paused,
3502
+ autoPausedForVisibility: this.autoPausedForVisibility
3503
+ });
3504
+ if (action === "pause") {
3505
+ this.autoPausedForVisibility = true;
3506
+ dbg.info("visibility", "tab hidden \u2014 auto-paused");
3507
+ this.session.pause();
3508
+ } else if (action === "resume") {
3509
+ this.autoPausedForVisibility = false;
3510
+ dbg.info("visibility", "tab visible \u2014 auto-resuming");
3511
+ void this.session.play().catch((err) => {
3512
+ console.warn("[avbridge] auto-resume after tab return failed:", err);
3513
+ });
3514
+ }
3515
+ }
3070
3516
  /** Seek to the given time in seconds. Throws if the player is not ready. */
3071
3517
  async seek(time) {
3072
- if (!this.session) throw new Error("player not ready");
3518
+ 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.");
3073
3519
  await this.session.seek(time);
3074
3520
  }
3075
3521
  /** Switch the active audio track by track ID. Throws if the player is not ready. */
3076
3522
  async setAudioTrack(id) {
3077
- if (!this.session) throw new Error("player not ready");
3523
+ 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.");
3078
3524
  await this.session.setAudioTrack(id);
3079
3525
  }
3080
3526
  /** Switch the active subtitle track by track ID, or pass `null` to disable subtitles. */
3081
3527
  async setSubtitleTrack(id) {
3082
- if (!this.session) throw new Error("player not ready");
3528
+ 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.");
3083
3529
  await this.session.setSubtitleTrack(id);
3084
3530
  }
3085
3531
  /** Return a snapshot of current diagnostics: container, codecs, strategy, runtime stats, and strategy history. */
@@ -3107,6 +3553,14 @@ var UnifiedPlayer = class _UnifiedPlayer {
3107
3553
  this.timeupdateInterval = null;
3108
3554
  }
3109
3555
  this.clearSupervisor();
3556
+ if (this.endedListener) {
3557
+ this.options.target.removeEventListener("ended", this.endedListener);
3558
+ this.endedListener = null;
3559
+ }
3560
+ if (this.visibilityListener) {
3561
+ document.removeEventListener("visibilitychange", this.visibilityListener);
3562
+ this.visibilityListener = null;
3563
+ }
3110
3564
  if (this.session) {
3111
3565
  await this.session.destroy();
3112
3566
  this.session = null;
@@ -3118,14 +3572,24 @@ var UnifiedPlayer = class _UnifiedPlayer {
3118
3572
  async function createPlayer(options) {
3119
3573
  return UnifiedPlayer.create(options);
3120
3574
  }
3575
+ function decideVisibilityAction(state) {
3576
+ if (state.hidden) {
3577
+ if (state.userIntent === "play" && state.sessionIsPlaying) return "pause";
3578
+ return "noop";
3579
+ }
3580
+ if (state.autoPausedForVisibility) return "resume";
3581
+ return "noop";
3582
+ }
3121
3583
  function buildInitialDecision(initial, ctx) {
3122
3584
  const natural = classifyContext(ctx);
3123
3585
  const cls = strategyToClass(initial, natural);
3586
+ const inherited = natural.fallbackChain ?? defaultFallbackChain(initial);
3587
+ const fallbackChain = inherited.filter((s) => s !== initial);
3124
3588
  return {
3125
3589
  class: cls,
3126
3590
  strategy: initial,
3127
3591
  reason: `initial strategy "${initial}" requested via options.initialStrategy`,
3128
- fallbackChain: natural.fallbackChain ?? defaultFallbackChain(initial)
3592
+ fallbackChain
3129
3593
  };
3130
3594
  }
3131
3595
  function strategyToClass(strategy, natural) {
@@ -3154,6 +3618,6 @@ function defaultFallbackChain(strategy) {
3154
3618
  }
3155
3619
  }
3156
3620
 
3157
- export { UnifiedPlayer, avbridgeAudioToMediabunny, avbridgeVideoToMediabunny, buildMediabunnySourceFromInput, classifyContext, createPlayer, probe, srtToVtt };
3158
- //# sourceMappingURL=chunk-C5VA5U5O.js.map
3159
- //# sourceMappingURL=chunk-C5VA5U5O.js.map
3621
+ export { FALLBACK_AUDIO_CODECS, FALLBACK_VIDEO_CODECS, NATIVE_AUDIO_CODECS, NATIVE_VIDEO_CODECS, UnifiedPlayer, avbridgeAudioToMediabunny, avbridgeVideoToMediabunny, buildMediabunnySourceFromInput, classifyContext, createPlayer, probe, srtToVtt };
3622
+ //# sourceMappingURL=chunk-NV7ILLWH.js.map
3623
+ //# sourceMappingURL=chunk-NV7ILLWH.js.map