avbridge 2.2.1 → 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 (119) hide show
  1. package/CHANGELOG.md +80 -1
  2. package/NOTICE.md +2 -2
  3. package/README.md +2 -3
  4. package/THIRD_PARTY_LICENSES.md +2 -2
  5. package/dist/avi-2JPBSHGA.js +183 -0
  6. package/dist/avi-2JPBSHGA.js.map +1 -0
  7. package/dist/avi-F6WZJK5T.cjs +185 -0
  8. package/dist/avi-F6WZJK5T.cjs.map +1 -0
  9. package/dist/{avi-GCGM7OJI.js → avi-NJXAXUXK.js} +9 -3
  10. package/dist/avi-NJXAXUXK.js.map +1 -0
  11. package/dist/{avi-6SJLWIWW.cjs → avi-W6L3BTWU.cjs} +10 -4
  12. package/dist/avi-W6L3BTWU.cjs.map +1 -0
  13. package/dist/{chunk-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-UF2N5L63.cjs → chunk-7RGG6ME7.cjs} +489 -76
  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-DMWARSEF.js → chunk-NV7ILLWH.js} +483 -74
  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 +558 -85
  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-U2NPmFvA.d.cts → player-B6WB74RD.d.cts} +62 -3
  54. package/dist/{player-U2NPmFvA.d.ts → player-B6WB74RD.d.ts} +62 -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 +104 -12
  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 +29 -4
  90. package/src/strategies/fallback/variant-routing.ts +7 -13
  91. package/src/strategies/fallback/video-renderer.ts +124 -32
  92. package/src/strategies/hybrid/decoder.ts +131 -20
  93. package/src/strategies/hybrid/index.ts +31 -5
  94. package/src/strategies/remux/mse.ts +12 -2
  95. package/src/subtitles/index.ts +7 -3
  96. package/src/types.ts +53 -1
  97. package/src/util/libav-http-reader.ts +5 -1
  98. package/src/util/source.ts +28 -8
  99. package/src/util/transport.ts +26 -0
  100. package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.mjs +1 -1
  101. package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.wasm +0 -0
  102. package/dist/avi-6SJLWIWW.cjs.map +0 -1
  103. package/dist/avi-GCGM7OJI.js.map +0 -1
  104. package/dist/chunk-DMWARSEF.js.map +0 -1
  105. package/dist/chunk-HZLQNKFN.cjs.map +0 -1
  106. package/dist/chunk-ILKDNBSE.js.map +0 -1
  107. package/dist/chunk-J5MCMN3S.js +0 -27
  108. package/dist/chunk-J5MCMN3S.js.map +0 -1
  109. package/dist/chunk-L4NPOJ36.cjs.map +0 -1
  110. package/dist/chunk-NZU7W256.cjs +0 -29
  111. package/dist/chunk-NZU7W256.cjs.map +0 -1
  112. package/dist/chunk-UF2N5L63.cjs.map +0 -1
  113. package/dist/chunk-WD2ZNQA7.js.map +0 -1
  114. package/dist/libav-http-reader-FPYDBMYK.cjs +0 -16
  115. package/dist/libav-http-reader-NQJVY273.js +0 -3
  116. package/dist/source-CN43EI7Z.cjs +0 -28
  117. package/dist/source-FFZ7TW2B.js +0 -3
  118. package/dist/variant-routing-GOHB2RZN.cjs +0 -12
  119. 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);
@@ -1159,6 +1199,10 @@ async function createRemuxSession(context, video) {
1159
1199
  }
1160
1200
 
1161
1201
  // src/strategies/fallback/video-renderer.ts
