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,6 +1,6 @@
1
- import { normalizeSource, sniffNormalizedSource } from './chunk-ILKDNBSE.js';
1
+ import { normalizeSource, sniffNormalizedSource, AvbridgeError, ERR_PROBE_FAILED, ERR_PROBE_UNKNOWN_CONTAINER, ERR_LIBAV_NOT_REACHABLE, ERR_ALL_STRATEGIES_EXHAUSTED, ERR_PLAYER_NOT_READY, fetchWith, ERR_MSE_NOT_SUPPORTED, ERR_MSE_CODEC_NOT_SUPPORTED } from './chunk-2PGRFCWB.js';
2
2
  import { dbg, loadLibav } from './chunk-5DMTJVIU.js';
3
- import { pickLibavVariant } from './chunk-J5MCMN3S.js';
3
+ import { pickLibavVariant } from './chunk-5YAWWKA3.js';
4
4
 
5
5
  // src/probe/mediabunny.ts
6
6
  async function probeWithMediabunny(source, sniffedContainer) {
@@ -178,37 +178,58 @@ var MEDIABUNNY_CONTAINERS = /* @__PURE__ */ new Set([
178
178
  "adts",
179
179
  "mpegts"
180
180
  ]);
181
- async function probe(source) {
182
- const normalized = await normalizeSource(source);
181
+ async function probe(source, transport) {
182
+ const normalized = await normalizeSource(source, transport);
183
183
  const sniffed = await sniffNormalizedSource(normalized);
184
184
  if (MEDIABUNNY_CONTAINERS.has(sniffed)) {
185
185
  try {
186
- return await probeWithMediabunny(normalized, sniffed);
186
+ const result = await probeWithMediabunny(normalized, sniffed);
187
+ const hasUnknownCodec = result.videoTracks.some((t) => t.codec === "unknown") || result.audioTracks.some((t) => t.codec === "unknown");
188
+ if (hasUnknownCodec) {
189
+ try {
190
+ const { probeWithLibav } = await import('./avi-NJXAXUXK.js');
191
+ return await probeWithLibav(normalized, sniffed);
192
+ } catch {
193
+ return result;
194
+ }
195
+ }
196
+ return result;
187
197
  } catch (mediabunnyErr) {
188
198
  console.warn(
189
199
  `[avbridge] mediabunny rejected ${sniffed} file, falling back to libav:`,
190
200
  mediabunnyErr.message
191
201
  );
192
202
  try {
193
- const { probeWithLibav } = await import('./avi-GCGM7OJI.js');
203
+ const { probeWithLibav } = await import('./avi-NJXAXUXK.js');
194
204
  return await probeWithLibav(normalized, sniffed);
195
205
  } catch (libavErr) {
196
206
  const mbMsg = mediabunnyErr.message || String(mediabunnyErr);
197
207
  const lvMsg = libavErr instanceof Error ? libavErr.message : String(libavErr);
198
- throw new Error(
199
- `failed to probe ${sniffed} file. mediabunny: ${mbMsg}. libav fallback: ${lvMsg}.`
208
+ throw new AvbridgeError(
209
+ ERR_PROBE_FAILED,
210
+ `Failed to probe ${sniffed.toUpperCase()} file. mediabunny: ${mbMsg}. libav: ${lvMsg}.`,
211
+ "The file may be corrupt, truncated, or in an unsupported format. Enable AVBRIDGE_DEBUG for detailed logs."
200
212
  );
201
213
  }
202
214
  }
203
215
  }
204
216
  try {
205
- const { probeWithLibav } = await import('./avi-GCGM7OJI.js');
217
+ const { probeWithLibav } = await import('./avi-NJXAXUXK.js');
206
218
  return await probeWithLibav(normalized, sniffed);
207
219
  } catch (err) {
208
220
  const inner = err instanceof Error ? err.message : String(err);
209
221
  console.error("[avbridge] libav probe failed for", sniffed, "file:", err);
210
- throw new Error(
211
- sniffed === "unknown" ? `unable to probe source: container could not be identified, and the libav.js fallback also failed: ${inner || "(no message \u2014 see browser console for the original error)"}` : `${sniffed.toUpperCase()} files require libav.js, which failed to load: ${inner || "(no message \u2014 see browser console for the original error)"}`
222
+ if (sniffed === "unknown") {
223
+ throw new AvbridgeError(
224
+ ERR_PROBE_UNKNOWN_CONTAINER,
225
+ `Unable to probe source: container format could not be identified. libav fallback: ${inner || "(no details)"}`,
226
+ "The file may be corrupt or in a format avbridge doesn't recognize. Check the file plays in VLC or ffprobe."
227
+ );
228
+ }
229
+ throw new AvbridgeError(
230
+ ERR_LIBAV_NOT_REACHABLE,
231
+ `${sniffed.toUpperCase()} files require libav.js, which failed to load: ${inner || "(no details)"}`,
232
+ "Install @libav.js/variant-webcodecs, or check that AVBRIDGE_LIBAV_BASE points to the correct path."
212
233
  );
213
234
  }
214
235
  }
@@ -309,7 +330,9 @@ var FALLBACK_AUDIO_CODECS = /* @__PURE__ */ new Set([
309
330
  "ra_144",
310
331
  "ra_288",
311
332
  "sipr",
312
- "atrac3"
333
+ "atrac3",
334
+ "dts",
335
+ "truehd"
313
336
  ]);
314
337
  var NATIVE_CONTAINERS = /* @__PURE__ */ new Set([
315
338
  "mp4",
@@ -371,7 +394,16 @@ function classifyContext(ctx) {
371
394
  reason: `video codec "${video.codec}" has no browser decoder; WASM fallback required`
372
395
  };
373
396
  }
374
- if (audio && FALLBACK_AUDIO_CODECS.has(audio.codec)) {
397
+ const audioNeedsFallback = audio && (FALLBACK_AUDIO_CODECS.has(audio.codec) || !NATIVE_AUDIO_CODECS.has(audio.codec));
398
+ if (audioNeedsFallback) {
399
+ if (NATIVE_VIDEO_CODECS.has(video.codec) && webCodecsAvailable()) {
400
+ return {
401
+ class: "HYBRID_CANDIDATE",
402
+ strategy: "hybrid",
403
+ reason: `video "${video.codec}" is hardware-decodable via WebCodecs; audio "${audio.codec}" decoded in software by libav`,
404
+ fallbackChain: ["fallback"]
405
+ };
406
+ }
375
407
  return {
376
408
  class: "FALLBACK_REQUIRED",
377
409
  strategy: "fallback",
@@ -738,10 +770,18 @@ var MseSink = class {
738
770
  constructor(options) {
739
771
  this.options = options;
740
772
  if (typeof MediaSource === "undefined") {
741
- throw new Error("MSE not supported in this environment");
773
+ throw new AvbridgeError(
774
+ ERR_MSE_NOT_SUPPORTED,
775
+ "MediaSource Extensions (MSE) are not supported in this environment.",
776
+ "MSE is required for the remux strategy. Use a browser that supports MSE, or try the fallback strategy."
777
+ );
742
778
  }
743
779
  if (!MediaSource.isTypeSupported(options.mime)) {
744
- throw new Error(`MSE does not support MIME "${options.mime}" \u2014 cannot remux`);
780
+ throw new AvbridgeError(
781
+ ERR_MSE_CODEC_NOT_SUPPORTED,
782
+ `This browser's MSE does not support "${options.mime}".`,
783
+ "The codec combination can't be played via remux in this browser. The player will try the next strategy automatically."
784
+ );
745
785
  }
746
786
  this.mediaSource = new MediaSource();
747
787
  this.objectUrl = URL.createObjectURL(this.mediaSource);
@@ -1157,6 +1197,10 @@ async function createRemuxSession(context, video) {
1157
1197
  }
1158
1198
 
1159
1199
  // src/strategies/fallback/video-renderer.ts
1200
+ function isDebug() {
1201
+ return typeof globalThis !== "undefined" && !!globalThis.AVBRIDGE_DEBUG;
1202
+ }
1203
+ var lastDebugLog = 0;
1160
1204
  var VideoRenderer = class {
1161
1205
  constructor(target, clock, fps = 30) {
1162
1206
  this.target = target;
@@ -1203,6 +1247,20 @@ var VideoRenderer = class {
1203
1247
  lastPaintWall = 0;
1204
1248
  /** Minimum ms between paints — paces video at roughly source fps. */
1205
1249
  paintIntervalMs;
1250
+ /** Cumulative count of frames skipped because all PTS are in the future. */
1251
+ ticksWaiting = 0;
1252
+ /** Cumulative count of ticks where PTS mode painted a frame. */
1253
+ ticksPainted = 0;
1254
+ /**
1255
+ * Calibration offset (microseconds) between video PTS and audio clock.
1256
+ * Video PTS and AudioContext.currentTime can drift ~0.1% relative to
1257
+ * each other (different clock domains). Over 45 minutes that's 2.6s.
1258
+ * We measure the offset on the first painted frame and update it
1259
+ * periodically so the PTS comparison stays calibrated.
1260
+ */
1261
+ ptsCalibrationUs = 0;
1262
+ ptsCalibrated = false;
1263
+ lastCalibrationWall = 0;
1206
1264
  /** Resolves once the first decoded frame has been enqueued. */
1207
1265
  firstFrameReady;
1208
1266
  resolveFirstFrame;
@@ -1251,21 +1309,81 @@ var VideoRenderer = class {
1251
1309
  }
1252
1310
  return;
1253
1311
  }
1254
- const wallNow = performance.now();
1255
- if (wallNow - this.lastPaintWall < this.paintIntervalMs - 2) return;
1256
- if (this.queue.length === 0) return;
1257
- if (this.framesPainted > 0 && this.framesPainted % 30 === 0) {
1258
- const audioNowUs = this.clock.now() * 1e6;
1259
- const headTs = this.queue[0].timestamp ?? 0;
1260
- const driftUs = headTs - audioNowUs;
1261
- if (driftUs < -15e4) {
1262
- this.queue.shift()?.close();
1263
- this.framesDroppedLate++;
1264
- if (this.queue.length === 0) return;
1265
- } else if (driftUs > 15e4) {
1312
+ const rawAudioNowUs = this.clock.now() * 1e6;
1313
+ const headTs = this.queue[0].timestamp ?? 0;
1314
+ const hasPts = headTs > 0 || this.queue.length > 1;
1315
+ if (hasPts) {
1316
+ const wallNow2 = performance.now();
1317
+ if (!this.ptsCalibrated || wallNow2 - this.lastCalibrationWall > 1e4) {
1318
+ this.ptsCalibrationUs = headTs - rawAudioNowUs;
1319
+ this.ptsCalibrated = true;
1320
+ this.lastCalibrationWall = wallNow2;
1321
+ }
1322
+ const audioNowUs = rawAudioNowUs + this.ptsCalibrationUs;
1323
+ const frameDurationUs = this.paintIntervalMs * 1e3;
1324
+ const deadlineUs = audioNowUs + frameDurationUs;
1325
+ let bestIdx = -1;
1326
+ for (let i = 0; i < this.queue.length; i++) {
1327
+ const ts = this.queue[i].timestamp ?? 0;
1328
+ if (ts <= deadlineUs) {
1329
+ bestIdx = i;
1330
+ } else {
1331
+ break;
1332
+ }
1333
+ }
1334
+ if (bestIdx < 0) {
1335
+ this.ticksWaiting++;
1336
+ if (isDebug()) {
1337
+ const now = performance.now();
1338
+ if (now - lastDebugLog > 1e3) {
1339
+ const headPtsMs = (headTs / 1e3).toFixed(1);
1340
+ const audioMs = (audioNowUs / 1e3).toFixed(1);
1341
+ const rawDriftMs = ((headTs - rawAudioNowUs) / 1e3).toFixed(1);
1342
+ const calibMs = (this.ptsCalibrationUs / 1e3).toFixed(1);
1343
+ console.log(
1344
+ `[avbridge:renderer] WAIT q=${this.queue.length} headPTS=${headPtsMs}ms calibAudio=${audioMs}ms rawDrift=${rawDriftMs}ms calib=${calibMs}ms painted=${this.framesPainted} dropped=${this.framesDroppedLate}`
1345
+ );
1346
+ lastDebugLog = now;
1347
+ }
1348
+ }
1266
1349
  return;
1267
1350
  }
1351
+ const dropThresholdUs = audioNowUs - frameDurationUs * 2;
1352
+ let dropped = 0;
1353
+ while (bestIdx > 0) {
1354
+ const ts = this.queue[0].timestamp ?? 0;
1355
+ if (ts < dropThresholdUs) {
1356
+ this.queue.shift()?.close();
1357
+ this.framesDroppedLate++;
1358
+ bestIdx--;
1359
+ dropped++;
1360
+ } else {
1361
+ break;
1362
+ }
1363
+ }
1364
+ this.ticksPainted++;
1365
+ if (isDebug()) {
1366
+ const now = performance.now();
1367
+ if (now - lastDebugLog > 1e3) {
1368
+ const paintedTs = this.queue[0]?.timestamp ?? 0;
1369
+ const audioMs = (audioNowUs / 1e3).toFixed(1);
1370
+ const ptsMs = (paintedTs / 1e3).toFixed(1);
1371
+ const rawDriftMs = ((paintedTs - rawAudioNowUs) / 1e3).toFixed(1);
1372
+ const calibMs = (this.ptsCalibrationUs / 1e3).toFixed(1);
1373
+ console.log(
1374
+ `[avbridge:renderer] PAINT q=${this.queue.length} calibAudio=${audioMs}ms nextPTS=${ptsMs}ms rawDrift=${rawDriftMs}ms calib=${calibMs}ms dropped=${dropped} total_drops=${this.framesDroppedLate} painted=${this.framesPainted}`
1375
+ );
1376
+ lastDebugLog = now;
1377
+ }
1378
+ }
1379
+ const frame2 = this.queue.shift();
1380
+ this.paint(frame2);
1381
+ frame2.close();
1382
+ this.lastPaintWall = performance.now();
1383
+ return;
1268
1384
  }
1385
+ const wallNow = performance.now();
1386
+ if (wallNow - this.lastPaintWall < this.paintIntervalMs - 2) return;
1269
1387
  const frame = this.queue.shift();
1270
1388
  this.paint(frame);
1271
1389
  frame.close();
@@ -1287,8 +1405,13 @@ var VideoRenderer = class {
1287
1405
  }
1288
1406
  /** Discard all queued frames. Used by seek to drop stale buffers. */
1289
1407
  flush() {
1408
+ const count = this.queue.length;
1290
1409
  while (this.queue.length > 0) this.queue.shift()?.close();
1291
1410
  this.prerolled = false;
1411
+ this.ptsCalibrated = false;
1412
+ if (isDebug() && count > 0) {
1413
+ console.log(`[avbridge:renderer] FLUSH discarded=${count} painted=${this.framesPainted} drops=${this.framesDroppedLate}`);
1414
+ }
1292
1415
  }
1293
1416
  stats() {
1294
1417
  return {
@@ -1333,11 +1456,38 @@ var AudioOutput = class {
1333
1456
  pendingQueue = [];
1334
1457
  framesScheduled = 0;
1335
1458
  destroyed = false;
1459
+ /** User-set volume (0..1). Applied to the gain node. */
1460
+ _volume = 1;
1461
+ /** User-set muted flag. When true, gain is forced to 0. */
1462
+ _muted = false;
1336
1463
  constructor() {
1337
1464
  this.ctx = new AudioContext();
1338
1465
  this.gain = this.ctx.createGain();
1339
1466
  this.gain.connect(this.ctx.destination);
1340
1467
  }
1468
+ /** Set volume (0..1). Applied immediately to the gain node. */
1469
+ setVolume(v) {
1470
+ this._volume = Math.max(0, Math.min(1, v));
1471
+ this.applyGain();
1472
+ }
1473
+ getVolume() {
1474
+ return this._volume;
1475
+ }
1476
+ /** Set muted. When true, output is silenced regardless of volume. */
1477
+ setMuted(m) {
1478
+ this._muted = m;
1479
+ this.applyGain();
1480
+ }
1481
+ getMuted() {
1482
+ return this._muted;
1483
+ }
1484
+ applyGain() {
1485
+ const target = this._muted ? 0 : this._volume;
1486
+ try {
1487
+ this.gain.gain.value = target;
1488
+ } catch {
1489
+ }
1490
+ }
1341
1491
  /**
1342
1492
  * Switch into wall-clock fallback mode. Called by the decoder when no
1343
1493
  * audio decoder could be initialized for the source. Once set, this
@@ -1487,6 +1637,7 @@ var AudioOutput = class {
1487
1637
  }
1488
1638
  this.gain = this.ctx.createGain();
1489
1639
  this.gain.connect(this.ctx.destination);
1640
+ this.applyGain();
1490
1641
  this.pendingQueue = [];
1491
1642
  this.mediaTimeOfAnchor = newMediaTime;
1492
1643
  this.mediaTimeOfNext = newMediaTime;
@@ -1518,8 +1669,8 @@ async function startHybridDecoder(opts) {
1518
1669
  const variant = pickLibavVariant(opts.context);
1519
1670
  const libav = await loadLibav(variant);
1520
1671
  const bridge = await loadBridge();
1521
- const { prepareLibavInput } = await import('./libav-http-reader-NQJVY273.js');
1522
- const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source);
1672
+ const { prepareLibavInput } = await import('./libav-http-reader-WXG3Z7AI.js');
1673
+ const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source, opts.transport);
1523
1674
  const readPkt = await libav.av_packet_alloc();
1524
1675
  const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
1525
1676
  const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
@@ -1590,6 +1741,56 @@ async function startHybridDecoder(opts) {
1590
1741
  });
1591
1742
  throw new Error("hybrid decoder: could not initialize any decoders");
1592
1743
  }
1744
+ let bsfCtx = null;
1745
+ let bsfPkt = null;
1746
+ if (videoStream && opts.context.videoTracks[0]?.codec === "mpeg4") {
1747
+ try {
1748
+ bsfCtx = await libav.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
1749
+ if (bsfCtx != null && bsfCtx >= 0) {
1750
+ const parIn = await libav.AVBSFContext_par_in(bsfCtx);
1751
+ await libav.avcodec_parameters_copy(parIn, videoStream.codecpar);
1752
+ await libav.av_bsf_init(bsfCtx);
1753
+ bsfPkt = await libav.av_packet_alloc();
1754
+ dbg.info("bsf", "mpeg4_unpack_bframes BSF active (hybrid)");
1755
+ } else {
1756
+ console.warn("[avbridge] mpeg4_unpack_bframes BSF not available in hybrid decoder");
1757
+ bsfCtx = null;
1758
+ }
1759
+ } catch (err) {
1760
+ console.warn("[avbridge] hybrid: failed to init BSF:", err.message);
1761
+ bsfCtx = null;
1762
+ bsfPkt = null;
1763
+ }
1764
+ }
1765
+ async function applyBSF(packets) {
1766
+ if (!bsfCtx || !bsfPkt) return packets;
1767
+ const out = [];
1768
+ for (const pkt of packets) {
1769
+ await libav.ff_copyin_packet(bsfPkt, pkt);
1770
+ const sendErr = await libav.av_bsf_send_packet(bsfCtx, bsfPkt);
1771
+ if (sendErr < 0) {
1772
+ out.push(pkt);
1773
+ continue;
1774
+ }
1775
+ while (true) {
1776
+ const recvErr = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
1777
+ if (recvErr < 0) break;
1778
+ out.push(await libav.ff_copyout_packet(bsfPkt));
1779
+ }
1780
+ }
1781
+ return out;
1782
+ }
1783
+ async function flushBSF() {
1784
+ if (!bsfCtx || !bsfPkt) return;
1785
+ try {
1786
+ await libav.av_bsf_send_packet(bsfCtx, 0);
1787
+ while (true) {
1788
+ const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
1789
+ if (err < 0) break;
1790
+ }
1791
+ } catch {
1792
+ }
1793
+ }
1593
1794
  let destroyed = false;
1594
1795
  let pumpToken = 0;
1595
1796
  let pumpRunning = null;
@@ -1617,8 +1818,15 @@ async function startHybridDecoder(opts) {
1617
1818
  if (myToken !== pumpToken || destroyed) return;
1618
1819
  const videoPackets = videoStream ? packets[videoStream.index] : void 0;
1619
1820
  const audioPackets = audioStream ? packets[audioStream.index] : void 0;
1821
+ if (audioDec && audioPackets && audioPackets.length > 0) {
1822
+ await decodeAudioBatch(audioPackets, myToken);
1823
+ }
1824
+ if (myToken !== pumpToken || destroyed) return;
1825
+ await new Promise((r) => setTimeout(r, 0));
1826
+ if (myToken !== pumpToken || destroyed) return;
1620
1827
  if (videoDecoder && videoPackets && videoPackets.length > 0) {
1621
- for (const pkt of videoPackets) {
1828
+ const processed = await applyBSF(videoPackets);
1829
+ for (const pkt of processed) {
1622
1830
  if (myToken !== pumpToken || destroyed) return;
1623
1831
  sanitizePacketTimestamp(pkt, () => {
1624
1832
  const ts = syntheticVideoUs;
@@ -1638,9 +1846,6 @@ async function startHybridDecoder(opts) {
1638
1846
  }
1639
1847
  }
1640
1848
  }
1641
- if (audioDec && audioPackets && audioPackets.length > 0) {
1642
- await decodeAudioBatch(audioPackets, myToken);
1643
- }
1644
1849
  packetsRead += (videoPackets?.length ?? 0) + (audioPackets?.length ?? 0);
1645
1850
  while (!destroyed && myToken === pumpToken && (videoDecoder && videoDecoder.decodeQueueSize > 10 || opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
1646
1851
  await new Promise((r) => setTimeout(r, 50));
@@ -1663,20 +1868,43 @@ async function startHybridDecoder(opts) {
1663
1868
  }
1664
1869
  async function decodeAudioBatch(pkts, myToken, flush = false) {
1665
1870
  if (!audioDec || destroyed || myToken !== pumpToken) return;
1666
- let frames;
1667
- try {
1668
- frames = await libav.ff_decode_multi(
1669
- audioDec.c,
1670
- audioDec.pkt,
1671
- audioDec.frame,
1672
- pkts,
1673
- flush ? { fin: true, ignoreErrors: true } : { ignoreErrors: true }
1674
- );
1675
- } catch (err) {
1676
- console.error("[avbridge] hybrid audio decode failed:", err);
1677
- return;
1871
+ const AUDIO_SUB_BATCH = 4;
1872
+ let allFrames = [];
1873
+ for (let i = 0; i < pkts.length; i += AUDIO_SUB_BATCH) {
1874
+ if (myToken !== pumpToken || destroyed) return;
1875
+ const slice = pkts.slice(i, i + AUDIO_SUB_BATCH);
1876
+ const isLast = i + AUDIO_SUB_BATCH >= pkts.length;
1877
+ try {
1878
+ const frames2 = await libav.ff_decode_multi(
1879
+ audioDec.c,
1880
+ audioDec.pkt,
1881
+ audioDec.frame,
1882
+ slice,
1883
+ isLast && flush ? { fin: true, ignoreErrors: true } : { ignoreErrors: true }
1884
+ );
1885
+ allFrames = allFrames.concat(frames2);
1886
+ } catch (err) {
1887
+ console.error("[avbridge] hybrid audio decode failed:", err);
1888
+ return;
1889
+ }
1890
+ if (!isLast) await new Promise((r) => setTimeout(r, 0));
1891
+ }
1892
+ if (pkts.length === 0 && flush) {
1893
+ try {
1894
+ allFrames = await libav.ff_decode_multi(
1895
+ audioDec.c,
1896
+ audioDec.pkt,
1897
+ audioDec.frame,
1898
+ [],
1899
+ { fin: true, ignoreErrors: true }
1900
+ );
1901
+ } catch (err) {
1902
+ console.error("[avbridge] hybrid audio flush failed:", err);
1903
+ return;
1904
+ }
1678
1905
  }
1679
1906
  if (myToken !== pumpToken || destroyed) return;
1907
+ const frames = allFrames;
1680
1908
  for (const f of frames) {
1681
1909
  if (myToken !== pumpToken || destroyed) return;
1682
1910
  sanitizeFrameTimestamp(
@@ -1713,6 +1941,14 @@ async function startHybridDecoder(opts) {
1713
1941
  await pumpRunning;
1714
1942
  } catch {
1715
1943
  }
1944
+ try {
1945
+ if (bsfCtx) await libav.av_bsf_free(bsfCtx);
1946
+ } catch {
1947
+ }
1948
+ try {
1949
+ if (bsfPkt) await libav.av_packet_free?.(bsfPkt);
1950
+ } catch {
1951
+ }
1716
1952
  try {
1717
1953
  if (videoDecoder && videoDecoder.state !== "closed") videoDecoder.close();
1718
1954
  } catch {
@@ -1766,6 +2002,7 @@ async function startHybridDecoder(opts) {
1766
2002
  if (audioDec) await libav.avcodec_flush_buffers?.(audioDec.c);
1767
2003
  } catch {
1768
2004
  }
2005
+ await flushBSF();
1769
2006
  syntheticVideoUs = Math.round(timeSec * 1e6);
1770
2007
  syntheticAudioUs = Math.round(timeSec * 1e6);
1771
2008
  pumpRunning = pumpLoop(newToken).catch(
@@ -1779,6 +2016,7 @@ async function startHybridDecoder(opts) {
1779
2016
  videoFramesDecoded,
1780
2017
  videoChunksFed,
1781
2018
  audioFramesDecoded,
2019
+ bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
1782
2020
  videoDecodeQueueSize: videoDecoder?.decodeQueueSize ?? 0,
1783
2021
  // Confirmed transport info — see fallback decoder for the pattern.
1784
2022
  _transport: inputHandle.transport === "http-range" ? "http-range" : "memory",
@@ -1955,8 +2193,8 @@ async function loadBridge() {
1955
2193
  // src/strategies/hybrid/index.ts
1956
2194
  var READY_AUDIO_BUFFER_SECONDS = 0.3;
1957
2195
  var READY_TIMEOUT_SECONDS = 10;
1958
- async function createHybridSession(ctx, target) {
1959
- const { normalizeSource: normalizeSource2 } = await import('./source-FFZ7TW2B.js');
2196
+ async function createHybridSession(ctx, target, transport) {
2197
+ const { normalizeSource: normalizeSource2 } = await import('./source-QJR3OHTW.js');
1960
2198
  const source = await normalizeSource2(ctx.source);
1961
2199
  const fps = ctx.videoTracks[0]?.fps ?? 30;
1962
2200
  const audio = new AudioOutput();
@@ -1968,7 +2206,8 @@ async function createHybridSession(ctx, target) {
1968
2206
  filename: ctx.name ?? "input.bin",
1969
2207
  context: ctx,
1970
2208
  renderer,
1971
- audio
2209
+ audio,
2210
+ transport
1972
2211
  });
1973
2212
  } catch (err) {
1974
2213
  audio.destroy();
@@ -1986,6 +2225,22 @@ async function createHybridSession(ctx, target) {
1986
2225
  configurable: true,
1987
2226
  get: () => !audio.isPlaying()
1988
2227
  });
2228
+ Object.defineProperty(target, "volume", {
2229
+ configurable: true,
2230
+ get: () => audio.getVolume(),
2231
+ set: (v) => {
2232
+ audio.setVolume(v);
2233
+ target.dispatchEvent(new Event("volumechange"));
2234
+ }
2235
+ });
2236
+ Object.defineProperty(target, "muted", {
2237
+ configurable: true,
2238
+ get: () => audio.getMuted(),
2239
+ set: (m) => {
2240
+ audio.setMuted(m);
2241
+ target.dispatchEvent(new Event("volumechange"));
2242
+ }
2243
+ });
1989
2244
  if (ctx.duration && Number.isFinite(ctx.duration)) {
1990
2245
  Object.defineProperty(target, "duration", {
1991
2246
  configurable: true,
@@ -2025,10 +2280,13 @@ async function createHybridSession(ctx, target) {
2025
2280
  if (!audio.isPlaying()) {
2026
2281
  await waitForBuffer();
2027
2282
  await audio.start();
2283
+ target.dispatchEvent(new Event("play"));
2284
+ target.dispatchEvent(new Event("playing"));
2028
2285
  }
2029
2286
  },
2030
2287
  pause() {
2031
2288
  void audio.pause();
2289
+ target.dispatchEvent(new Event("pause"));
2032
2290
  },
2033
2291
  async seek(time) {
2034
2292
  await doSeek(time);
@@ -2051,6 +2309,8 @@ async function createHybridSession(ctx, target) {
2051
2309
  delete target.currentTime;
2052
2310
  delete target.duration;
2053
2311
  delete target.paused;
2312
+ delete target.volume;
2313
+ delete target.muted;
2054
2314
  } catch {
2055
2315
  }
2056
2316
  },
@@ -2065,8 +2325,8 @@ async function startDecoder(opts) {
2065
2325
  const variant = pickLibavVariant(opts.context);
2066
2326
  const libav = await loadLibav(variant);
2067
2327
  const bridge = await loadBridge2();
2068
- const { prepareLibavInput } = await import('./libav-http-reader-NQJVY273.js');
2069
- const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source);
2328
+ const { prepareLibavInput } = await import('./libav-http-reader-WXG3Z7AI.js');
2329
+ const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source, opts.transport);
2070
2330
  const readPkt = await libav.av_packet_alloc();
2071
2331
  const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
2072
2332
  const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
@@ -2122,6 +2382,56 @@ async function startDecoder(opts) {
2122
2382
  `fallback decoder: could not initialize any libav decoders (${codecs}).${hint}`
2123
2383
  );
2124
2384
  }
2385
+ let bsfCtx = null;
2386
+ let bsfPkt = null;
2387
+ if (videoStream && opts.context.videoTracks[0]?.codec === "mpeg4") {
2388
+ try {
2389
+ bsfCtx = await libav.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
2390
+ if (bsfCtx != null && bsfCtx >= 0) {
2391
+ const parIn = await libav.AVBSFContext_par_in(bsfCtx);
2392
+ await libav.avcodec_parameters_copy(parIn, videoStream.codecpar);
2393
+ await libav.av_bsf_init(bsfCtx);
2394
+ bsfPkt = await libav.av_packet_alloc();
2395
+ dbg.info("bsf", "mpeg4_unpack_bframes BSF active");
2396
+ } else {
2397
+ console.warn("[avbridge] mpeg4_unpack_bframes BSF not available \u2014 decoding without it");
2398
+ bsfCtx = null;
2399
+ }
2400
+ } catch (err) {
2401
+ console.warn("[avbridge] failed to init mpeg4_unpack_bframes BSF:", err.message);
2402
+ bsfCtx = null;
2403
+ bsfPkt = null;
2404
+ }
2405
+ }
2406
+ async function applyBSF(packets) {
2407
+ if (!bsfCtx || !bsfPkt) return packets;
2408
+ const out = [];
2409
+ for (const pkt of packets) {
2410
+ await libav.ff_copyin_packet(bsfPkt, pkt);
2411
+ const sendErr = await libav.av_bsf_send_packet(bsfCtx, bsfPkt);
2412
+ if (sendErr < 0) {
2413
+ out.push(pkt);
2414
+ continue;
2415
+ }
2416
+ while (true) {
2417
+ const recvErr = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
2418
+ if (recvErr < 0) break;
2419
+ out.push(await libav.ff_copyout_packet(bsfPkt));
2420
+ }
2421
+ }
2422
+ return out;
2423
+ }
2424
+ async function flushBSF() {
2425
+ if (!bsfCtx || !bsfPkt) return;
2426
+ try {
2427
+ await libav.av_bsf_send_packet(bsfCtx, 0);
2428
+ while (true) {
2429
+ const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
2430
+ if (err < 0) break;
2431
+ }
2432
+ } catch {
2433
+ }
2434
+ }
2125
2435
  let destroyed = false;
2126
2436
  let pumpToken = 0;
2127
2437
  let pumpRunning = null;
@@ -2157,7 +2467,8 @@ async function startDecoder(opts) {
2157
2467
  }
2158
2468
  if (myToken !== pumpToken || destroyed) return;
2159
2469
  if (videoDec && videoPackets && videoPackets.length > 0) {
2160
- await decodeVideoBatch(videoPackets, myToken);
2470
+ const processed = await applyBSF(videoPackets);
2471
+ await decodeVideoBatch(processed, myToken);
2161
2472
  }
2162
2473
  packetsRead += (videoPackets?.length ?? 0) + (audioPackets?.length ?? 0);
2163
2474
  if (videoFramesDecoded > 0) {
@@ -2303,6 +2614,14 @@ async function startDecoder(opts) {
2303
2614
  await pumpRunning;
2304
2615
  } catch {
2305
2616
  }
2617
+ try {
2618
+ if (bsfCtx) await libav.av_bsf_free(bsfCtx);
2619
+ } catch {
2620
+ }
2621
+ try {
2622
+ if (bsfPkt) await libav.av_packet_free?.(bsfPkt);
2623
+ } catch {
2624
+ }
2306
2625
  try {
2307
2626
  if (videoDec) await libav.ff_free_decoder?.(videoDec.c, videoDec.pkt, videoDec.frame);
2308
2627
  } catch {
@@ -2354,6 +2673,7 @@ async function startDecoder(opts) {
2354
2673
  if (audioDec) await libav.avcodec_flush_buffers?.(audioDec.c);
2355
2674
  } catch {
2356
2675
  }
2676
+ await flushBSF();
2357
2677
  syntheticVideoUs = Math.round(timeSec * 1e6);
2358
2678
  syntheticAudioUs = Math.round(timeSec * 1e6);
2359
2679
  pumpRunning = pumpLoop(newToken).catch(
@@ -2366,6 +2686,7 @@ async function startDecoder(opts) {
2366
2686
  packetsRead,
2367
2687
  videoFramesDecoded,
2368
2688
  audioFramesDecoded,
2689
+ bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
2369
2690
  // Confirmed transport info: once prepareLibavInput returns
2370
2691
  // successfully, we *know* whether the source is http-range (probe
2371
2692
  // succeeded and returned 206) or in-memory blob. Diagnostics hoists
@@ -2521,8 +2842,8 @@ async function loadBridge2() {
2521
2842
  // src/strategies/fallback/index.ts
2522
2843
  var READY_AUDIO_BUFFER_SECONDS2 = 0.04;
2523
2844
  var READY_TIMEOUT_SECONDS2 = 3;
2524
- async function createFallbackSession(ctx, target) {
2525
- const { normalizeSource: normalizeSource2 } = await import('./source-FFZ7TW2B.js');
2845
+ async function createFallbackSession(ctx, target, transport) {
2846
+ const { normalizeSource: normalizeSource2 } = await import('./source-QJR3OHTW.js');
2526
2847
  const source = await normalizeSource2(ctx.source);
2527
2848
  const fps = ctx.videoTracks[0]?.fps ?? 30;
2528
2849
  const audio = new AudioOutput();
@@ -2534,7 +2855,8 @@ async function createFallbackSession(ctx, target) {
2534
2855
  filename: ctx.name ?? "input.bin",
2535
2856
  context: ctx,
2536
2857
  renderer,
2537
- audio
2858
+ audio,
2859
+ transport
2538
2860
  });
2539
2861
  } catch (err) {
2540
2862
  audio.destroy();
@@ -2552,6 +2874,22 @@ async function createFallbackSession(ctx, target) {
2552
2874
  configurable: true,
2553
2875
  get: () => !audio.isPlaying()
2554
2876
  });
2877
+ Object.defineProperty(target, "volume", {
2878
+ configurable: true,
2879
+ get: () => audio.getVolume(),
2880
+ set: (v) => {
2881
+ audio.setVolume(v);
2882
+ target.dispatchEvent(new Event("volumechange"));
2883
+ }
2884
+ });
2885
+ Object.defineProperty(target, "muted", {
2886
+ configurable: true,
2887
+ get: () => audio.getMuted(),
2888
+ set: (m) => {
2889
+ audio.setMuted(m);
2890
+ target.dispatchEvent(new Event("volumechange"));
2891
+ }
2892
+ });
2555
2893
  if (ctx.duration && Number.isFinite(ctx.duration)) {
2556
2894
  Object.defineProperty(target, "duration", {
2557
2895
  configurable: true,
@@ -2615,10 +2953,13 @@ async function createFallbackSession(ctx, target) {
2615
2953
  if (!audio.isPlaying()) {
2616
2954
  await waitForBuffer();
2617
2955
  await audio.start();
2956
+ target.dispatchEvent(new Event("play"));
2957
+ target.dispatchEvent(new Event("playing"));
2618
2958
  }
2619
2959
  },
2620
2960
  pause() {
2621
2961
  void audio.pause();
2962
+ target.dispatchEvent(new Event("pause"));
2622
2963
  },
2623
2964
  async seek(time) {
2624
2965
  await doSeek(time);
@@ -2638,6 +2979,8 @@ async function createFallbackSession(ctx, target) {
2638
2979
  delete target.currentTime;
2639
2980
  delete target.duration;
2640
2981
  delete target.paused;
2982
+ delete target.volume;
2983
+ delete target.muted;
2641
2984
  } catch {
2642
2985
  }
2643
2986
  },
@@ -2661,12 +3004,12 @@ var remuxPlugin = {
2661
3004
  var hybridPlugin = {
2662
3005
  name: "hybrid",
2663
3006
  canHandle: () => typeof VideoDecoder !== "undefined",
2664
- execute: (ctx, video) => createHybridSession(ctx, video)
3007
+ execute: (ctx, video, transport) => createHybridSession(ctx, video, transport)
2665
3008
  };
2666
3009
  var fallbackPlugin = {
2667
3010
  name: "fallback",
2668
3011
  canHandle: () => true,
2669
- execute: (ctx, video) => createFallbackSession(ctx, video)
3012
+ execute: (ctx, video, transport) => createFallbackSession(ctx, video, transport)
2670
3013
  };
2671
3014
  function registerBuiltins(registry) {
2672
3015
  registry.register(nativePlugin);
@@ -2722,7 +3065,8 @@ var SubtitleResourceBag = class {
2722
3065
  this.urls.clear();
2723
3066
  }
2724
3067
  };
2725
- async function attachSubtitleTracks(video, tracks, bag, onError) {
3068
+ async function attachSubtitleTracks(video, tracks, bag, onError, transport) {
3069
+ const doFetch = fetchWith(transport);
2726
3070
  for (const t of Array.from(video.querySelectorAll("track[data-avbridge]"))) {
2727
3071
  t.remove();
2728
3072
  }
@@ -2731,13 +3075,13 @@ async function attachSubtitleTracks(video, tracks, bag, onError) {
2731
3075
  try {
2732
3076
  let url = t.sidecarUrl;
2733
3077
  if (t.format === "srt") {
2734
- const res = await fetch(t.sidecarUrl);
3078
+ const res = await doFetch(t.sidecarUrl, transport?.requestInit);
2735
3079
  const text = await res.text();
2736
3080
  const vtt = srtToVtt(text);
2737
3081
  const blob = new Blob([vtt], { type: "text/vtt" });
2738
3082
  url = bag ? bag.createObjectURL(blob) : URL.createObjectURL(blob);
2739
3083
  } else if (t.format === "vtt") {
2740
- const res = await fetch(t.sidecarUrl);
3084
+ const res = await doFetch(t.sidecarUrl, transport?.requestInit);
2741
3085
  const text = await res.text();
2742
3086
  if (!isVtt(text)) {
2743
3087
  console.warn("[avbridge] subtitle missing WEBVTT header:", t.sidecarUrl);
@@ -2765,6 +3109,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
2765
3109
  constructor(options, registry) {
2766
3110
  this.options = options;
2767
3111
  this.registry = registry;
3112
+ const { requestInit, fetchFn } = options;
3113
+ if (requestInit || fetchFn) {
3114
+ this.transport = { requestInit, fetchFn };
3115
+ }
2768
3116
  }
2769
3117
  options;
2770
3118
  registry;
@@ -2784,11 +3132,23 @@ var UnifiedPlayer = class _UnifiedPlayer {
2784
3132
  // listener outlives the player and accumulates on elements that swap
2785
3133
  // source (e.g. <avbridge-video>).
2786
3134
  endedListener = null;
3135
+ // Background tab handling. userIntent is what the user last asked for
3136
+ // (play vs pause) — used to decide whether to auto-resume on visibility
3137
+ // return. autoPausedForVisibility tracks whether we paused because the
3138
+ // tab was hidden, so we don't resume playback the user deliberately
3139
+ // paused (e.g. via media keys while hidden).
3140
+ userIntent = "pause";
3141
+ autoPausedForVisibility = false;
3142
+ visibilityListener = null;
2787
3143
  // Serializes escalation / setStrategy calls
2788
3144
  switchingPromise = Promise.resolve();
2789
3145
  // Owns blob URLs created during sidecar discovery + SRT->VTT conversion.
2790
3146
  // Revoked at destroy() so repeated source swaps don't leak.
2791
3147
  subtitleResources = new SubtitleResourceBag();
3148
+ // Transport config extracted from CreatePlayerOptions. Threaded to probe,
3149
+ // subtitle fetches, and strategy session creators. Not stored on MediaContext
3150
+ // because it's runtime config, not media analysis.
3151
+ transport;
2792
3152
  static async create(options) {
2793
3153
  const registry = new PluginRegistry();
2794
3154
  registerBuiltins(registry);
@@ -2812,7 +3172,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
2812
3172
  const bootstrapStart = performance.now();
2813
3173
  try {
2814
3174
  dbg.info("bootstrap", "start");
2815
- const ctx = await dbg.timed("probe", "probe", 3e3, () => probe(this.options.source));
3175
+ const ctx = await dbg.timed("probe", "probe", 3e3, () => probe(this.options.source, this.transport));
2816
3176
  dbg.info(
2817
3177
  "probe",
2818
3178
  `container=${ctx.container} video=${ctx.videoTracks[0]?.codec ?? "-"} audio=${ctx.audioTracks[0]?.codec ?? "-"} probedBy=${ctx.probedBy}`
@@ -2860,7 +3220,8 @@ var UnifiedPlayer = class _UnifiedPlayer {
2860
3220
  this.subtitleResources,
2861
3221
  (err, track) => {
2862
3222
  console.warn(`[avbridge] subtitle ${track.id} failed: ${err.message}`);
2863
- }
3223
+ },
3224
+ this.transport
2864
3225
  );
2865
3226
  }
2866
3227
  this.emitter.emitSticky("tracks", {
@@ -2871,6 +3232,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
2871
3232
  this.startTimeupdateLoop();
2872
3233
  this.endedListener = () => this.emitter.emit("ended", void 0);
2873
3234
  this.options.target.addEventListener("ended", this.endedListener);
3235
+ if (this.options.backgroundBehavior !== "continue" && typeof document !== "undefined") {
3236
+ this.visibilityListener = () => this.onVisibilityChange();
3237
+ document.addEventListener("visibilitychange", this.visibilityListener);
3238
+ }
2874
3239
  this.emitter.emitSticky("ready", void 0);
2875
3240
  const bootstrapElapsed = performance.now() - bootstrapStart;
2876
3241
  dbg.info("bootstrap", `ready in ${bootstrapElapsed.toFixed(0)}ms`);
@@ -2897,7 +3262,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
2897
3262
  throw new Error(`no plugin available for strategy "${strategy}"`);
2898
3263
  }
2899
3264
  try {
2900
- this.session = await plugin.execute(this.mediaContext, this.options.target);
3265
+ this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
2901
3266
  } catch (err) {
2902
3267
  const chain = this.classification?.fallbackChain;
2903
3268
  if (chain && chain.length > 0) {
@@ -2970,7 +3335,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
2970
3335
  continue;
2971
3336
  }
2972
3337
  try {
2973
- this.session = await plugin.execute(this.mediaContext, this.options.target);
3338
+ this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
2974
3339
  } catch (err) {
2975
3340
  const msg = err instanceof Error ? err.message : String(err);
2976
3341
  errors.push(`${nextStrategy}: ${msg}`);
@@ -2993,8 +3358,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
2993
3358
  }
2994
3359
  return;
2995
3360
  }
2996
- this.emitter.emit("error", new Error(
2997
- `all fallback strategies failed: ${errors.join("; ")}`
3361
+ this.emitter.emit("error", new AvbridgeError(
3362
+ ERR_ALL_STRATEGIES_EXHAUSTED,
3363
+ `All playback strategies failed: ${errors.join("; ")}`,
3364
+ "This file may require a codec or container that isn't available in this browser. Try the fallback strategy or check browser codec support."
2998
3365
  ));
2999
3366
  }
3000
3367
  // ── Stall supervision ─────────────────────────────────────────────────
@@ -3046,7 +3413,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
3046
3413
  // ── Public: manual strategy switch ────────────────────────────────────
3047
3414
  /** Manually switch to a different playback strategy. Preserves current position and play/pause state. Concurrent calls are serialized. */
3048
3415
  async setStrategy(strategy, reason) {
3049
- if (!this.mediaContext) throw new Error("player not ready");
3416
+ if (!this.mediaContext) throw new AvbridgeError(ERR_PLAYER_NOT_READY, "Player not ready \u2014 wait for the 'ready' event before calling playback methods.", "Await the 'ready' event or check player.readyState before calling play/pause/seek.");
3050
3417
  if (this.session?.strategy === strategy) return;
3051
3418
  this.switchingPromise = this.switchingPromise.then(
3052
3419
  () => this.doSetStrategy(strategy, reason)
@@ -3075,7 +3442,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
3075
3442
  }
3076
3443
  const plugin = this.registry.findFor(this.mediaContext, strategy);
3077
3444
  if (!plugin) throw new Error(`no plugin available for strategy "${strategy}"`);
3078
- this.session = await plugin.execute(this.mediaContext, this.options.target);
3445
+ this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
3079
3446
  this.emitter.emitSticky("strategy", {
3080
3447
  strategy,
3081
3448
  reason: switchReason
@@ -3109,26 +3476,56 @@ var UnifiedPlayer = class _UnifiedPlayer {
3109
3476
  }
3110
3477
  /** Begin or resume playback. Throws if the player is not ready. */
3111
3478
  async play() {
3112
- if (!this.session) throw new Error("player not ready");
3479
+ if (!this.session) throw new AvbridgeError(ERR_PLAYER_NOT_READY, "Player not ready \u2014 wait for the 'ready' event before calling playback methods.", "Await the 'ready' event or check player.readyState before calling play/pause/seek.");
3480
+ this.userIntent = "play";
3481
+ this.autoPausedForVisibility = false;
3113
3482
  await this.session.play();
3114
3483
  }
3115
3484
  /** Pause playback. No-op if the player is not ready or already paused. */
3116
3485
  pause() {
3486
+ this.userIntent = "pause";
3487
+ this.autoPausedForVisibility = false;
3117
3488
  this.session?.pause();
3118
3489
  }
3490
+ /**
3491
+ * Handle browser tab visibility changes. On hide: pause if the user
3492
+ * had been playing. On show: resume if we were the one who paused.
3493
+ * Skips when `backgroundBehavior: "continue"` is set (listener isn't
3494
+ * installed in that case).
3495
+ */
3496
+ onVisibilityChange() {
3497
+ if (!this.session) return;
3498
+ const action = decideVisibilityAction({
3499
+ hidden: document.hidden,
3500
+ userIntent: this.userIntent,
3501
+ sessionIsPlaying: !this.options.target.paused,
3502
+ autoPausedForVisibility: this.autoPausedForVisibility
3503
+ });
3504
+ if (action === "pause") {
3505
+ this.autoPausedForVisibility = true;
3506
+ dbg.info("visibility", "tab hidden \u2014 auto-paused");
3507
+ this.session.pause();
3508
+ } else if (action === "resume") {
3509
+ this.autoPausedForVisibility = false;
3510
+ dbg.info("visibility", "tab visible \u2014 auto-resuming");
3511
+ void this.session.play().catch((err) => {
3512
+ console.warn("[avbridge] auto-resume after tab return failed:", err);
3513
+ });
3514
+ }
3515
+ }
3119
3516
  /** Seek to the given time in seconds. Throws if the player is not ready. */
3120
3517
  async seek(time) {
3121
- if (!this.session) throw new Error("player not ready");
3518
+ if (!this.session) throw new AvbridgeError(ERR_PLAYER_NOT_READY, "Player not ready \u2014 wait for the 'ready' event before calling playback methods.", "Await the 'ready' event or check player.readyState before calling play/pause/seek.");
3122
3519
  await this.session.seek(time);
3123
3520
  }
3124
3521
  /** Switch the active audio track by track ID. Throws if the player is not ready. */
3125
3522
  async setAudioTrack(id) {
3126
- if (!this.session) throw new Error("player not ready");
3523
+ if (!this.session) throw new AvbridgeError(ERR_PLAYER_NOT_READY, "Player not ready \u2014 wait for the 'ready' event before calling playback methods.", "Await the 'ready' event or check player.readyState before calling play/pause/seek.");
3127
3524
  await this.session.setAudioTrack(id);
3128
3525
  }
3129
3526
  /** Switch the active subtitle track by track ID, or pass `null` to disable subtitles. */
3130
3527
  async setSubtitleTrack(id) {
3131
- if (!this.session) throw new Error("player not ready");
3528
+ if (!this.session) throw new AvbridgeError(ERR_PLAYER_NOT_READY, "Player not ready \u2014 wait for the 'ready' event before calling playback methods.", "Await the 'ready' event or check player.readyState before calling play/pause/seek.");
3132
3529
  await this.session.setSubtitleTrack(id);
3133
3530
  }
3134
3531
  /** Return a snapshot of current diagnostics: container, codecs, strategy, runtime stats, and strategy history. */
@@ -3160,6 +3557,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
3160
3557
  this.options.target.removeEventListener("ended", this.endedListener);
3161
3558
  this.endedListener = null;
3162
3559
  }
3560
+ if (this.visibilityListener) {
3561
+ document.removeEventListener("visibilitychange", this.visibilityListener);
3562
+ this.visibilityListener = null;
3563
+ }
3163
3564
  if (this.session) {
3164
3565
  await this.session.destroy();
3165
3566
  this.session = null;
@@ -3171,6 +3572,14 @@ var UnifiedPlayer = class _UnifiedPlayer {
3171
3572
  async function createPlayer(options) {
3172
3573
  return UnifiedPlayer.create(options);
3173
3574
  }
3575
+ function decideVisibilityAction(state) {
3576
+ if (state.hidden) {
3577
+ if (state.userIntent === "play" && state.sessionIsPlaying) return "pause";
3578
+ return "noop";
3579
+ }
3580
+ if (state.autoPausedForVisibility) return "resume";
3581
+ return "noop";
3582
+ }
3174
3583
  function buildInitialDecision(initial, ctx) {
3175
3584
  const natural = classifyContext(ctx);
3176
3585
  const cls = strategyToClass(initial, natural);
@@ -3209,6 +3618,6 @@ function defaultFallbackChain(strategy) {
3209
3618
  }
3210
3619
  }
3211
3620
 
3212
- export { UnifiedPlayer, avbridgeAudioToMediabunny, avbridgeVideoToMediabunny, buildMediabunnySourceFromInput, classifyContext, createPlayer, probe, srtToVtt };
3213
- //# sourceMappingURL=chunk-DMWARSEF.js.map
3214
- //# sourceMappingURL=chunk-DMWARSEF.js.map
3621
+ export { FALLBACK_AUDIO_CODECS, FALLBACK_VIDEO_CODECS, NATIVE_AUDIO_CODECS, NATIVE_VIDEO_CODECS, UnifiedPlayer, avbridgeAudioToMediabunny, avbridgeVideoToMediabunny, buildMediabunnySourceFromInput, classifyContext, createPlayer, probe, srtToVtt };
3622
+ //# sourceMappingURL=chunk-NV7ILLWH.js.map
3623
+ //# sourceMappingURL=chunk-NV7ILLWH.js.map