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,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
- var chunkHZLQNKFN_cjs = require('./chunk-HZLQNKFN.cjs');
3
+ var chunk6UUT4BEA_cjs = require('./chunk-6UUT4BEA.cjs');
4
4
  var chunkG4APZMCP_cjs = require('./chunk-G4APZMCP.cjs');
5
- var chunkNZU7W256_cjs = require('./chunk-NZU7W256.cjs');
5
+ var chunkF3LQJKXK_cjs = require('./chunk-F3LQJKXK.cjs');
6
6
 
7
7
  // src/probe/mediabunny.ts
8
8
  async function probeWithMediabunny(source, sniffedContainer) {
@@ -180,37 +180,58 @@ var MEDIABUNNY_CONTAINERS = /* @__PURE__ */ new Set([
180
180
  "adts",
181
181
  "mpegts"
182
182
  ]);
183
- async function probe(source) {
184
- const normalized = await chunkHZLQNKFN_cjs.normalizeSource(source);
185
- const sniffed = await chunkHZLQNKFN_cjs.sniffNormalizedSource(normalized);
183
+ async function probe(source, transport) {
184
+ const normalized = await chunk6UUT4BEA_cjs.normalizeSource(source, transport);
185
+ const sniffed = await chunk6UUT4BEA_cjs.sniffNormalizedSource(normalized);
186
186
  if (MEDIABUNNY_CONTAINERS.has(sniffed)) {
187
187
  try {
188
- return await probeWithMediabunny(normalized, sniffed);
188
+ const result = await probeWithMediabunny(normalized, sniffed);
189
+ const hasUnknownCodec = result.videoTracks.some((t) => t.codec === "unknown") || result.audioTracks.some((t) => t.codec === "unknown");
190
+ if (hasUnknownCodec) {
191
+ try {
192
+ const { probeWithLibav } = await import('./avi-W6L3BTWU.cjs');
193
+ return await probeWithLibav(normalized, sniffed);
194
+ } catch {
195
+ return result;
196
+ }
197
+ }
198
+ return result;
189
199
  } catch (mediabunnyErr) {
190
200
  console.warn(
191
201
  `[avbridge] mediabunny rejected ${sniffed} file, falling back to libav:`,
192
202
  mediabunnyErr.message
193
203
  );
194
204
  try {
195
- const { probeWithLibav } = await import('./avi-6SJLWIWW.cjs');
205
+ const { probeWithLibav } = await import('./avi-W6L3BTWU.cjs');
196
206
  return await probeWithLibav(normalized, sniffed);
197
207
  } catch (libavErr) {
198
208
  const mbMsg = mediabunnyErr.message || String(mediabunnyErr);
199
209
  const lvMsg = libavErr instanceof Error ? libavErr.message : String(libavErr);
200
- throw new Error(
201
- `failed to probe ${sniffed} file. mediabunny: ${mbMsg}. libav fallback: ${lvMsg}.`
210
+ throw new chunk6UUT4BEA_cjs.AvbridgeError(
211
+ chunk6UUT4BEA_cjs.ERR_PROBE_FAILED,
212
+ `Failed to probe ${sniffed.toUpperCase()} file. mediabunny: ${mbMsg}. libav: ${lvMsg}.`,
213
+ "The file may be corrupt, truncated, or in an unsupported format. Enable AVBRIDGE_DEBUG for detailed logs."
202
214
  );
203
215
  }
204
216
  }
205
217
  }
206
218
  try {
207
- const { probeWithLibav } = await import('./avi-6SJLWIWW.cjs');
219
+ const { probeWithLibav } = await import('./avi-W6L3BTWU.cjs');
208
220
  return await probeWithLibav(normalized, sniffed);
209
221
  } catch (err) {
210
222
  const inner = err instanceof Error ? err.message : String(err);
211
223
  console.error("[avbridge] libav probe failed for", sniffed, "file:", err);
212
- throw new Error(
213
- sniffed === "unknown" ? `unable to probe source: container could not be identified, and the libav.js fallback also failed: ${inner || "(no message \u2014 see browser console for the original error)"}` : `${sniffed.toUpperCase()} files require libav.js, which failed to load: ${inner || "(no message \u2014 see browser console for the original error)"}`
224
+ if (sniffed === "unknown") {
225
+ throw new chunk6UUT4BEA_cjs.AvbridgeError(
226
+ chunk6UUT4BEA_cjs.ERR_PROBE_UNKNOWN_CONTAINER,
227
+ `Unable to probe source: container format could not be identified. libav fallback: ${inner || "(no details)"}`,
228
+ "The file may be corrupt or in a format avbridge doesn't recognize. Check the file plays in VLC or ffprobe."
229
+ );
230
+ }
231
+ throw new chunk6UUT4BEA_cjs.AvbridgeError(
232
+ chunk6UUT4BEA_cjs.ERR_LIBAV_NOT_REACHABLE,
233
+ `${sniffed.toUpperCase()} files require libav.js, which failed to load: ${inner || "(no details)"}`,
234
+ "Install @libav.js/variant-webcodecs, or check that AVBRIDGE_LIBAV_BASE points to the correct path."
214
235
  );
215
236
  }
216
237
  }
@@ -311,7 +332,9 @@ var FALLBACK_AUDIO_CODECS = /* @__PURE__ */ new Set([
311
332
  "ra_144",
312
333
  "ra_288",
313
334
  "sipr",
314
- "atrac3"
335
+ "atrac3",
336
+ "dts",
337
+ "truehd"
315
338
  ]);
316
339
  var NATIVE_CONTAINERS = /* @__PURE__ */ new Set([
317
340
  "mp4",
@@ -373,7 +396,16 @@ function classifyContext(ctx) {
373
396
  reason: `video codec "${video.codec}" has no browser decoder; WASM fallback required`
374
397
  };
375
398
  }
376
- if (audio && FALLBACK_AUDIO_CODECS.has(audio.codec)) {
399
+ const audioNeedsFallback = audio && (FALLBACK_AUDIO_CODECS.has(audio.codec) || !NATIVE_AUDIO_CODECS.has(audio.codec));
400
+ if (audioNeedsFallback) {
401
+ if (NATIVE_VIDEO_CODECS.has(video.codec) && webCodecsAvailable()) {
402
+ return {
403
+ class: "HYBRID_CANDIDATE",
404
+ strategy: "hybrid",
405
+ reason: `video "${video.codec}" is hardware-decodable via WebCodecs; audio "${audio.codec}" decoded in software by libav`,
406
+ fallbackChain: ["fallback"]
407
+ };
408
+ }
377
409
  return {
378
410
  class: "FALLBACK_REQUIRED",
379
411
  strategy: "fallback",
@@ -740,10 +772,18 @@ var MseSink = class {
740
772
  constructor(options) {
741
773
  this.options = options;
742
774
  if (typeof MediaSource === "undefined") {
743
- throw new Error("MSE not supported in this environment");
775
+ throw new chunk6UUT4BEA_cjs.AvbridgeError(
776
+ chunk6UUT4BEA_cjs.ERR_MSE_NOT_SUPPORTED,
777
+ "MediaSource Extensions (MSE) are not supported in this environment.",
778
+ "MSE is required for the remux strategy. Use a browser that supports MSE, or try the fallback strategy."
779
+ );
744
780
  }
745
781
  if (!MediaSource.isTypeSupported(options.mime)) {
746
- throw new Error(`MSE does not support MIME "${options.mime}" \u2014 cannot remux`);
782
+ throw new chunk6UUT4BEA_cjs.AvbridgeError(
783
+ chunk6UUT4BEA_cjs.ERR_MSE_CODEC_NOT_SUPPORTED,
784
+ `This browser's MSE does not support "${options.mime}".`,
785
+ "The codec combination can't be played via remux in this browser. The player will try the next strategy automatically."
786
+ );
747
787
  }
748
788
  this.mediaSource = new MediaSource();
749
789
  this.objectUrl = URL.createObjectURL(this.mediaSource);
@@ -1072,6 +1112,10 @@ async function createRemuxPipeline(ctx, video) {
1072
1112
  console.error("[avbridge] remux pipeline reseek failed:", err);
1073
1113
  });
1074
1114
  },
1115
+ setAutoPlay(autoPlay) {
1116
+ pendingAutoPlay = autoPlay;
1117
+ if (sink) sink.setPlayOnSeek(autoPlay);
1118
+ },
1075
1119
  async destroy() {
1076
1120
  destroyed = true;
1077
1121
  pumpToken++;
@@ -1112,7 +1156,11 @@ async function createRemuxSession(context, video) {
1112
1156
  await pipeline.start(video.currentTime || 0, true);
1113
1157
  return;
1114
1158
  }
1115
- await video.play();
1159
+ pipeline.setAutoPlay(true);
1160
+ try {
1161
+ await video.play();
1162
+ } catch {
1163
+ }
1116
1164
  },
1117
1165
  pause() {
1118
1166
  wantPlay = false;
@@ -1151,6 +1199,10 @@ async function createRemuxSession(context, video) {
1151
1199
  }
1152
1200
 
1153
1201
  // src/strategies/fallback/video-renderer.ts
1202
+ function isDebug() {
1203
+ return typeof globalThis !== "undefined" && !!globalThis.AVBRIDGE_DEBUG;
1204
+ }
1205
+ var lastDebugLog = 0;
1154
1206
  var VideoRenderer = class {
1155
1207
  constructor(target, clock, fps = 30) {
1156
1208
  this.target = target;
@@ -1160,7 +1212,7 @@ var VideoRenderer = class {
1160
1212
  this.resolveFirstFrame = resolve;
1161
1213
  });
1162
1214
  this.canvas = document.createElement("canvas");
1163
- this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;";
1215
+ this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;object-fit:contain;";
1164
1216
  const parent = target.parentElement ?? target.parentNode;
1165
1217
  if (parent && parent instanceof HTMLElement) {
1166
1218
  if (getComputedStyle(parent).position === "static") {
@@ -1197,6 +1249,20 @@ var VideoRenderer = class {
1197
1249
  lastPaintWall = 0;
1198
1250
  /** Minimum ms between paints — paces video at roughly source fps. */
1199
1251
  paintIntervalMs;
1252
+ /** Cumulative count of frames skipped because all PTS are in the future. */
1253
+ ticksWaiting = 0;
1254
+ /** Cumulative count of ticks where PTS mode painted a frame. */
1255
+ ticksPainted = 0;
1256
+ /**
1257
+ * Calibration offset (microseconds) between video PTS and audio clock.
1258
+ * Video PTS and AudioContext.currentTime can drift ~0.1% relative to
1259
+ * each other (different clock domains). Over 45 minutes that's 2.6s.
1260
+ * We measure the offset on the first painted frame and update it
1261
+ * periodically so the PTS comparison stays calibrated.
1262
+ */
1263
+ ptsCalibrationUs = 0;
1264
+ ptsCalibrated = false;
1265
+ lastCalibrationWall = 0;
1200
1266
  /** Resolves once the first decoded frame has been enqueued. */
1201
1267
  firstFrameReady;
1202
1268
  resolveFirstFrame;
@@ -1245,21 +1311,81 @@ var VideoRenderer = class {
1245
1311
  }
1246
1312
  return;
1247
1313
  }
1248
- const wallNow = performance.now();
1249
- if (wallNow - this.lastPaintWall < this.paintIntervalMs - 2) return;
1250
- if (this.queue.length === 0) return;
1251
- if (this.framesPainted > 0 && this.framesPainted % 30 === 0) {
1252
- const audioNowUs = this.clock.now() * 1e6;
1253
- const headTs = this.queue[0].timestamp ?? 0;
1254
- const driftUs = headTs - audioNowUs;
1255
- if (driftUs < -15e4) {
1256
- this.queue.shift()?.close();
1257
- this.framesDroppedLate++;
1258
- if (this.queue.length === 0) return;
1259
- } else if (driftUs > 15e4) {
1314
+ const rawAudioNowUs = this.clock.now() * 1e6;
1315
+ const headTs = this.queue[0].timestamp ?? 0;
1316
+ const hasPts = headTs > 0 || this.queue.length > 1;
1317
+ if (hasPts) {
1318
+ const wallNow2 = performance.now();
1319
+ if (!this.ptsCalibrated || wallNow2 - this.lastCalibrationWall > 1e4) {
1320
+ this.ptsCalibrationUs = headTs - rawAudioNowUs;
1321
+ this.ptsCalibrated = true;
1322
+ this.lastCalibrationWall = wallNow2;
1323
+ }
1324
+ const audioNowUs = rawAudioNowUs + this.ptsCalibrationUs;
1325
+ const frameDurationUs = this.paintIntervalMs * 1e3;
1326
+ const deadlineUs = audioNowUs + frameDurationUs;
1327
+ let bestIdx = -1;
1328
+ for (let i = 0; i < this.queue.length; i++) {
1329
+ const ts = this.queue[i].timestamp ?? 0;
1330
+ if (ts <= deadlineUs) {
1331
+ bestIdx = i;
1332
+ } else {
1333
+ break;
1334
+ }
1335
+ }
1336
+ if (bestIdx < 0) {
1337
+ this.ticksWaiting++;
1338
+ if (isDebug()) {
1339
+ const now = performance.now();
1340
+ if (now - lastDebugLog > 1e3) {
1341
+ const headPtsMs = (headTs / 1e3).toFixed(1);
1342
+ const audioMs = (audioNowUs / 1e3).toFixed(1);
1343
+ const rawDriftMs = ((headTs - rawAudioNowUs) / 1e3).toFixed(1);
1344
+ const calibMs = (this.ptsCalibrationUs / 1e3).toFixed(1);
1345
+ console.log(
1346
+ `[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}`
1347
+ );
1348
+ lastDebugLog = now;
1349
+ }
1350
+ }
1260
1351
  return;
1261
1352
  }
1353
+ const dropThresholdUs = audioNowUs - frameDurationUs * 2;
1354
+ let dropped = 0;
1355
+ while (bestIdx > 0) {
1356
+ const ts = this.queue[0].timestamp ?? 0;
1357
+ if (ts < dropThresholdUs) {
1358
+ this.queue.shift()?.close();
1359
+ this.framesDroppedLate++;
1360
+ bestIdx--;
1361
+ dropped++;
1362
+ } else {
1363
+ break;
1364
+ }
1365
+ }
1366
+ this.ticksPainted++;
1367
+ if (isDebug()) {
1368
+ const now = performance.now();
1369
+ if (now - lastDebugLog > 1e3) {
1370
+ const paintedTs = this.queue[0]?.timestamp ?? 0;
1371
+ const audioMs = (audioNowUs / 1e3).toFixed(1);
1372
+ const ptsMs = (paintedTs / 1e3).toFixed(1);
1373
+ const rawDriftMs = ((paintedTs - rawAudioNowUs) / 1e3).toFixed(1);
1374
+ const calibMs = (this.ptsCalibrationUs / 1e3).toFixed(1);
1375
+ console.log(
1376
+ `[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}`
1377
+ );
1378
+ lastDebugLog = now;
1379
+ }
1380
+ }
1381
+ const frame2 = this.queue.shift();
1382
+ this.paint(frame2);
1383
+ frame2.close();
1384
+ this.lastPaintWall = performance.now();
1385
+ return;
1262
1386
  }
1387
+ const wallNow = performance.now();
1388
+ if (wallNow - this.lastPaintWall < this.paintIntervalMs - 2) return;
1263
1389
  const frame = this.queue.shift();
1264
1390
  this.paint(frame);
1265
1391
  frame.close();
@@ -1281,8 +1407,13 @@ var VideoRenderer = class {
1281
1407
  }
1282
1408
  /** Discard all queued frames. Used by seek to drop stale buffers. */
1283
1409
  flush() {
1410
+ const count = this.queue.length;
1284
1411
  while (this.queue.length > 0) this.queue.shift()?.close();
1285
1412
  this.prerolled = false;
1413
+ this.ptsCalibrated = false;
1414
+ if (isDebug() && count > 0) {
1415
+ console.log(`[avbridge:renderer] FLUSH discarded=${count} painted=${this.framesPainted} drops=${this.framesDroppedLate}`);
1416
+ }
1286
1417
  }
1287
1418
  stats() {
1288
1419
  return {
@@ -1327,11 +1458,38 @@ var AudioOutput = class {
1327
1458
  pendingQueue = [];
1328
1459
  framesScheduled = 0;
1329
1460
  destroyed = false;
1461
+ /** User-set volume (0..1). Applied to the gain node. */
1462
+ _volume = 1;
1463
+ /** User-set muted flag. When true, gain is forced to 0. */
1464
+ _muted = false;
1330
1465
  constructor() {
1331
1466
  this.ctx = new AudioContext();
1332
1467
  this.gain = this.ctx.createGain();
1333
1468
  this.gain.connect(this.ctx.destination);
1334
1469
  }
1470
+ /** Set volume (0..1). Applied immediately to the gain node. */
1471
+ setVolume(v) {
1472
+ this._volume = Math.max(0, Math.min(1, v));
1473
+ this.applyGain();
1474
+ }
1475
+ getVolume() {
1476
+ return this._volume;
1477
+ }
1478
+ /** Set muted. When true, output is silenced regardless of volume. */
1479
+ setMuted(m) {
1480
+ this._muted = m;
1481
+ this.applyGain();
1482
+ }
1483
+ getMuted() {
1484
+ return this._muted;
1485
+ }
1486
+ applyGain() {
1487
+ const target = this._muted ? 0 : this._volume;
1488
+ try {
1489
+ this.gain.gain.value = target;
1490
+ } catch {
1491
+ }
1492
+ }
1335
1493
  /**
1336
1494
  * Switch into wall-clock fallback mode. Called by the decoder when no
1337
1495
  * audio decoder could be initialized for the source. Once set, this
@@ -1402,9 +1560,13 @@ var AudioOutput = class {
1402
1560
  const node = this.ctx.createBufferSource();
1403
1561
  node.buffer = buffer;
1404
1562
  node.connect(this.gain);
1405
- const ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor);
1406
- const safeStart = Math.max(ctxStart, this.ctx.currentTime);
1407
- node.start(safeStart);
1563
+ let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor);
1564
+ if (ctxStart < this.ctx.currentTime) {
1565
+ this.ctxTimeAtAnchor = this.ctx.currentTime;
1566
+ this.mediaTimeOfAnchor = this.mediaTimeOfNext;
1567
+ ctxStart = this.ctx.currentTime;
1568
+ }
1569
+ node.start(ctxStart);
1408
1570
  this.mediaTimeOfNext += frameCount / sampleRate;
1409
1571
  this.framesScheduled++;
1410
1572
  }
@@ -1477,6 +1639,7 @@ var AudioOutput = class {
1477
1639
  }
1478
1640
  this.gain = this.ctx.createGain();
1479
1641
  this.gain.connect(this.ctx.destination);
1642
+ this.applyGain();
1480
1643
  this.pendingQueue = [];
1481
1644
  this.mediaTimeOfAnchor = newMediaTime;
1482
1645
  this.mediaTimeOfNext = newMediaTime;
@@ -1505,11 +1668,11 @@ var AudioOutput = class {
1505
1668
 
1506
1669
  // src/strategies/hybrid/decoder.ts
1507
1670
  async function startHybridDecoder(opts) {
1508
- const variant = chunkNZU7W256_cjs.pickLibavVariant(opts.context);
1671
+ const variant = chunkF3LQJKXK_cjs.pickLibavVariant(opts.context);
1509
1672
  const libav = await chunkG4APZMCP_cjs.loadLibav(variant);
1510
1673
  const bridge = await loadBridge();
1511
- const { prepareLibavInput } = await import('./libav-http-reader-FPYDBMYK.cjs');
1512
- const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source);
1674
+ const { prepareLibavInput } = await import('./libav-http-reader-AZLE7YFS.cjs');
1675
+ const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source, opts.transport);
1513
1676
  const readPkt = await libav.av_packet_alloc();
1514
1677
  const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
1515
1678
  const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
@@ -1580,6 +1743,56 @@ async function startHybridDecoder(opts) {
1580
1743
  });
1581
1744
  throw new Error("hybrid decoder: could not initialize any decoders");
1582
1745
  }
1746
+ let bsfCtx = null;
1747
+ let bsfPkt = null;
1748
+ if (videoStream && opts.context.videoTracks[0]?.codec === "mpeg4") {
1749
+ try {
1750
+ bsfCtx = await libav.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
1751
+ if (bsfCtx != null && bsfCtx >= 0) {
1752
+ const parIn = await libav.AVBSFContext_par_in(bsfCtx);
1753
+ await libav.avcodec_parameters_copy(parIn, videoStream.codecpar);
1754
+ await libav.av_bsf_init(bsfCtx);
1755
+ bsfPkt = await libav.av_packet_alloc();
1756
+ chunkG4APZMCP_cjs.dbg.info("bsf", "mpeg4_unpack_bframes BSF active (hybrid)");
1757
+ } else {
1758
+ console.warn("[avbridge] mpeg4_unpack_bframes BSF not available in hybrid decoder");
1759
+ bsfCtx = null;
1760
+ }
1761
+ } catch (err) {
1762
+ console.warn("[avbridge] hybrid: failed to init BSF:", err.message);
1763
+ bsfCtx = null;
1764
+ bsfPkt = null;
1765
+ }
1766
+ }
1767
+ async function applyBSF(packets) {
1768
+ if (!bsfCtx || !bsfPkt) return packets;
1769
+ const out = [];
1770
+ for (const pkt of packets) {
1771
+ await libav.ff_copyin_packet(bsfPkt, pkt);
1772
+ const sendErr = await libav.av_bsf_send_packet(bsfCtx, bsfPkt);
1773
+ if (sendErr < 0) {
1774
+ out.push(pkt);
1775
+ continue;
1776
+ }
1777
+ while (true) {
1778
+ const recvErr = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
1779
+ if (recvErr < 0) break;
1780
+ out.push(await libav.ff_copyout_packet(bsfPkt));
1781
+ }
1782
+ }
1783
+ return out;
1784
+ }
1785
+ async function flushBSF() {
1786
+ if (!bsfCtx || !bsfPkt) return;
1787
+ try {
1788
+ await libav.av_bsf_send_packet(bsfCtx, 0);
1789
+ while (true) {
1790
+ const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
1791
+ if (err < 0) break;
1792
+ }
1793
+ } catch {
1794
+ }
1795
+ }
1583
1796
  let destroyed = false;
1584
1797
  let pumpToken = 0;
1585
1798
  let pumpRunning = null;
@@ -1607,8 +1820,15 @@ async function startHybridDecoder(opts) {
1607
1820
  if (myToken !== pumpToken || destroyed) return;
1608
1821
  const videoPackets = videoStream ? packets[videoStream.index] : void 0;
1609
1822
  const audioPackets = audioStream ? packets[audioStream.index] : void 0;
1823
+ if (audioDec && audioPackets && audioPackets.length > 0) {
1824
+ await decodeAudioBatch(audioPackets, myToken);
1825
+ }
1826
+ if (myToken !== pumpToken || destroyed) return;
1827
+ await new Promise((r) => setTimeout(r, 0));
1828
+ if (myToken !== pumpToken || destroyed) return;
1610
1829
  if (videoDecoder && videoPackets && videoPackets.length > 0) {
1611
- for (const pkt of videoPackets) {
1830
+ const processed = await applyBSF(videoPackets);
1831
+ for (const pkt of processed) {
1612
1832
  if (myToken !== pumpToken || destroyed) return;
1613
1833
  sanitizePacketTimestamp(pkt, () => {
1614
1834
  const ts = syntheticVideoUs;
@@ -1628,9 +1848,6 @@ async function startHybridDecoder(opts) {
1628
1848
  }
1629
1849
  }
1630
1850
  }
1631
- if (audioDec && audioPackets && audioPackets.length > 0) {
1632
- await decodeAudioBatch(audioPackets, myToken);
1633
- }
1634
1851
  packetsRead += (videoPackets?.length ?? 0) + (audioPackets?.length ?? 0);
1635
1852
  while (!destroyed && myToken === pumpToken && (videoDecoder && videoDecoder.decodeQueueSize > 10 || opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
1636
1853
  await new Promise((r) => setTimeout(r, 50));
@@ -1653,20 +1870,43 @@ async function startHybridDecoder(opts) {
1653
1870
  }
1654
1871
  async function decodeAudioBatch(pkts, myToken, flush = false) {
1655
1872
  if (!audioDec || destroyed || myToken !== pumpToken) return;
1656
- let frames;
1657
- try {
1658
- frames = await libav.ff_decode_multi(
1659
- audioDec.c,
1660
- audioDec.pkt,
1661
- audioDec.frame,
1662
- pkts,
1663
- flush ? { fin: true, ignoreErrors: true } : { ignoreErrors: true }
1664
- );
1665
- } catch (err) {
1666
- console.error("[avbridge] hybrid audio decode failed:", err);
1667
- return;
1873
+ const AUDIO_SUB_BATCH = 4;
1874
+ let allFrames = [];
1875
+ for (let i = 0; i < pkts.length; i += AUDIO_SUB_BATCH) {
1876
+ if (myToken !== pumpToken || destroyed) return;
1877
+ const slice = pkts.slice(i, i + AUDIO_SUB_BATCH);
1878
+ const isLast = i + AUDIO_SUB_BATCH >= pkts.length;
1879
+ try {
1880
+ const frames2 = await libav.ff_decode_multi(
1881
+ audioDec.c,
1882
+ audioDec.pkt,
1883
+ audioDec.frame,
1884
+ slice,
1885
+ isLast && flush ? { fin: true, ignoreErrors: true } : { ignoreErrors: true }
1886
+ );
1887
+ allFrames = allFrames.concat(frames2);
1888
+ } catch (err) {
1889
+ console.error("[avbridge] hybrid audio decode failed:", err);
1890
+ return;
1891
+ }
1892
+ if (!isLast) await new Promise((r) => setTimeout(r, 0));
1893
+ }
1894
+ if (pkts.length === 0 && flush) {
1895
+ try {
1896
+ allFrames = await libav.ff_decode_multi(
1897
+ audioDec.c,
1898
+ audioDec.pkt,
1899
+ audioDec.frame,
1900
+ [],
1901
+ { fin: true, ignoreErrors: true }
1902
+ );
1903
+ } catch (err) {
1904
+ console.error("[avbridge] hybrid audio flush failed:", err);
1905
+ return;
1906
+ }
1668
1907
  }
1669
1908
  if (myToken !== pumpToken || destroyed) return;
1909
+ const frames = allFrames;
1670
1910
  for (const f of frames) {
1671
1911
  if (myToken !== pumpToken || destroyed) return;
1672
1912
  sanitizeFrameTimestamp(
@@ -1703,6 +1943,14 @@ async function startHybridDecoder(opts) {
1703
1943
  await pumpRunning;
1704
1944
  } catch {
1705
1945
  }
1946
+ try {
1947
+ if (bsfCtx) await libav.av_bsf_free(bsfCtx);
1948
+ } catch {
1949
+ }
1950
+ try {
1951
+ if (bsfPkt) await libav.av_packet_free?.(bsfPkt);
1952
+ } catch {
1953
+ }
1706
1954
  try {
1707
1955
  if (videoDecoder && videoDecoder.state !== "closed") videoDecoder.close();
1708
1956
  } catch {
@@ -1756,6 +2004,7 @@ async function startHybridDecoder(opts) {
1756
2004
  if (audioDec) await libav.avcodec_flush_buffers?.(audioDec.c);
1757
2005
  } catch {
1758
2006
  }
2007
+ await flushBSF();
1759
2008
  syntheticVideoUs = Math.round(timeSec * 1e6);
1760
2009
  syntheticAudioUs = Math.round(timeSec * 1e6);
1761
2010
  pumpRunning = pumpLoop(newToken).catch(
@@ -1769,6 +2018,7 @@ async function startHybridDecoder(opts) {
1769
2018
  videoFramesDecoded,
1770
2019
  videoChunksFed,
1771
2020
  audioFramesDecoded,
2021
+ bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
1772
2022
  videoDecodeQueueSize: videoDecoder?.decodeQueueSize ?? 0,
1773
2023
  // Confirmed transport info — see fallback decoder for the pattern.
1774
2024
  _transport: inputHandle.transport === "http-range" ? "http-range" : "memory",
@@ -1945,8 +2195,8 @@ async function loadBridge() {
1945
2195
  // src/strategies/hybrid/index.ts
1946
2196
  var READY_AUDIO_BUFFER_SECONDS = 0.3;
1947
2197
  var READY_TIMEOUT_SECONDS = 10;
1948
- async function createHybridSession(ctx, target) {
1949
- const { normalizeSource: normalizeSource2 } = await import('./source-CN43EI7Z.cjs');
2198
+ async function createHybridSession(ctx, target, transport) {
2199
+ const { normalizeSource: normalizeSource2 } = await import('./source-73CAH6HW.cjs');
1950
2200
  const source = await normalizeSource2(ctx.source);
1951
2201
  const fps = ctx.videoTracks[0]?.fps ?? 30;
1952
2202
  const audio = new AudioOutput();
@@ -1958,7 +2208,8 @@ async function createHybridSession(ctx, target) {
1958
2208
  filename: ctx.name ?? "input.bin",
1959
2209
  context: ctx,
1960
2210
  renderer,
1961
- audio
2211
+ audio,
2212
+ transport
1962
2213
  });
1963
2214
  } catch (err) {
1964
2215
  audio.destroy();
@@ -1972,6 +2223,26 @@ async function createHybridSession(ctx, target) {
1972
2223
  void doSeek(v);
1973
2224
  }
1974
2225
  });
2226
+ Object.defineProperty(target, "paused", {
2227
+ configurable: true,
2228
+ get: () => !audio.isPlaying()
2229
+ });
2230
+ Object.defineProperty(target, "volume", {
2231
+ configurable: true,
2232
+ get: () => audio.getVolume(),
2233
+ set: (v) => {
2234
+ audio.setVolume(v);
2235
+ target.dispatchEvent(new Event("volumechange"));
2236
+ }
2237
+ });
2238
+ Object.defineProperty(target, "muted", {
2239
+ configurable: true,
2240
+ get: () => audio.getMuted(),
2241
+ set: (m) => {
2242
+ audio.setMuted(m);
2243
+ target.dispatchEvent(new Event("volumechange"));
2244
+ }
2245
+ });
1975
2246
  if (ctx.duration && Number.isFinite(ctx.duration)) {
1976
2247
  Object.defineProperty(target, "duration", {
1977
2248
  configurable: true,
@@ -2011,10 +2282,13 @@ async function createHybridSession(ctx, target) {
2011
2282
  if (!audio.isPlaying()) {
2012
2283
  await waitForBuffer();
2013
2284
  await audio.start();
2285
+ target.dispatchEvent(new Event("play"));
2286
+ target.dispatchEvent(new Event("playing"));
2014
2287
  }
2015
2288
  },
2016
2289
  pause() {
2017
2290
  void audio.pause();
2291
+ target.dispatchEvent(new Event("pause"));
2018
2292
  },
2019
2293
  async seek(time) {
2020
2294
  await doSeek(time);
@@ -2036,6 +2310,9 @@ async function createHybridSession(ctx, target) {
2036
2310
  try {
2037
2311
  delete target.currentTime;
2038
2312
  delete target.duration;
2313
+ delete target.paused;
2314
+ delete target.volume;
2315
+ delete target.muted;
2039
2316
  } catch {
2040
2317
  }
2041
2318
  },
@@ -2047,11 +2324,11 @@ async function createHybridSession(ctx, target) {
2047
2324
 
2048
2325
  // src/strategies/fallback/decoder.ts
2049
2326
  async function startDecoder(opts) {
2050
- const variant = chunkNZU7W256_cjs.pickLibavVariant(opts.context);
2327
+ const variant = chunkF3LQJKXK_cjs.pickLibavVariant(opts.context);
2051
2328
  const libav = await chunkG4APZMCP_cjs.loadLibav(variant);
2052
2329
  const bridge = await loadBridge2();
2053
- const { prepareLibavInput } = await import('./libav-http-reader-FPYDBMYK.cjs');
2054
- const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source);
2330
+ const { prepareLibavInput } = await import('./libav-http-reader-AZLE7YFS.cjs');
2331
+ const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source, opts.transport);
2055
2332
  const readPkt = await libav.av_packet_alloc();
2056
2333
  const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
2057
2334
  const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
@@ -2107,6 +2384,56 @@ async function startDecoder(opts) {
2107
2384
  `fallback decoder: could not initialize any libav decoders (${codecs}).${hint}`
2108
2385
  );
2109
2386
  }
2387
+ let bsfCtx = null;
2388
+ let bsfPkt = null;
2389
+ if (videoStream && opts.context.videoTracks[0]?.codec === "mpeg4") {
2390
+ try {
2391
+ bsfCtx = await libav.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
2392
+ if (bsfCtx != null && bsfCtx >= 0) {
2393
+ const parIn = await libav.AVBSFContext_par_in(bsfCtx);
2394
+ await libav.avcodec_parameters_copy(parIn, videoStream.codecpar);
2395
+ await libav.av_bsf_init(bsfCtx);
2396
+ bsfPkt = await libav.av_packet_alloc();
2397
+ chunkG4APZMCP_cjs.dbg.info("bsf", "mpeg4_unpack_bframes BSF active");
2398
+ } else {
2399
+ console.warn("[avbridge] mpeg4_unpack_bframes BSF not available \u2014 decoding without it");
2400
+ bsfCtx = null;
2401
+ }
2402
+ } catch (err) {
2403
+ console.warn("[avbridge] failed to init mpeg4_unpack_bframes BSF:", err.message);
2404
+ bsfCtx = null;
2405
+ bsfPkt = null;
2406
+ }
2407
+ }
2408
+ async function applyBSF(packets) {
2409
+ if (!bsfCtx || !bsfPkt) return packets;
2410
+ const out = [];
2411
+ for (const pkt of packets) {
2412
+ await libav.ff_copyin_packet(bsfPkt, pkt);
2413
+ const sendErr = await libav.av_bsf_send_packet(bsfCtx, bsfPkt);
2414
+ if (sendErr < 0) {
2415
+ out.push(pkt);
2416
+ continue;
2417
+ }
2418
+ while (true) {
2419
+ const recvErr = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
2420
+ if (recvErr < 0) break;
2421
+ out.push(await libav.ff_copyout_packet(bsfPkt));
2422
+ }
2423
+ }
2424
+ return out;
2425
+ }
2426
+ async function flushBSF() {
2427
+ if (!bsfCtx || !bsfPkt) return;
2428
+ try {
2429
+ await libav.av_bsf_send_packet(bsfCtx, 0);
2430
+ while (true) {
2431
+ const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
2432
+ if (err < 0) break;
2433
+ }
2434
+ } catch {
2435
+ }
2436
+ }
2110
2437
  let destroyed = false;
2111
2438
  let pumpToken = 0;
2112
2439
  let pumpRunning = null;
@@ -2115,7 +2442,8 @@ async function startDecoder(opts) {
2115
2442
  let audioFramesDecoded = 0;
2116
2443
  let watchdogFirstFrameMs = 0;
2117
2444
  let watchdogSlowSinceMs = 0;
2118
- let watchdogWarned = false;
2445
+ let watchdogSlowWarned = false;
2446
+ let watchdogOverflowWarned = false;
2119
2447
  let syntheticVideoUs = 0;
2120
2448
  let syntheticAudioUs = 0;
2121
2449
  const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
@@ -2127,7 +2455,7 @@ async function startDecoder(opts) {
2127
2455
  let packets;
2128
2456
  try {
2129
2457
  [readErr, packets] = await libav.ff_read_frame_multi(fmt_ctx, readPkt, {
2130
- limit: 64 * 1024
2458
+ limit: 16 * 1024
2131
2459
  });
2132
2460
  } catch (err) {
2133
2461
  console.error("[avbridge] ff_read_frame_multi failed:", err);
@@ -2136,26 +2464,27 @@ async function startDecoder(opts) {
2136
2464
  if (myToken !== pumpToken || destroyed) return;
2137
2465
  const videoPackets = videoStream ? packets[videoStream.index] : void 0;
2138
2466
  const audioPackets = audioStream ? packets[audioStream.index] : void 0;
2139
- if (videoDec && videoPackets && videoPackets.length > 0) {
2140
- await decodeVideoBatch(videoPackets, myToken);
2141
- }
2142
- if (myToken !== pumpToken || destroyed) return;
2143
2467
  if (audioDec && audioPackets && audioPackets.length > 0) {
2144
2468
  await decodeAudioBatch(audioPackets, myToken);
2145
2469
  }
2470
+ if (myToken !== pumpToken || destroyed) return;
2471
+ if (videoDec && videoPackets && videoPackets.length > 0) {
2472
+ const processed = await applyBSF(videoPackets);
2473
+ await decodeVideoBatch(processed, myToken);
2474
+ }
2146
2475
  packetsRead += (videoPackets?.length ?? 0) + (audioPackets?.length ?? 0);
2147
2476
  if (videoFramesDecoded > 0) {
2148
2477
  if (watchdogFirstFrameMs === 0) {
2149
2478
  watchdogFirstFrameMs = performance.now();
2150
2479
  }
2151
2480
  const elapsedSinceFirst = (performance.now() - watchdogFirstFrameMs) / 1e3;
2152
- if (elapsedSinceFirst > 1 && !watchdogWarned) {
2481
+ if (elapsedSinceFirst > 1 && !watchdogSlowWarned) {
2153
2482
  const expectedFrames = elapsedSinceFirst * videoFps;
2154
2483
  const ratio = videoFramesDecoded / expectedFrames;
2155
2484
  if (ratio < 0.6) {
2156
2485
  if (watchdogSlowSinceMs === 0) watchdogSlowSinceMs = performance.now();
2157
2486
  if ((performance.now() - watchdogSlowSinceMs) / 1e3 > 5) {
2158
- watchdogWarned = true;
2487
+ watchdogSlowWarned = true;
2159
2488
  console.warn(
2160
2489
  "[avbridge:decode-rate]",
2161
2490
  `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.`
@@ -2165,6 +2494,17 @@ async function startDecoder(opts) {
2165
2494
  watchdogSlowSinceMs = 0;
2166
2495
  }
2167
2496
  }
2497
+ if (!watchdogOverflowWarned && videoFramesDecoded > 100) {
2498
+ const rendererStats = opts.renderer.stats();
2499
+ const overflow = rendererStats.framesDroppedOverflow ?? 0;
2500
+ if (overflow / videoFramesDecoded > 0.1) {
2501
+ watchdogOverflowWarned = true;
2502
+ console.warn(
2503
+ "[avbridge:overflow-drop]",
2504
+ `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.`
2505
+ );
2506
+ }
2507
+ }
2168
2508
  }
2169
2509
  while (!destroyed && myToken === pumpToken && (opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
2170
2510
  await new Promise((r) => setTimeout(r, 50));
@@ -2276,6 +2616,14 @@ async function startDecoder(opts) {
2276
2616
  await pumpRunning;
2277
2617
  } catch {
2278
2618
  }
2619
+ try {
2620
+ if (bsfCtx) await libav.av_bsf_free(bsfCtx);
2621
+ } catch {
2622
+ }
2623
+ try {
2624
+ if (bsfPkt) await libav.av_packet_free?.(bsfPkt);
2625
+ } catch {
2626
+ }
2279
2627
  try {
2280
2628
  if (videoDec) await libav.ff_free_decoder?.(videoDec.c, videoDec.pkt, videoDec.frame);
2281
2629
  } catch {
@@ -2327,6 +2675,7 @@ async function startDecoder(opts) {
2327
2675
  if (audioDec) await libav.avcodec_flush_buffers?.(audioDec.c);
2328
2676
  } catch {
2329
2677
  }
2678
+ await flushBSF();
2330
2679
  syntheticVideoUs = Math.round(timeSec * 1e6);
2331
2680
  syntheticAudioUs = Math.round(timeSec * 1e6);
2332
2681
  pumpRunning = pumpLoop(newToken).catch(
@@ -2339,6 +2688,7 @@ async function startDecoder(opts) {
2339
2688
  packetsRead,
2340
2689
  videoFramesDecoded,
2341
2690
  audioFramesDecoded,
2691
+ bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
2342
2692
  // Confirmed transport info: once prepareLibavInput returns
2343
2693
  // successfully, we *know* whether the source is http-range (probe
2344
2694
  // succeeded and returned 206) or in-memory blob. Diagnostics hoists
@@ -2494,8 +2844,8 @@ async function loadBridge2() {
2494
2844
  // src/strategies/fallback/index.ts
2495
2845
  var READY_AUDIO_BUFFER_SECONDS2 = 0.04;
2496
2846
  var READY_TIMEOUT_SECONDS2 = 3;
2497
- async function createFallbackSession(ctx, target) {
2498
- const { normalizeSource: normalizeSource2 } = await import('./source-CN43EI7Z.cjs');
2847
+ async function createFallbackSession(ctx, target, transport) {
2848
+ const { normalizeSource: normalizeSource2 } = await import('./source-73CAH6HW.cjs');
2499
2849
  const source = await normalizeSource2(ctx.source);
2500
2850
  const fps = ctx.videoTracks[0]?.fps ?? 30;
2501
2851
  const audio = new AudioOutput();
@@ -2507,7 +2857,8 @@ async function createFallbackSession(ctx, target) {
2507
2857
  filename: ctx.name ?? "input.bin",
2508
2858
  context: ctx,
2509
2859
  renderer,
2510
- audio
2860
+ audio,
2861
+ transport
2511
2862
  });
2512
2863
  } catch (err) {
2513
2864
  audio.destroy();
@@ -2521,6 +2872,26 @@ async function createFallbackSession(ctx, target) {
2521
2872
  void doSeek(v);
2522
2873
  }
2523
2874
  });
2875
+ Object.defineProperty(target, "paused", {
2876
+ configurable: true,
2877
+ get: () => !audio.isPlaying()
2878
+ });
2879
+ Object.defineProperty(target, "volume", {
2880
+ configurable: true,
2881
+ get: () => audio.getVolume(),
2882
+ set: (v) => {
2883
+ audio.setVolume(v);
2884
+ target.dispatchEvent(new Event("volumechange"));
2885
+ }
2886
+ });
2887
+ Object.defineProperty(target, "muted", {
2888
+ configurable: true,
2889
+ get: () => audio.getMuted(),
2890
+ set: (m) => {
2891
+ audio.setMuted(m);
2892
+ target.dispatchEvent(new Event("volumechange"));
2893
+ }
2894
+ });
2524
2895
  if (ctx.duration && Number.isFinite(ctx.duration)) {
2525
2896
  Object.defineProperty(target, "duration", {
2526
2897
  configurable: true,
@@ -2529,25 +2900,35 @@ async function createFallbackSession(ctx, target) {
2529
2900
  }
2530
2901
  async function waitForBuffer() {
2531
2902
  const start = performance.now();
2903
+ let firstFrameAtMs = 0;
2532
2904
  chunkG4APZMCP_cjs.dbg.info(
2533
2905
  "cold-start",
2534
- `gate entry: need audio >= ${READY_AUDIO_BUFFER_SECONDS2 * 1e3}ms + 1 frame`
2906
+ `gate entry: want audio \u2265 ${READY_AUDIO_BUFFER_SECONDS2 * 1e3}ms + 1 frame`
2535
2907
  );
2536
2908
  while (true) {
2537
2909
  const audioAhead = audio.isNoAudio() ? Infinity : audio.bufferAhead();
2538
2910
  const audioReady = audio.isNoAudio() || audioAhead >= READY_AUDIO_BUFFER_SECONDS2;
2539
2911
  const hasFrames = renderer.hasFrames();
2912
+ const nowMs = performance.now();
2913
+ if (hasFrames && firstFrameAtMs === 0) firstFrameAtMs = nowMs;
2540
2914
  if (audioReady && hasFrames) {
2541
2915
  chunkG4APZMCP_cjs.dbg.info(
2542
2916
  "cold-start",
2543
- `gate satisfied in ${(performance.now() - start).toFixed(0)}ms (audio=${(audioAhead * 1e3).toFixed(0)}ms, frames=${renderer.queueDepth()})`
2917
+ `gate satisfied in ${(nowMs - start).toFixed(0)}ms (audio=${(audioAhead * 1e3).toFixed(0)}ms, frames=${renderer.queueDepth()})`
2918
+ );
2919
+ return;
2920
+ }
2921
+ if (hasFrames && firstFrameAtMs > 0 && nowMs - firstFrameAtMs >= 500) {
2922
+ chunkG4APZMCP_cjs.dbg.info(
2923
+ "cold-start",
2924
+ `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)`
2544
2925
  );
2545
2926
  return;
2546
2927
  }
2547
- if ((performance.now() - start) / 1e3 > READY_TIMEOUT_SECONDS2) {
2928
+ if ((nowMs - start) / 1e3 > READY_TIMEOUT_SECONDS2) {
2548
2929
  chunkG4APZMCP_cjs.dbg.diag(
2549
2930
  "cold-start",
2550
- `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.`
2931
+ `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.`
2551
2932
  );
2552
2933
  return;
2553
2934
  }
@@ -2574,10 +2955,13 @@ async function createFallbackSession(ctx, target) {
2574
2955
  if (!audio.isPlaying()) {
2575
2956
  await waitForBuffer();
2576
2957
  await audio.start();
2958
+ target.dispatchEvent(new Event("play"));
2959
+ target.dispatchEvent(new Event("playing"));
2577
2960
  }
2578
2961
  },
2579
2962
  pause() {
2580
2963
  void audio.pause();
2964
+ target.dispatchEvent(new Event("pause"));
2581
2965
  },
2582
2966
  async seek(time) {
2583
2967
  await doSeek(time);
@@ -2596,6 +2980,9 @@ async function createFallbackSession(ctx, target) {
2596
2980
  try {
2597
2981
  delete target.currentTime;
2598
2982
  delete target.duration;
2983
+ delete target.paused;
2984
+ delete target.volume;
2985
+ delete target.muted;
2599
2986
  } catch {
2600
2987
  }
2601
2988
  },
@@ -2619,12 +3006,12 @@ var remuxPlugin = {
2619
3006
  var hybridPlugin = {
2620
3007
  name: "hybrid",
2621
3008
  canHandle: () => typeof VideoDecoder !== "undefined",
2622
- execute: (ctx, video) => createHybridSession(ctx, video)
3009
+ execute: (ctx, video, transport) => createHybridSession(ctx, video, transport)
2623
3010
  };
2624
3011
  var fallbackPlugin = {
2625
3012
  name: "fallback",
2626
3013
  canHandle: () => true,
2627
- execute: (ctx, video) => createFallbackSession(ctx, video)
3014
+ execute: (ctx, video, transport) => createFallbackSession(ctx, video, transport)
2628
3015
  };
2629
3016
  function registerBuiltins(registry) {
2630
3017
  registry.register(nativePlugin);
@@ -2680,7 +3067,8 @@ var SubtitleResourceBag = class {
2680
3067
  this.urls.clear();
2681
3068
  }
2682
3069
  };
2683
- async function attachSubtitleTracks(video, tracks, bag, onError) {
3070
+ async function attachSubtitleTracks(video, tracks, bag, onError, transport) {
3071
+ const doFetch = chunk6UUT4BEA_cjs.fetchWith(transport);
2684
3072
  for (const t of Array.from(video.querySelectorAll("track[data-avbridge]"))) {
2685
3073
  t.remove();
2686
3074
  }
@@ -2689,13 +3077,13 @@ async function attachSubtitleTracks(video, tracks, bag, onError) {
2689
3077
  try {
2690
3078
  let url = t.sidecarUrl;
2691
3079
  if (t.format === "srt") {
2692
- const res = await fetch(t.sidecarUrl);
3080
+ const res = await doFetch(t.sidecarUrl, transport?.requestInit);
2693
3081
  const text = await res.text();
2694
3082
  const vtt = srtToVtt(text);
2695
3083
  const blob = new Blob([vtt], { type: "text/vtt" });
2696
3084
  url = bag ? bag.createObjectURL(blob) : URL.createObjectURL(blob);
2697
3085
  } else if (t.format === "vtt") {
2698
- const res = await fetch(t.sidecarUrl);
3086
+ const res = await doFetch(t.sidecarUrl, transport?.requestInit);
2699
3087
  const text = await res.text();
2700
3088
  if (!isVtt(text)) {
2701
3089
  console.warn("[avbridge] subtitle missing WEBVTT header:", t.sidecarUrl);
@@ -2723,6 +3111,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
2723
3111
  constructor(options, registry) {
2724
3112
  this.options = options;
2725
3113
  this.registry = registry;
3114
+ const { requestInit, fetchFn } = options;
3115
+ if (requestInit || fetchFn) {
3116
+ this.transport = { requestInit, fetchFn };
3117
+ }
2726
3118
  }
2727
3119
  options;
2728
3120
  registry;
@@ -2738,11 +3130,27 @@ var UnifiedPlayer = class _UnifiedPlayer {
2738
3130
  lastProgressTime = 0;
2739
3131
  lastProgressPosition = -1;
2740
3132
  errorListener = null;
3133
+ // Bound so we can removeEventListener in destroy(); without this the
3134
+ // listener outlives the player and accumulates on elements that swap
3135
+ // source (e.g. <avbridge-video>).
3136
+ endedListener = null;
3137
+ // Background tab handling. userIntent is what the user last asked for
3138
+ // (play vs pause) — used to decide whether to auto-resume on visibility
3139
+ // return. autoPausedForVisibility tracks whether we paused because the
3140
+ // tab was hidden, so we don't resume playback the user deliberately
3141
+ // paused (e.g. via media keys while hidden).
3142
+ userIntent = "pause";
3143
+ autoPausedForVisibility = false;
3144
+ visibilityListener = null;
2741
3145
  // Serializes escalation / setStrategy calls
2742
3146
  switchingPromise = Promise.resolve();
2743
3147
  // Owns blob URLs created during sidecar discovery + SRT->VTT conversion.
2744
3148
  // Revoked at destroy() so repeated source swaps don't leak.
2745
3149
  subtitleResources = new SubtitleResourceBag();
3150
+ // Transport config extracted from CreatePlayerOptions. Threaded to probe,
3151
+ // subtitle fetches, and strategy session creators. Not stored on MediaContext
3152
+ // because it's runtime config, not media analysis.
3153
+ transport;
2746
3154
  static async create(options) {
2747
3155
  const registry = new PluginRegistry();
2748
3156
  registerBuiltins(registry);
@@ -2766,7 +3174,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
2766
3174
  const bootstrapStart = performance.now();
2767
3175
  try {
2768
3176
  chunkG4APZMCP_cjs.dbg.info("bootstrap", "start");
2769
- const ctx = await chunkG4APZMCP_cjs.dbg.timed("probe", "probe", 3e3, () => probe(this.options.source));
3177
+ const ctx = await chunkG4APZMCP_cjs.dbg.timed("probe", "probe", 3e3, () => probe(this.options.source, this.transport));
2770
3178
  chunkG4APZMCP_cjs.dbg.info(
2771
3179
  "probe",
2772
3180
  `container=${ctx.container} video=${ctx.videoTracks[0]?.codec ?? "-"} audio=${ctx.audioTracks[0]?.codec ?? "-"} probedBy=${ctx.probedBy}`
@@ -2814,7 +3222,8 @@ var UnifiedPlayer = class _UnifiedPlayer {
2814
3222
  this.subtitleResources,
2815
3223
  (err, track) => {
2816
3224
  console.warn(`[avbridge] subtitle ${track.id} failed: ${err.message}`);
2817
- }
3225
+ },
3226
+ this.transport
2818
3227
  );
2819
3228
  }
2820
3229
  this.emitter.emitSticky("tracks", {
@@ -2823,7 +3232,12 @@ var UnifiedPlayer = class _UnifiedPlayer {
2823
3232
  subtitle: ctx.subtitleTracks
2824
3233
  });
2825
3234
  this.startTimeupdateLoop();
2826
- this.options.target.addEventListener("ended", () => this.emitter.emit("ended", void 0));
3235
+ this.endedListener = () => this.emitter.emit("ended", void 0);
3236
+ this.options.target.addEventListener("ended", this.endedListener);
3237
+ if (this.options.backgroundBehavior !== "continue" && typeof document !== "undefined") {
3238
+ this.visibilityListener = () => this.onVisibilityChange();
3239
+ document.addEventListener("visibilitychange", this.visibilityListener);
3240
+ }
2827
3241
  this.emitter.emitSticky("ready", void 0);
2828
3242
  const bootstrapElapsed = performance.now() - bootstrapStart;
2829
3243
  chunkG4APZMCP_cjs.dbg.info("bootstrap", `ready in ${bootstrapElapsed.toFixed(0)}ms`);
@@ -2850,7 +3264,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
2850
3264
  throw new Error(`no plugin available for strategy "${strategy}"`);
2851
3265
  }
2852
3266
  try {
2853
- this.session = await plugin.execute(this.mediaContext, this.options.target);
3267
+ this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
2854
3268
  } catch (err) {
2855
3269
  const chain = this.classification?.fallbackChain;
2856
3270
  if (chain && chain.length > 0) {
@@ -2923,7 +3337,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
2923
3337
  continue;
2924
3338
  }
2925
3339
  try {
2926
- this.session = await plugin.execute(this.mediaContext, this.options.target);
3340
+ this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
2927
3341
  } catch (err) {
2928
3342
  const msg = err instanceof Error ? err.message : String(err);
2929
3343
  errors.push(`${nextStrategy}: ${msg}`);
@@ -2946,8 +3360,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
2946
3360
  }
2947
3361
  return;
2948
3362
  }
2949
- this.emitter.emit("error", new Error(
2950
- `all fallback strategies failed: ${errors.join("; ")}`
3363
+ this.emitter.emit("error", new chunk6UUT4BEA_cjs.AvbridgeError(
3364
+ chunk6UUT4BEA_cjs.ERR_ALL_STRATEGIES_EXHAUSTED,
3365
+ `All playback strategies failed: ${errors.join("; ")}`,
3366
+ "This file may require a codec or container that isn't available in this browser. Try the fallback strategy or check browser codec support."
2951
3367
  ));
2952
3368
  }
2953
3369
  // ── Stall supervision ─────────────────────────────────────────────────
@@ -2999,7 +3415,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
2999
3415
  // ── Public: manual strategy switch ────────────────────────────────────
3000
3416
  /** Manually switch to a different playback strategy. Preserves current position and play/pause state. Concurrent calls are serialized. */
3001
3417
  async setStrategy(strategy, reason) {
3002
- if (!this.mediaContext) throw new Error("player not ready");
3418
+ if (!this.mediaContext) throw new chunk6UUT4BEA_cjs.AvbridgeError(chunk6UUT4BEA_cjs.ERR_PLAYER_NOT_READY, "Player not ready \u2014 wait for the 'ready' event before calling playback methods.", "Await the 'ready' event or check player.readyState before calling play/pause/seek.");
3003
3419
  if (this.session?.strategy === strategy) return;
3004
3420
  this.switchingPromise = this.switchingPromise.then(
3005
3421
  () => this.doSetStrategy(strategy, reason)
@@ -3028,7 +3444,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
3028
3444
  }
3029
3445
  const plugin = this.registry.findFor(this.mediaContext, strategy);
3030
3446
  if (!plugin) throw new Error(`no plugin available for strategy "${strategy}"`);
3031
- this.session = await plugin.execute(this.mediaContext, this.options.target);
3447
+ this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
3032
3448
  this.emitter.emitSticky("strategy", {
3033
3449
  strategy,
3034
3450
  reason: switchReason
@@ -3062,26 +3478,56 @@ var UnifiedPlayer = class _UnifiedPlayer {
3062
3478
  }
3063
3479
  /** Begin or resume playback. Throws if the player is not ready. */
3064
3480
  async play() {
3065
- if (!this.session) throw new Error("player not ready");
3481
+ if (!this.session) throw new chunk6UUT4BEA_cjs.AvbridgeError(chunk6UUT4BEA_cjs.ERR_PLAYER_NOT_READY, "Player not ready \u2014 wait for the 'ready' event before calling playback methods.", "Await the 'ready' event or check player.readyState before calling play/pause/seek.");
3482
+ this.userIntent = "play";
3483
+ this.autoPausedForVisibility = false;
3066
3484
  await this.session.play();
3067
3485
  }
3068
3486
  /** Pause playback. No-op if the player is not ready or already paused. */
3069
3487
  pause() {
3488
+ this.userIntent = "pause";
3489
+ this.autoPausedForVisibility = false;
3070
3490
  this.session?.pause();
3071
3491
  }
3492
+ /**
3493
+ * Handle browser tab visibility changes. On hide: pause if the user
3494
+ * had been playing. On show: resume if we were the one who paused.
3495
+ * Skips when `backgroundBehavior: "continue"` is set (listener isn't
3496
+ * installed in that case).
3497
+ */
3498
+ onVisibilityChange() {
3499
+ if (!this.session) return;
3500
+ const action = decideVisibilityAction({
3501
+ hidden: document.hidden,
3502
+ userIntent: this.userIntent,
3503
+ sessionIsPlaying: !this.options.target.paused,
3504
+ autoPausedForVisibility: this.autoPausedForVisibility
3505
+ });
3506
+ if (action === "pause") {
3507
+ this.autoPausedForVisibility = true;
3508
+ chunkG4APZMCP_cjs.dbg.info("visibility", "tab hidden \u2014 auto-paused");
3509
+ this.session.pause();
3510
+ } else if (action === "resume") {
3511
+ this.autoPausedForVisibility = false;
3512
+ chunkG4APZMCP_cjs.dbg.info("visibility", "tab visible \u2014 auto-resuming");
3513
+ void this.session.play().catch((err) => {
3514
+ console.warn("[avbridge] auto-resume after tab return failed:", err);
3515
+ });
3516
+ }
3517
+ }
3072
3518
  /** Seek to the given time in seconds. Throws if the player is not ready. */
3073
3519
  async seek(time) {
3074
- if (!this.session) throw new Error("player not ready");
3520
+ if (!this.session) throw new chunk6UUT4BEA_cjs.AvbridgeError(chunk6UUT4BEA_cjs.ERR_PLAYER_NOT_READY, "Player not ready \u2014 wait for the 'ready' event before calling playback methods.", "Await the 'ready' event or check player.readyState before calling play/pause/seek.");
3075
3521
  await this.session.seek(time);
3076
3522
  }
3077
3523
  /** Switch the active audio track by track ID. Throws if the player is not ready. */
3078
3524
  async setAudioTrack(id) {
3079
- if (!this.session) throw new Error("player not ready");
3525
+ if (!this.session) throw new chunk6UUT4BEA_cjs.AvbridgeError(chunk6UUT4BEA_cjs.ERR_PLAYER_NOT_READY, "Player not ready \u2014 wait for the 'ready' event before calling playback methods.", "Await the 'ready' event or check player.readyState before calling play/pause/seek.");
3080
3526
  await this.session.setAudioTrack(id);
3081
3527
  }
3082
3528
  /** Switch the active subtitle track by track ID, or pass `null` to disable subtitles. */
3083
3529
  async setSubtitleTrack(id) {
3084
- if (!this.session) throw new Error("player not ready");
3530
+ if (!this.session) throw new chunk6UUT4BEA_cjs.AvbridgeError(chunk6UUT4BEA_cjs.ERR_PLAYER_NOT_READY, "Player not ready \u2014 wait for the 'ready' event before calling playback methods.", "Await the 'ready' event or check player.readyState before calling play/pause/seek.");
3085
3531
  await this.session.setSubtitleTrack(id);
3086
3532
  }
3087
3533
  /** Return a snapshot of current diagnostics: container, codecs, strategy, runtime stats, and strategy history. */
@@ -3109,6 +3555,14 @@ var UnifiedPlayer = class _UnifiedPlayer {
3109
3555
  this.timeupdateInterval = null;
3110
3556
  }
3111
3557
  this.clearSupervisor();
3558
+ if (this.endedListener) {
3559
+ this.options.target.removeEventListener("ended", this.endedListener);
3560
+ this.endedListener = null;
3561
+ }
3562
+ if (this.visibilityListener) {
3563
+ document.removeEventListener("visibilitychange", this.visibilityListener);
3564
+ this.visibilityListener = null;
3565
+ }
3112
3566
  if (this.session) {
3113
3567
  await this.session.destroy();
3114
3568
  this.session = null;
@@ -3120,14 +3574,24 @@ var UnifiedPlayer = class _UnifiedPlayer {
3120
3574
  async function createPlayer(options) {
3121
3575
  return UnifiedPlayer.create(options);
3122
3576
  }
3577
+ function decideVisibilityAction(state) {
3578
+ if (state.hidden) {
3579
+ if (state.userIntent === "play" && state.sessionIsPlaying) return "pause";
3580
+ return "noop";
3581
+ }
3582
+ if (state.autoPausedForVisibility) return "resume";
3583
+ return "noop";
3584
+ }
3123
3585
  function buildInitialDecision(initial, ctx) {
3124
3586
  const natural = classifyContext(ctx);
3125
3587
  const cls = strategyToClass(initial, natural);
3588
+ const inherited = natural.fallbackChain ?? defaultFallbackChain(initial);
3589
+ const fallbackChain = inherited.filter((s) => s !== initial);
3126
3590
  return {
3127
3591
  class: cls,
3128
3592
  strategy: initial,
3129
3593
  reason: `initial strategy "${initial}" requested via options.initialStrategy`,
3130
- fallbackChain: natural.fallbackChain ?? defaultFallbackChain(initial)
3594
+ fallbackChain
3131
3595
  };
3132
3596
  }
3133
3597
  function strategyToClass(strategy, natural) {
@@ -3156,6 +3620,10 @@ function defaultFallbackChain(strategy) {
3156
3620
  }
3157
3621
  }
3158
3622
 
3623
+ exports.FALLBACK_AUDIO_CODECS = FALLBACK_AUDIO_CODECS;
3624
+ exports.FALLBACK_VIDEO_CODECS = FALLBACK_VIDEO_CODECS;
3625
+ exports.NATIVE_AUDIO_CODECS = NATIVE_AUDIO_CODECS;
3626
+ exports.NATIVE_VIDEO_CODECS = NATIVE_VIDEO_CODECS;
3159
3627
  exports.UnifiedPlayer = UnifiedPlayer;
3160
3628
  exports.avbridgeAudioToMediabunny = avbridgeAudioToMediabunny;
3161
3629
  exports.avbridgeVideoToMediabunny = avbridgeVideoToMediabunny;
@@ -3164,5 +3632,5 @@ exports.classifyContext = classifyContext;
3164
3632
  exports.createPlayer = createPlayer;
3165
3633
  exports.probe = probe;
3166
3634
  exports.srtToVtt = srtToVtt;
3167
- //# sourceMappingURL=chunk-OE66B34H.cjs.map
3168
- //# sourceMappingURL=chunk-OE66B34H.cjs.map
3635
+ //# sourceMappingURL=chunk-7RGG6ME7.cjs.map
3636
+ //# sourceMappingURL=chunk-7RGG6ME7.cjs.map