1202
+ function isDebug() {
1203
+ return typeof globalThis !== "undefined" && !!globalThis.AVBRIDGE_DEBUG;
1204
+ }
1205
+ var lastDebugLog = 0;
1162
1206
  var VideoRenderer = class {
1163
1207
  constructor(target, clock, fps = 30) {
1164
1208
  this.target = target;
@@ -1205,6 +1249,20 @@ var VideoRenderer = class {
1205
1249
  lastPaintWall = 0;
1206
1250
  /** Minimum ms between paints — paces video at roughly source fps. */
1207
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;
1208
1266
  /** Resolves once the first decoded frame has been enqueued. */
1209
1267
  firstFrameReady;
1210
1268
  resolveFirstFrame;
@@ -1253,21 +1311,81 @@ var VideoRenderer = class {
1253
1311
  }
1254
1312
  return;
1255
1313
  }
1256
- const wallNow = performance.now();
1257
- if (wallNow - this.lastPaintWall < this.paintIntervalMs - 2) return;
1258
- if (this.queue.length === 0) return;
1259
- if (this.framesPainted > 0 && this.framesPainted % 30 === 0) {
1260
- const audioNowUs = this.clock.now() * 1e6;
1261
- const headTs = this.queue[0].timestamp ?? 0;
1262
- const driftUs = headTs - audioNowUs;
1263
- if (driftUs < -15e4) {
1264
- this.queue.shift()?.close();
1265
- this.framesDroppedLate++;
1266
- if (this.queue.length === 0) return;
1267
- } else if (driftUs > 15e4) {
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
+ }
1268
1351
  return;
1269
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;
1270
1386
  }
1387
+ const wallNow = performance.now();
1388
+ if (wallNow - this.lastPaintWall < this.paintIntervalMs - 2) return;
1271
1389
  const frame = this.queue.shift();
1272
1390
  this.paint(frame);
1273
1391
  frame.close();
@@ -1289,8 +1407,13 @@ var VideoRenderer = class {
1289
1407
  }
1290
1408
  /** Discard all queued frames. Used by seek to drop stale buffers. */
1291
1409
  flush() {
1410
+ const count = this.queue.length;
1292
1411
  while (this.queue.length > 0) this.queue.shift()?.close();
1293
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
+ }
1294
1417
  }
1295
1418
  stats() {
1296
1419
  return {
@@ -1335,11 +1458,38 @@ var AudioOutput = class {
1335
1458
  pendingQueue = [];
1336
1459
  framesScheduled = 0;
1337
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;
1338
1465
  constructor() {
1339
1466
  this.ctx = new AudioContext();
1340
1467
  this.gain = this.ctx.createGain();
1341
1468
  this.gain.connect(this.ctx.destination);
1342
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
+ }
1343
1493
  /**
1344
1494
  * Switch into wall-clock fallback mode. Called by the decoder when no
1345
1495
  * audio decoder could be initialized for the source. Once set, this
@@ -1489,6 +1639,7 @@ var AudioOutput = class {
1489
1639
  }
1490
1640
  this.gain = this.ctx.createGain();
1491
1641
  this.gain.connect(this.ctx.destination);
1642
+ this.applyGain();
1492
1643
  this.pendingQueue = [];
1493
1644
  this.mediaTimeOfAnchor = newMediaTime;
1494
1645
  this.mediaTimeOfNext = newMediaTime;
@@ -1517,11 +1668,11 @@ var AudioOutput = class {
1517
1668
 
1518
1669
  // src/strategies/hybrid/decoder.ts
1519
1670
  async function startHybridDecoder(opts) {
1520
- const variant = chunkNZU7W256_cjs.pickLibavVariant(opts.context);
1671
+ const variant = chunkF3LQJKXK_cjs.pickLibavVariant(opts.context);
1521
1672
  const libav = await chunkG4APZMCP_cjs.loadLibav(variant);
1522
1673
  const bridge = await loadBridge();
1523
- const { prepareLibavInput } = await import('./libav-http-reader-FPYDBMYK.cjs');
1524
- 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);
1525
1676
  const readPkt = await libav.av_packet_alloc();
1526
1677
  const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
1527
1678
  const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
@@ -1592,6 +1743,56 @@ async function startHybridDecoder(opts) {
1592
1743
  });
1593
1744
  throw new Error("hybrid decoder: could not initialize any decoders");
1594
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
+ }
1595
1796
  let destroyed = false;
1596
1797
  let pumpToken = 0;
1597
1798
  let pumpRunning = null;
@@ -1619,8 +1820,15 @@ async function startHybridDecoder(opts) {
1619
1820
  if (myToken !== pumpToken || destroyed) return;
1620
1821
  const videoPackets = videoStream ? packets[videoStream.index] : void 0;
1621
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;
1622
1829
  if (videoDecoder && videoPackets && videoPackets.length > 0) {
1623
- for (const pkt of videoPackets) {
1830
+ const processed = await applyBSF(videoPackets);
1831
+ for (const pkt of processed) {
1624
1832
  if (myToken !== pumpToken || destroyed) return;
1625
1833
  sanitizePacketTimestamp(pkt, () => {
1626
1834
  const ts = syntheticVideoUs;
@@ -1640,9 +1848,6 @@ async function startHybridDecoder(opts) {
1640
1848
  }
1641
1849
  }
1642
1850
  }
1643
- if (audioDec && audioPackets && audioPackets.length > 0) {
1644
- await decodeAudioBatch(audioPackets, myToken);
1645
- }
1646
1851
  packetsRead += (videoPackets?.length ?? 0) + (audioPackets?.length ?? 0);
1647
1852
  while (!destroyed && myToken === pumpToken && (videoDecoder && videoDecoder.decodeQueueSize > 10 || opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
1648
1853
  await new Promise((r) => setTimeout(r, 50));
@@ -1665,20 +1870,43 @@ async function startHybridDecoder(opts) {
1665
1870
  }
1666
1871
  async function decodeAudioBatch(pkts, myToken, flush = false) {
1667
1872
  if (!audioDec || destroyed || myToken !== pumpToken) return;
1668
- let frames;
1669
- try {
1670
- frames = await libav.ff_decode_multi(
1671
- audioDec.c,
1672
- audioDec.pkt,
1673
- audioDec.frame,
1674
- pkts,
1675
- flush ? { fin: true, ignoreErrors: true } : { ignoreErrors: true }
1676
- );
1677
- } catch (err) {
1678
- console.error("[avbridge] hybrid audio decode failed:", err);
1679
- return;
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
+ }
1680
1907
  }
1681
1908
  if (myToken !== pumpToken || destroyed) return;
1909
+ const frames = allFrames;
1682
1910
  for (const f of frames) {
1683
1911
  if (myToken !== pumpToken || destroyed) return;
1684
1912
  sanitizeFrameTimestamp(
@@ -1715,6 +1943,14 @@ async function startHybridDecoder(opts) {
1715
1943
  await pumpRunning;
1716
1944
  } catch {
1717
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
+ }
1718
1954
  try {
1719
1955
  if (videoDecoder && videoDecoder.state !== "closed") videoDecoder.close();
1720
1956
  } catch {
@@ -1768,6 +2004,7 @@ async function startHybridDecoder(opts) {
1768
2004
  if (audioDec) await libav.avcodec_flush_buffers?.(audioDec.c);
1769
2005
  } catch {
1770
2006
  }
2007
+ await flushBSF();
1771
2008
  syntheticVideoUs = Math.round(timeSec * 1e6);
1772
2009
  syntheticAudioUs = Math.round(timeSec * 1e6);
1773
2010
  pumpRunning = pumpLoop(newToken).catch(
@@ -1781,6 +2018,7 @@ async function startHybridDecoder(opts) {
1781
2018
  videoFramesDecoded,
1782
2019
  videoChunksFed,
1783
2020
  audioFramesDecoded,
2021
+ bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
1784
2022
  videoDecodeQueueSize: videoDecoder?.decodeQueueSize ?? 0,
1785
2023
  // Confirmed transport info — see fallback decoder for the pattern.
1786
2024
  _transport: inputHandle.transport === "http-range" ? "http-range" : "memory",
@@ -1957,8 +2195,8 @@ async function loadBridge() {
1957
2195
  // src/strategies/hybrid/index.ts
1958
2196
  var READY_AUDIO_BUFFER_SECONDS = 0.3;
1959
2197
  var READY_TIMEOUT_SECONDS = 10;
1960
- async function createHybridSession(ctx, target) {
1961
- const { normalizeSource: normalizeSource2 } = await import('./source-CN43EI7Z.cjs');
2198
+ async function createHybridSession(ctx, target, transport) {
2199
+ const { normalizeSource: normalizeSource2 } = await import('./source-73CAH6HW.cjs');
1962
2200
  const source = await normalizeSource2(ctx.source);
1963
2201
  const fps = ctx.videoTracks[0]?.fps ?? 30;
1964
2202
  const audio = new AudioOutput();
@@ -1970,7 +2208,8 @@ async function createHybridSession(ctx, target) {
1970
2208
  filename: ctx.name ?? "input.bin",
1971
2209
  context: ctx,
1972
2210
  renderer,
1973
- audio
2211
+ audio,
2212
+ transport
1974
2213
  });
1975
2214
  } catch (err) {
1976
2215
  audio.destroy();
@@ -1988,6 +2227,22 @@ async function createHybridSession(ctx, target) {
1988
2227
  configurable: true,
1989
2228
  get: () => !audio.isPlaying()
1990
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
+ });
1991
2246
  if (ctx.duration && Number.isFinite(ctx.duration)) {
1992
2247
  Object.defineProperty(target, "duration", {
1993
2248
  configurable: true,
@@ -2027,10 +2282,13 @@ async function createHybridSession(ctx, target) {
2027
2282
  if (!audio.isPlaying()) {
2028
2283
  await waitForBuffer();
2029
2284
  await audio.start();
2285
+ target.dispatchEvent(new Event("play"));
2286
+ target.dispatchEvent(new Event("playing"));
2030
2287
  }
2031
2288
  },
2032
2289
  pause() {
2033
2290
  void audio.pause();
2291
+ target.dispatchEvent(new Event("pause"));
2034
2292
  },
2035
2293
  async seek(time) {
2036
2294
  await doSeek(time);
@@ -2053,6 +2311,8 @@ async function createHybridSession(ctx, target) {
2053
2311
  delete target.currentTime;
2054
2312
  delete target.duration;
2055
2313
  delete target.paused;
2314
+ delete target.volume;
2315
+ delete target.muted;
2056
2316
  } catch {
2057
2317
  }
2058
2318
  },
@@ -2064,11 +2324,11 @@ async function createHybridSession(ctx, target) {
2064
2324
 
2065
2325
  // src/strategies/fallback/decoder.ts
2066
2326
  async function startDecoder(opts) {
2067
- const variant = chunkNZU7W256_cjs.pickLibavVariant(opts.context);
2327
+ const variant = chunkF3LQJKXK_cjs.pickLibavVariant(opts.context);
2068
2328
  const libav = await chunkG4APZMCP_cjs.loadLibav(variant);
2069
2329
  const bridge = await loadBridge2();
2070
- const { prepareLibavInput } = await import('./libav-http-reader-FPYDBMYK.cjs');
2071
- 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);
2072
2332
  const readPkt = await libav.av_packet_alloc();
2073
2333
  const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
2074
2334
  const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
@@ -2124,6 +2384,56 @@ async function startDecoder(opts) {
2124
2384
  `fallback decoder: could not initialize any libav decoders (${codecs}).${hint}`
2125
2385
  );
2126
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
+ }
2127
2437
  let destroyed = false;
2128
2438
  let pumpToken = 0;
2129
2439
  let pumpRunning = null;
@@ -2159,7 +2469,8 @@ async function startDecoder(opts) {
2159
2469
  }
2160
2470
  if (myToken !== pumpToken || destroyed) return;
2161
2471
  if (videoDec && videoPackets && videoPackets.length > 0) {
2162
- await decodeVideoBatch(videoPackets, myToken);
2472
+ const processed = await applyBSF(videoPackets);
2473
+ await decodeVideoBatch(processed, myToken);
2163
2474
  }
2164
2475
  packetsRead += (videoPackets?.length ?? 0) + (audioPackets?.length ?? 0);
2165
2476
  if (videoFramesDecoded > 0) {
@@ -2305,6 +2616,14 @@ async function startDecoder(opts) {
2305
2616
  await pumpRunning;
2306
2617
  } catch {
2307
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
+ }
2308
2627
  try {
2309
2628
  if (videoDec) await libav.ff_free_decoder?.(videoDec.c, videoDec.pkt, videoDec.frame);
2310
2629
  } catch {
@@ -2356,6 +2675,7 @@ async function startDecoder(opts) {
2356
2675
  if (audioDec) await libav.avcodec_flush_buffers?.(audioDec.c);
2357
2676
  } catch {
2358
2677
  }
2678
+ await flushBSF();
2359
2679
  syntheticVideoUs = Math.round(timeSec * 1e6);
2360
2680
  syntheticAudioUs = Math.round(timeSec * 1e6);
2361
2681
  pumpRunning = pumpLoop(newToken).catch(
@@ -2368,6 +2688,7 @@ async function startDecoder(opts) {
2368
2688
  packetsRead,
2369
2689
  videoFramesDecoded,
2370
2690
  audioFramesDecoded,
2691
+ bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
2371
2692
  // Confirmed transport info: once prepareLibavInput returns
2372
2693
  // successfully, we *know* whether the source is http-range (probe
2373
2694
  // succeeded and returned 206) or in-memory blob. Diagnostics hoists
@@ -2523,8 +2844,8 @@ async function loadBridge2() {
2523
2844
  // src/strategies/fallback/index.ts
2524
2845
  var READY_AUDIO_BUFFER_SECONDS2 = 0.04;
2525
2846
  var READY_TIMEOUT_SECONDS2 = 3;
2526
- async function createFallbackSession(ctx, target) {
2527
- const { normalizeSource: normalizeSource2 } = await import('./source-CN43EI7Z.cjs');
2847
+ async function createFallbackSession(ctx, target, transport) {
2848
+ const { normalizeSource: normalizeSource2 } = await import('./source-73CAH6HW.cjs');
2528
2849
  const source = await normalizeSource2(ctx.source);
2529
2850
  const fps = ctx.videoTracks[0]?.fps ?? 30;
2530
2851
  const audio = new AudioOutput();
@@ -2536,7 +2857,8 @@ async function createFallbackSession(ctx, target) {
2536
2857
  filename: ctx.name ?? "input.bin",
2537
2858
  context: ctx,
2538
2859
  renderer,
2539
- audio
2860
+ audio,
2861
+ transport
2540
2862
  });
2541
2863
  } catch (err) {
2542
2864
  audio.destroy();
@@ -2554,6 +2876,22 @@ async function createFallbackSession(ctx, target) {
2554
2876
  configurable: true,
2555
2877
  get: () => !audio.isPlaying()
2556
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
+ });
2557
2895
  if (ctx.duration && Number.isFinite(ctx.duration)) {
2558
2896
  Object.defineProperty(target, "duration", {
2559
2897
  configurable: true,
@@ -2617,10 +2955,13 @@ async function createFallbackSession(ctx, target) {
2617
2955
  if (!audio.isPlaying()) {
2618
2956
  await waitForBuffer();
2619
2957
  await audio.start();
2958
+ target.dispatchEvent(new Event("play"));
2959
+ target.dispatchEvent(new Event("playing"));
2620
2960
  }
2621
2961
  },
2622
2962
  pause() {
2623
2963
  void audio.pause();
2964
+ target.dispatchEvent(new Event("pause"));
2624
2965
  },
2625
2966
  async seek(time) {
2626
2967
  await doSeek(time);
@@ -2640,6 +2981,8 @@ async function createFallbackSession(ctx, target) {
2640
2981
  delete target.currentTime;
2641
2982
  delete target.duration;
2642
2983
  delete target.paused;
2984
+ delete target.volume;
2985
+ delete target.muted;
2643
2986
  } catch {
2644
2987
  }
2645
2988
  },
@@ -2663,12 +3006,12 @@ var remuxPlugin = {
2663
3006
  var hybridPlugin = {
2664
3007
  name: "hybrid",
2665
3008
  canHandle: () => typeof VideoDecoder !== "undefined",
2666
- execute: (ctx, video) => createHybridSession(ctx, video)
3009
+ execute: (ctx, video, transport) => createHybridSession(ctx, video, transport)
2667
3010
  };
2668
3011
  var fallbackPlugin = {
2669
3012
  name: "fallback",
2670
3013
  canHandle: () => true,
2671
- execute: (ctx, video) => createFallbackSession(ctx, video)
3014
+ execute: (ctx, video, transport) => createFallbackSession(ctx, video, transport)
2672
3015
  };
2673
3016
  function registerBuiltins(registry) {
2674
3017
  registry.register(nativePlugin);
@@ -2724,7 +3067,8 @@ var SubtitleResourceBag = class {
2724
3067
  this.urls.clear();
2725
3068
  }
2726
3069
  };
2727
- async function attachSubtitleTracks(video, tracks, bag, onError) {
3070
+ async function attachSubtitleTracks(video, tracks, bag, onError, transport) {
3071
+ const doFetch = chunk6UUT4BEA_cjs.fetchWith(transport);
2728
3072
  for (const t of Array.from(video.querySelectorAll("track[data-avbridge]"))) {
2729
3073
  t.remove();
2730
3074
  }
@@ -2733,13 +3077,13 @@ async function attachSubtitleTracks(video, tracks, bag, onError) {
2733
3077
  try {
2734
3078
  let url = t.sidecarUrl;
2735
3079
  if (t.format === "srt") {
2736
- const res = await fetch(t.sidecarUrl);
3080
+ const res = await doFetch(t.sidecarUrl, transport?.requestInit);
2737
3081
  const text = await res.text();
2738
3082
  const vtt = srtToVtt(text);
2739
3083
  const blob = new Blob([vtt], { type: "text/vtt" });
2740
3084
  url = bag ? bag.createObjectURL(blob) : URL.createObjectURL(blob);
2741
3085
  } else if (t.format === "vtt") {
2742
- const res = await fetch(t.sidecarUrl);
3086
+ const res = await doFetch(t.sidecarUrl, transport?.requestInit);
2743
3087
  const text = await res.text();
2744
3088
  if (!isVtt(text)) {
2745
3089
  console.warn("[avbridge] subtitle missing WEBVTT header:", t.sidecarUrl);
@@ -2767,6 +3111,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
2767
3111
  constructor(options, registry) {
2768
3112
  this.options = options;
2769
3113
  this.registry = registry;
3114
+ const { requestInit, fetchFn } = options;
3115
+ if (requestInit || fetchFn) {
3116
+ this.transport = { requestInit, fetchFn };
3117
+ }
2770
3118
  }
2771
3119
  options;
2772
3120
  registry;
@@ -2786,11 +3134,23 @@ var UnifiedPlayer = class _UnifiedPlayer {
2786
3134
  // listener outlives the player and accumulates on elements that swap
2787
3135
  // source (e.g. <avbridge-video>).
2788
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;
2789
3145
  // Serializes escalation / setStrategy calls
2790
3146
  switchingPromise = Promise.resolve();
2791
3147
  // Owns blob URLs created during sidecar discovery + SRT->VTT conversion.
2792
3148
  // Revoked at destroy() so repeated source swaps don't leak.
2793
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;
2794
3154
  static async create(options) {
2795
3155
  const registry = new PluginRegistry();
2796
3156
  registerBuiltins(registry);
@@ -2814,7 +3174,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
2814
3174
  const bootstrapStart = performance.now();
2815
3175
  try {
2816
3176
  chunkG4APZMCP_cjs.dbg.info("bootstrap", "start");
2817
- 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));
2818
3178
  chunkG4APZMCP_cjs.dbg.info(
2819
3179
  "probe",
2820
3180
  `container=${ctx.container} video=${ctx.videoTracks[0]?.codec ?? "-"} audio=${ctx.audioTracks[0]?.codec ?? "-"} probedBy=${ctx.probedBy}`
@@ -2862,7 +3222,8 @@ var UnifiedPlayer = class _UnifiedPlayer {
2862
3222
  this.subtitleResources,
2863
3223
  (err, track) => {
2864
3224
  console.warn(`[avbridge] subtitle ${track.id} failed: ${err.message}`);
2865
- }
3225
+ },
3226
+ this.transport
2866
3227
  );
2867
3228
  }
2868
3229
  this.emitter.emitSticky("tracks", {
@@ -2873,6 +3234,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
2873
3234
  this.startTimeupdateLoop();
2874
3235
  this.endedListener = () => this.emitter.emit("ended", void 0);
2875
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
+ }
2876
3241
  this.emitter.emitSticky("ready", void 0);
2877
3242
  const bootstrapElapsed = performance.now() - bootstrapStart;
2878
3243
  chunkG4APZMCP_cjs.dbg.info("bootstrap", `ready in ${bootstrapElapsed.toFixed(0)}ms`);
@@ -2899,7 +3264,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
2899
3264
  throw new Error(`no plugin available for strategy "${strategy}"`);
2900
3265
  }
2901
3266
  try {
2902
- this.session = await plugin.execute(this.mediaContext, this.options.target);
3267
+ this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
2903
3268
  } catch (err) {
2904
3269
  const chain = this.classification?.fallbackChain;
2905
3270
  if (chain && chain.length > 0) {
@@ -2972,7 +3337,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
2972
3337
  continue;
2973
3338
  }
2974
3339
  try {
2975
- this.session = await plugin.execute(this.mediaContext, this.options.target);
3340
+ this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
2976
3341
  } catch (err) {
2977
3342
  const msg = err instanceof Error ? err.message : String(err);
2978
3343
  errors.push(`${nextStrategy}: ${msg}`);
@@ -2995,8 +3360,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
2995
3360
  }
2996
3361
  return;
2997
3362
  }
2998
- this.emitter.emit("error", new Error(
2999
- `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."
3000
3367
  ));
3001
3368
  }
3002
3369
  // ── Stall supervision ─────────────────────────────────────────────────
@@ -3048,7 +3415,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
3048
3415
  // ── Public: manual strategy switch ────────────────────────────────────
3049
3416
  /** Manually switch to a different playback strategy. Preserves current position and play/pause state. Concurrent calls are serialized. */
3050
3417
  async setStrategy(strategy, reason) {
3051
- 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.");
3052
3419
  if (this.session?.strategy === strategy) return;
3053
3420
  this.switchingPromise = this.switchingPromise.then(
3054
3421
  () => this.doSetStrategy(strategy, reason)
@@ -3077,7 +3444,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
3077
3444
  }
3078
3445
  const plugin = this.registry.findFor(this.mediaContext, strategy);
3079
3446
  if (!plugin) throw new Error(`no plugin available for strategy "${strategy}"`);
3080
- this.session = await plugin.execute(this.mediaContext, this.options.target);
3447
+ this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
3081
3448
  this.emitter.emitSticky("strategy", {
3082
3449
  strategy,
3083
3450
  reason: switchReason
@@ -3111,26 +3478,56 @@ var UnifiedPlayer = class _UnifiedPlayer {
3111
3478
  }
3112
3479
  /** Begin or resume playback. Throws if the player is not ready. */
3113
3480
  async play() {
3114
- 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;
3115
3484
  await this.session.play();
3116
3485
  }
3117
3486
  /** Pause playback. No-op if the player is not ready or already paused. */
3118
3487
  pause() {
3488
+ this.userIntent = "pause";
3489
+ this.autoPausedForVisibility = false;
3119
3490
  this.session?.pause();
3120
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
+ }
3121
3518
  /** Seek to the given time in seconds. Throws if the player is not ready. */
3122
3519
  async seek(time) {
3123
- 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.");
3124
3521
  await this.session.seek(time);
3125
3522
  }
3126
3523
  /** Switch the active audio track by track ID. Throws if the player is not ready. */
3127
3524
  async setAudioTrack(id) {
3128
- 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.");
3129
3526
  await this.session.setAudioTrack(id);
3130
3527
  }
3131
3528
  /** Switch the active subtitle track by track ID, or pass `null` to disable subtitles. */
3132
3529
  async setSubtitleTrack(id) {
3133
- 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.");
3134
3531
  await this.session.setSubtitleTrack(id);
3135
3532
  }
3136
3533
  /** Return a snapshot of current diagnostics: container, codecs, strategy, runtime stats, and strategy history. */
@@ -3162,6 +3559,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
3162
3559
  this.options.target.removeEventListener("ended", this.endedListener);
3163
3560
  this.endedListener = null;
3164
3561
  }
3562
+ if (this.visibilityListener) {
3563
+ document.removeEventListener("visibilitychange", this.visibilityListener);
3564
+ this.visibilityListener = null;
3565
+ }
3165
3566
  if (this.session) {
3166
3567
  await this.session.destroy();
3167
3568
  this.session = null;
@@ -3173,6 +3574,14 @@ var UnifiedPlayer = class _UnifiedPlayer {
3173
3574
  async function createPlayer(options) {
3174
3575
  return UnifiedPlayer.create(options);
3175
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
+ }
3176
3585
  function buildInitialDecision(initial, ctx) {
3177
3586
  const natural = classifyContext(ctx);
3178
3587
  const cls = strategyToClass(initial, natural);
@@ -3211,6 +3620,10 @@ function defaultFallbackChain(strategy) {
3211
3620
  }
3212
3621
  }
3213
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;
3214
3627
  exports.UnifiedPlayer = UnifiedPlayer;
3215
3628
  exports.avbridgeAudioToMediabunny = avbridgeAudioToMediabunny;
3216
3629
  exports.avbridgeVideoToMediabunny = avbridgeVideoToMediabunny;
@@ -3219,5 +3632,5 @@ exports.classifyContext = classifyContext;
3219
3632
  exports.createPlayer = createPlayer;
3220
3633
  exports.probe = probe;
3221
3634
  exports.srtToVtt = srtToVtt;
3222
- //# sourceMappingURL=chunk-UF2N5L63.cjs.map
3223
- //# sourceMappingURL=chunk-UF2N5L63.cjs.map
3635
+ //# sourceMappingURL=chunk-7RGG6ME7.cjs.map
3636
+ //# sourceMappingURL=chunk-7RGG6ME7.cjs.map