avbridge 2.3.0 → 2.6.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 (111) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/dist/{chunk-6UUT4BEA.cjs → chunk-2IJ66NTD.cjs} +13 -20
  3. package/dist/chunk-2IJ66NTD.cjs.map +1 -0
  4. package/dist/{chunk-XKPSTC34.cjs → chunk-2XW2O3YI.cjs} +5 -20
  5. package/dist/chunk-2XW2O3YI.cjs.map +1 -0
  6. package/dist/chunk-5KVLE6YI.js +167 -0
  7. package/dist/chunk-5KVLE6YI.js.map +1 -0
  8. package/dist/{chunk-7RGG6ME7.cjs → chunk-6SOFJV44.cjs} +422 -688
  9. package/dist/chunk-6SOFJV44.cjs.map +1 -0
  10. package/dist/{chunk-2PGRFCWB.js → chunk-CPJLFFCC.js} +8 -18
  11. package/dist/chunk-CPJLFFCC.js.map +1 -0
  12. package/dist/chunk-CPZ7PXAM.cjs +240 -0
  13. package/dist/chunk-CPZ7PXAM.cjs.map +1 -0
  14. package/dist/{chunk-QQXBPW72.js → chunk-E76AMWI4.js} +4 -18
  15. package/dist/chunk-E76AMWI4.js.map +1 -0
  16. package/dist/chunk-LUFA47FP.js +19 -0
  17. package/dist/chunk-LUFA47FP.js.map +1 -0
  18. package/dist/{chunk-NV7ILLWH.js → chunk-OGYHFY6K.js} +404 -665
  19. package/dist/chunk-OGYHFY6K.js.map +1 -0
  20. package/dist/chunk-Q2VUO52Z.cjs +374 -0
  21. package/dist/chunk-Q2VUO52Z.cjs.map +1 -0
  22. package/dist/chunk-QDJLQR53.cjs +22 -0
  23. package/dist/chunk-QDJLQR53.cjs.map +1 -0
  24. package/dist/chunk-S4WAZC2T.cjs +173 -0
  25. package/dist/chunk-S4WAZC2T.cjs.map +1 -0
  26. package/dist/chunk-SMH6IOP2.js +368 -0
  27. package/dist/chunk-SMH6IOP2.js.map +1 -0
  28. package/dist/chunk-SR3MPV4D.js +237 -0
  29. package/dist/chunk-SR3MPV4D.js.map +1 -0
  30. package/dist/chunk-X2K3GIWE.js +235 -0
  31. package/dist/chunk-X2K3GIWE.js.map +1 -0
  32. package/dist/chunk-ZCUXHW55.cjs +242 -0
  33. package/dist/chunk-ZCUXHW55.cjs.map +1 -0
  34. package/dist/element-browser.js +883 -492
  35. package/dist/element-browser.js.map +1 -1
  36. package/dist/element.cjs +88 -6
  37. package/dist/element.cjs.map +1 -1
  38. package/dist/element.d.cts +51 -1
  39. package/dist/element.d.ts +51 -1
  40. package/dist/element.js +87 -5
  41. package/dist/element.js.map +1 -1
  42. package/dist/index.cjs +523 -393
  43. package/dist/index.cjs.map +1 -1
  44. package/dist/index.d.cts +2 -2
  45. package/dist/index.d.ts +2 -2
  46. package/dist/index.js +494 -366
  47. package/dist/index.js.map +1 -1
  48. package/dist/libav-demux-H2GS46GH.cjs +27 -0
  49. package/dist/libav-demux-H2GS46GH.cjs.map +1 -0
  50. package/dist/libav-demux-OWZ4T2YW.js +6 -0
  51. package/dist/libav-demux-OWZ4T2YW.js.map +1 -0
  52. package/dist/{libav-import-GST2AMPL.cjs → libav-import-2ZVKV2E7.cjs} +2 -2
  53. package/dist/{libav-import-GST2AMPL.cjs.map → libav-import-2ZVKV2E7.cjs.map} +1 -1
  54. package/dist/{libav-import-2JURFHEW.js → libav-import-6MGLCXVQ.js} +2 -2
  55. package/dist/{libav-import-2JURFHEW.js.map → libav-import-6MGLCXVQ.js.map} +1 -1
  56. package/dist/{player-B6WB74RD.d.ts → player-DGXeCNfD.d.cts} +41 -1
  57. package/dist/{player-B6WB74RD.d.cts → player-DGXeCNfD.d.ts} +41 -1
  58. package/dist/player.cjs +731 -472
  59. package/dist/player.cjs.map +1 -1
  60. package/dist/player.d.cts +229 -120
  61. package/dist/player.d.ts +229 -120
  62. package/dist/player.js +710 -451
  63. package/dist/player.js.map +1 -1
  64. package/dist/remux-OBSMIENG.cjs +35 -0
  65. package/dist/remux-OBSMIENG.cjs.map +1 -0
  66. package/dist/remux-WBYIZBBX.js +10 -0
  67. package/dist/remux-WBYIZBBX.js.map +1 -0
  68. package/dist/source-4TZ6KMNV.js +4 -0
  69. package/dist/{source-F656KYYV.js.map → source-4TZ6KMNV.js.map} +1 -1
  70. package/dist/source-7YLO6E7X.cjs +29 -0
  71. package/dist/{source-73CAH6HW.cjs.map → source-7YLO6E7X.cjs.map} +1 -1
  72. package/dist/source-MTX5ELUZ.js +4 -0
  73. package/dist/{source-QJR3OHTW.js.map → source-MTX5ELUZ.js.map} +1 -1
  74. package/dist/source-VFLXLOCN.cjs +29 -0
  75. package/dist/{source-VB74JQ7Z.cjs.map → source-VFLXLOCN.cjs.map} +1 -1
  76. package/dist/subtitles-4T74JRGT.js +4 -0
  77. package/dist/subtitles-4T74JRGT.js.map +1 -0
  78. package/dist/subtitles-QUH4LPI4.cjs +29 -0
  79. package/dist/subtitles-QUH4LPI4.cjs.map +1 -0
  80. package/package.json +1 -1
  81. package/src/convert/remux.ts +1 -35
  82. package/src/convert/transcode-libav.ts +691 -0
  83. package/src/convert/transcode.ts +12 -4
  84. package/src/element/avbridge-player.ts +100 -0
  85. package/src/element/avbridge-video.ts +140 -3
  86. package/src/element/player-styles.ts +12 -0
  87. package/src/errors.ts +6 -0
  88. package/src/player.ts +15 -16
  89. package/src/strategies/fallback/decoder.ts +96 -173
  90. package/src/strategies/fallback/index.ts +46 -2
  91. package/src/strategies/fallback/libav-import.ts +9 -1
  92. package/src/strategies/fallback/video-renderer.ts +107 -0
  93. package/src/strategies/hybrid/decoder.ts +88 -180
  94. package/src/strategies/hybrid/index.ts +35 -2
  95. package/src/strategies/native.ts +6 -3
  96. package/src/strategies/remux/index.ts +14 -2
  97. package/src/strategies/remux/pipeline.ts +72 -12
  98. package/src/subtitles/render.ts +8 -0
  99. package/src/types.ts +32 -0
  100. package/src/util/libav-demux.ts +405 -0
  101. package/src/util/time-ranges.ts +40 -0
  102. package/dist/chunk-2PGRFCWB.js.map +0 -1
  103. package/dist/chunk-6UUT4BEA.cjs.map +0 -1
  104. package/dist/chunk-7RGG6ME7.cjs.map +0 -1
  105. package/dist/chunk-NV7ILLWH.js.map +0 -1
  106. package/dist/chunk-QQXBPW72.js.map +0 -1
  107. package/dist/chunk-XKPSTC34.cjs.map +0 -1
  108. package/dist/source-73CAH6HW.cjs +0 -28
  109. package/dist/source-F656KYYV.js +0 -3
  110. package/dist/source-QJR3OHTW.js +0 -3
  111. package/dist/source-VB74JQ7Z.cjs +0 -28
package/dist/player.cjs CHANGED
@@ -1,7 +1,10 @@
1
1
  'use strict';
2
2
 
3
- var chunkXKPSTC34_cjs = require('./chunk-XKPSTC34.cjs');
3
+ var chunk2XW2O3YI_cjs = require('./chunk-2XW2O3YI.cjs');
4
4
  var chunkNNVOHKXJ_cjs = require('./chunk-NNVOHKXJ.cjs');
5
+ require('./chunk-Z33SBWL5.cjs');
6
+ var chunkS4WAZC2T_cjs = require('./chunk-S4WAZC2T.cjs');
7
+ require('./chunk-QDJLQR53.cjs');
5
8
 
6
9
  // src/events.ts
7
10
  var TypedEmitter = class {
@@ -228,8 +231,8 @@ var MEDIABUNNY_CONTAINERS = /* @__PURE__ */ new Set([
228
231
  "mpegts"
229
232
  ]);
230
233
  async function probe(source, transport) {
231
- const normalized = await chunkXKPSTC34_cjs.normalizeSource(source, transport);
232
- const sniffed = await chunkXKPSTC34_cjs.sniffNormalizedSource(normalized);
234
+ const normalized = await chunk2XW2O3YI_cjs.normalizeSource(source, transport);
235
+ const sniffed = await chunk2XW2O3YI_cjs.sniffNormalizedSource(normalized);
233
236
  if (MEDIABUNNY_CONTAINERS.has(sniffed)) {
234
237
  try {
235
238
  const result = await probeWithMediabunny(normalized, sniffed);
@@ -254,8 +257,8 @@ async function probe(source, transport) {
254
257
  } catch (libavErr) {
255
258
  const mbMsg = mediabunnyErr.message || String(mediabunnyErr);
256
259
  const lvMsg = libavErr instanceof Error ? libavErr.message : String(libavErr);
257
- throw new chunkXKPSTC34_cjs.AvbridgeError(
258
- chunkXKPSTC34_cjs.ERR_PROBE_FAILED,
260
+ throw new chunk2XW2O3YI_cjs.AvbridgeError(
261
+ chunk2XW2O3YI_cjs.ERR_PROBE_FAILED,
259
262
  `Failed to probe ${sniffed.toUpperCase()} file. mediabunny: ${mbMsg}. libav: ${lvMsg}.`,
260
263
  "The file may be corrupt, truncated, or in an unsupported format. Enable AVBRIDGE_DEBUG for detailed logs."
261
264
  );
@@ -269,14 +272,14 @@ async function probe(source, transport) {
269
272
  const inner = err instanceof Error ? err.message : String(err);
270
273
  console.error("[avbridge] libav probe failed for", sniffed, "file:", err);
271
274
  if (sniffed === "unknown") {
272
- throw new chunkXKPSTC34_cjs.AvbridgeError(
273
- chunkXKPSTC34_cjs.ERR_PROBE_UNKNOWN_CONTAINER,
275
+ throw new chunk2XW2O3YI_cjs.AvbridgeError(
276
+ chunk2XW2O3YI_cjs.ERR_PROBE_UNKNOWN_CONTAINER,
274
277
  `Unable to probe source: container format could not be identified. libav fallback: ${inner || "(no details)"}`,
275
278
  "The file may be corrupt or in a format avbridge doesn't recognize. Check the file plays in VLC or ffprobe."
276
279
  );
277
280
  }
278
- throw new chunkXKPSTC34_cjs.AvbridgeError(
279
- chunkXKPSTC34_cjs.ERR_LIBAV_NOT_REACHABLE,
281
+ throw new chunk2XW2O3YI_cjs.AvbridgeError(
282
+ chunk2XW2O3YI_cjs.ERR_LIBAV_NOT_REACHABLE,
280
283
  `${sniffed.toUpperCase()} files require libav.js, which failed to load: ${inner || "(no details)"}`,
281
284
  "Install @libav.js/variant-webcodecs, or check that AVBRIDGE_LIBAV_BASE points to the correct path."
282
285
  );
@@ -689,7 +692,7 @@ async function createNativeSession(context, video) {
689
692
  },
690
693
  async setAudioTrack(id) {
691
694
  const tracks = video.audioTracks;
692
- if (!tracks) return;
695
+ if (!tracks || tracks.length === 0) return;
693
696
  for (let i = 0; i < tracks.length; i++) {
694
697
  tracks[i].enabled = tracks[i].id === String(id) || i === id;
695
698
  }
@@ -741,15 +744,15 @@ var MseSink = class {
741
744
  constructor(options) {
742
745
  this.options = options;
743
746
  if (typeof MediaSource === "undefined") {
744
- throw new chunkXKPSTC34_cjs.AvbridgeError(
745
- chunkXKPSTC34_cjs.ERR_MSE_NOT_SUPPORTED,
747
+ throw new chunk2XW2O3YI_cjs.AvbridgeError(
748
+ chunk2XW2O3YI_cjs.ERR_MSE_NOT_SUPPORTED,
746
749
  "MediaSource Extensions (MSE) are not supported in this environment.",
747
750
  "MSE is required for the remux strategy. Use a browser that supports MSE, or try the fallback strategy."
748
751
  );
749
752
  }
750
753
  if (!MediaSource.isTypeSupported(options.mime)) {
751
- throw new chunkXKPSTC34_cjs.AvbridgeError(
752
- chunkXKPSTC34_cjs.ERR_MSE_CODEC_NOT_SUPPORTED,
754
+ throw new chunk2XW2O3YI_cjs.AvbridgeError(
755
+ chunk2XW2O3YI_cjs.ERR_MSE_CODEC_NOT_SUPPORTED,
753
756
  `This browser's MSE does not support "${options.mime}".`,
754
757
  "The codec combination can't be played via remux in this browser. The player will try the next strategy automatically."
755
758
  );
@@ -937,30 +940,49 @@ var MseSink = class {
937
940
  async function createRemuxPipeline(ctx, video) {
938
941
  const mb = await import('mediabunny');
939
942
  const videoTrackInfo = ctx.videoTracks[0];
940
- const audioTrackInfo = ctx.audioTracks[0];
941
943
  if (!videoTrackInfo) throw new Error("remux: source has no video track");
942
944
  const mbVideoCodec = avbridgeVideoToMediabunny(videoTrackInfo.codec);
943
945
  if (!mbVideoCodec) {
944
946
  throw new Error(`remux: video codec "${videoTrackInfo.codec}" is not supported by mediabunny output`);
945
947
  }
946
- const mbAudioCodec = audioTrackInfo ? avbridgeAudioToMediabunny(audioTrackInfo.codec) : null;
947
948
  const input = new mb.Input({
948
949
  source: await buildMediabunnySourceFromInput(mb, ctx.source),
949
950
  formats: mb.ALL_FORMATS
950
951
  });
951
952
  const allTracks = await input.getTracks();
952
953
  const inputVideo = allTracks.find((t) => t.id === videoTrackInfo.id && t.isVideoTrack());
953
- const inputAudio = audioTrackInfo ? allTracks.find((t) => t.id === audioTrackInfo.id && t.isAudioTrack()) : null;
954
954
  if (!inputVideo || !inputVideo.isVideoTrack()) {
955
955
  throw new Error("remux: video track not found in input");
956
956
  }
957
- if (audioTrackInfo && (!inputAudio || !inputAudio.isAudioTrack())) {
958
- throw new Error("remux: audio track not found in input");
959
- }
960
957
  const videoConfig = await inputVideo.getDecoderConfig();
961
- const audioConfig = inputAudio && inputAudio.isAudioTrack() ? await inputAudio.getDecoderConfig() : null;
962
958
  const videoSink = new mb.EncodedPacketSink(inputVideo);
963
- const audioSink = inputAudio?.isAudioTrack() ? new mb.EncodedPacketSink(inputAudio) : null;
959
+ let selectedAudioTrackId = ctx.audioTracks[0]?.id ?? null;
960
+ let inputAudio = null;
961
+ let mbAudioCodec = null;
962
+ let audioSink = null;
963
+ let audioConfig = null;
964
+ async function rebuildAudio() {
965
+ if (selectedAudioTrackId == null) {
966
+ inputAudio = null;
967
+ mbAudioCodec = null;
968
+ audioSink = null;
969
+ audioConfig = null;
970
+ return;
971
+ }
972
+ const trackInfo = ctx.audioTracks.find((t) => t.id === selectedAudioTrackId);
973
+ if (!trackInfo) {
974
+ throw new Error(`remux: no audio track with id ${selectedAudioTrackId}`);
975
+ }
976
+ const newInput = allTracks.find((t) => t.id === trackInfo.id && t.isAudioTrack());
977
+ if (!newInput || !newInput.isAudioTrack()) {
978
+ throw new Error("remux: audio track not found in input");
979
+ }
980
+ inputAudio = newInput;
981
+ mbAudioCodec = avbridgeAudioToMediabunny(trackInfo.codec);
982
+ audioSink = new mb.EncodedPacketSink(newInput);
983
+ audioConfig = await newInput.getDecoderConfig();
984
+ }
985
+ await rebuildAudio();
964
986
  let sink = null;
965
987
  const stats = { videoPackets: 0, audioPackets: 0, bytesWritten: 0, fragments: 0 };
966
988
  let destroyed = false;
@@ -1085,6 +1107,30 @@ async function createRemuxPipeline(ctx, video) {
1085
1107
  pendingAutoPlay = autoPlay;
1086
1108
  if (sink) sink.setPlayOnSeek(autoPlay);
1087
1109
  },
1110
+ async setAudioTrack(trackId, time, autoPlay) {
1111
+ if (selectedAudioTrackId === trackId) return;
1112
+ if (!ctx.audioTracks.some((t) => t.id === trackId)) {
1113
+ console.warn("[avbridge] remux: setAudioTrack \u2014 unknown track id", trackId);
1114
+ return;
1115
+ }
1116
+ pumpToken++;
1117
+ selectedAudioTrackId = trackId;
1118
+ await rebuildAudio().catch((err) => {
1119
+ console.warn("[avbridge] remux: rebuildAudio failed:", err.message);
1120
+ });
1121
+ if (sink) {
1122
+ try {
1123
+ sink.destroy();
1124
+ } catch {
1125
+ }
1126
+ sink = null;
1127
+ }
1128
+ pendingAutoPlay = autoPlay;
1129
+ pendingStartTime = time;
1130
+ pumpLoop(++pumpToken, time).catch((err) => {
1131
+ console.error("[avbridge] remux pipeline setAudioTrack pump failed:", err);
1132
+ });
1133
+ },
1088
1134
  async destroy() {
1089
1135
  destroyed = true;
1090
1136
  pumpToken++;
@@ -1144,7 +1190,19 @@ async function createRemuxSession(context, video) {
1144
1190
  const wasPlaying = !video.paused;
1145
1191
  await pipeline.seek(time, wasPlaying || wantPlay);
1146
1192
  },
1147
- async setAudioTrack(_id) {
1193
+ async setAudioTrack(id) {
1194
+ if (!context.audioTracks.some((t) => t.id === id)) {
1195
+ console.warn("[avbridge] remux: setAudioTrack \u2014 unknown track id", id);
1196
+ return;
1197
+ }
1198
+ const wasPlaying = !video.paused;
1199
+ const time = video.currentTime || 0;
1200
+ if (!started) {
1201
+ started = true;
1202
+ await pipeline.setAudioTrack(id, time, wantPlay || wasPlaying);
1203
+ return;
1204
+ }
1205
+ await pipeline.setAudioTrack(id, time, wasPlaying || wantPlay);
1148
1206
  },
1149
1207
  async setSubtitleTrack(id) {
1150
1208
  const tracks = video.textTracks;
@@ -1197,6 +1255,9 @@ var VideoRenderer = class {
1197
1255
  document.body.appendChild(this.canvas);
1198
1256
  }
1199
1257
  target.style.visibility = "hidden";
1258
+ const overlayParent = parent instanceof HTMLElement ? parent : document.body;
1259
+ this.subtitleOverlay = new chunkS4WAZC2T_cjs.SubtitleOverlay(overlayParent);
1260
+ this.watchTextTracks(target);
1200
1261
  const ctx = this.canvas.getContext("2d");
1201
1262
  if (!ctx) throw new Error("video renderer: failed to acquire 2D context");
1202
1263
  this.ctx = ctx;
@@ -1222,6 +1283,15 @@ var VideoRenderer = class {
1222
1283
  ticksWaiting = 0;
1223
1284
  /** Cumulative count of ticks where PTS mode painted a frame. */
1224
1285
  ticksPainted = 0;
1286
+ /**
1287
+ * Subtitle overlay div attached to the stage wrapper alongside the
1288
+ * canvas. Created lazily when subtitle tracks are attached via the
1289
+ * target's `<track>` children. Canvas strategies (hybrid, fallback)
1290
+ * hide the <video>, so we can't rely on the browser's native cue
1291
+ * rendering; we read TextTrack.cues and render into this overlay.
1292
+ */
1293
+ subtitleOverlay = null;
1294
+ subtitleTrack = null;
1225
1295
  /**
1226
1296
  * Calibration offset (microseconds) between video PTS and audio clock.
1227
1297
  * Video PTS and AudioContext.currentTime can drift ~0.1% relative to
@@ -1265,9 +1335,80 @@ var VideoRenderer = class {
1265
1335
  this.framesDroppedOverflow++;
1266
1336
  }
1267
1337
  }
1338
+ /**
1339
+ * Watch the target <video>'s textTracks list. When a track is added,
1340
+ * grab it and start polling cues on each render tick. Existing tracks
1341
+ * (if any) are picked up immediately.
1342
+ */
1343
+ watchTextTracks(target) {
1344
+ const pick = () => {
1345
+ if (this.subtitleTrack) return;
1346
+ const tracks = target.textTracks;
1347
+ if (isDebug()) {
1348
+ console.log(`[avbridge:subs] watchTextTracks pick() \u2014 ${tracks.length} tracks`);
1349
+ }
1350
+ for (let i = 0; i < tracks.length; i++) {
1351
+ const t = tracks[i];
1352
+ if (isDebug()) {
1353
+ console.log(`[avbridge:subs] track ${i}: kind=${t.kind} mode=${t.mode} cues=${t.cues?.length ?? 0}`);
1354
+ }
1355
+ if (t.kind === "subtitles" || t.kind === "captions") {
1356
+ this.subtitleTrack = t;
1357
+ t.mode = "hidden";
1358
+ if (isDebug()) {
1359
+ console.log(`[avbridge:subs] picked track, mode=hidden`);
1360
+ }
1361
+ const trackEl = target.querySelector(`track[srclang="${t.language}"]`);
1362
+ if (trackEl) {
1363
+ trackEl.addEventListener("load", () => {
1364
+ if (isDebug()) {
1365
+ console.log(`[avbridge:subs] track element loaded, cues=${t.cues?.length ?? 0}`);
1366
+ }
1367
+ });
1368
+ trackEl.addEventListener("error", (ev) => {
1369
+ console.warn(`[avbridge:subs] track element error:`, ev);
1370
+ });
1371
+ }
1372
+ break;
1373
+ }
1374
+ }
1375
+ };
1376
+ pick();
1377
+ if (typeof target.textTracks.addEventListener === "function") {
1378
+ target.textTracks.addEventListener("addtrack", (e) => {
1379
+ if (isDebug()) {
1380
+ console.log("[avbridge:subs] addtrack event fired");
1381
+ }
1382
+ pick();
1383
+ });
1384
+ }
1385
+ }
1386
+ _loggedCues = false;
1387
+ /** Find the active cue (if any) for the given media time. */
1388
+ updateSubtitles() {
1389
+ if (!this.subtitleOverlay || !this.subtitleTrack) return;
1390
+ const cues = this.subtitleTrack.cues;
1391
+ if (!cues || cues.length === 0) return;
1392
+ if (isDebug() && !this._loggedCues) {
1393
+ this._loggedCues = true;
1394
+ console.log(`[avbridge:subs] cues available: ${cues.length}, first start=${cues[0].startTime}, last end=${cues[cues.length - 1].endTime}`);
1395
+ }
1396
+ const t = this.clock.now();
1397
+ let activeText = "";
1398
+ for (let i = 0; i < cues.length; i++) {
1399
+ const c = cues[i];
1400
+ if (t >= c.startTime && t <= c.endTime) {
1401
+ const vttCue = c;
1402
+ activeText = vttCue.text ?? "";
1403
+ break;
1404
+ }
1405
+ }
1406
+ this.subtitleOverlay.setText(activeText.replace(/<[^>]+>/g, ""));
1407
+ }
1268
1408
  tick() {
1269
1409
  if (this.destroyed) return;
1270
1410
  this.rafHandle = requestAnimationFrame(this.tick);
1411
+ this.updateSubtitles();
1271
1412
  if (this.queue.length === 0) return;
1272
1413
  const playing = this.clock.isPlaying();
1273
1414
  if (!playing) {
@@ -1396,6 +1537,11 @@ var VideoRenderer = class {
1396
1537
  this.destroyed = true;
1397
1538
  if (this.rafHandle != null) cancelAnimationFrame(this.rafHandle);
1398
1539
  this.flush();
1540
+ if (this.subtitleOverlay) {
1541
+ this.subtitleOverlay.destroy();
1542
+ this.subtitleOverlay = null;
1543
+ }
1544
+ this.subtitleTrack = null;
1399
1545
  this.canvas.remove();
1400
1546
  this.target.style.visibility = "";
1401
1547
  }
@@ -1650,6 +1796,160 @@ function pickLibavVariant(ctx) {
1650
1796
  return "webcodecs";
1651
1797
  }
1652
1798
 
1799
+ // src/util/libav-demux.ts
1800
+ function sanitizePacketTimestamp(pkt, nextUs, fallbackTimeBase) {
1801
+ const lo = pkt.pts ?? 0;
1802
+ const hi = pkt.ptshi ?? 0;
1803
+ const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
1804
+ if (isInvalid) {
1805
+ const us2 = nextUs();
1806
+ pkt.pts = us2;
1807
+ pkt.ptshi = 0;
1808
+ pkt.time_base_num = 1;
1809
+ pkt.time_base_den = 1e6;
1810
+ return;
1811
+ }
1812
+ const tb = fallbackTimeBase ?? [1, 1e6];
1813
+ const pts64 = hi * 4294967296 + lo;
1814
+ const us = Math.round(pts64 * 1e6 * tb[0] / tb[1]);
1815
+ if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
1816
+ pkt.pts = us;
1817
+ pkt.ptshi = us < 0 ? -1 : 0;
1818
+ pkt.time_base_num = 1;
1819
+ pkt.time_base_den = 1e6;
1820
+ return;
1821
+ }
1822
+ const fallback = nextUs();
1823
+ pkt.pts = fallback;
1824
+ pkt.ptshi = 0;
1825
+ pkt.time_base_num = 1;
1826
+ pkt.time_base_den = 1e6;
1827
+ }
1828
+ var AV_SAMPLE_FMT_U8 = 0;
1829
+ var AV_SAMPLE_FMT_S16 = 1;
1830
+ var AV_SAMPLE_FMT_S32 = 2;
1831
+ var AV_SAMPLE_FMT_FLT = 3;
1832
+ var AV_SAMPLE_FMT_U8P = 5;
1833
+ var AV_SAMPLE_FMT_S16P = 6;
1834
+ var AV_SAMPLE_FMT_S32P = 7;
1835
+ var AV_SAMPLE_FMT_FLTP = 8;
1836
+ function libavFrameToInterleavedFloat32(frame) {
1837
+ const channels = frame.channels ?? frame.ch_layout_nb_channels ?? 1;
1838
+ const sampleRate = frame.sample_rate ?? 44100;
1839
+ const nbSamples = frame.nb_samples ?? 0;
1840
+ if (nbSamples === 0) return null;
1841
+ const out = new Float32Array(nbSamples * channels);
1842
+ switch (frame.format) {
1843
+ case AV_SAMPLE_FMT_FLTP: {
1844
+ const planes = ensurePlanes(frame.data, channels);
1845
+ for (let ch = 0; ch < channels; ch++) {
1846
+ const plane = asFloat32(planes[ch]);
1847
+ for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i];
1848
+ }
1849
+ return { data: out, channels, sampleRate };
1850
+ }
1851
+ case AV_SAMPLE_FMT_FLT: {
1852
+ const flat = asFloat32(frame.data);
1853
+ for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i];
1854
+ return { data: out, channels, sampleRate };
1855
+ }
1856
+ case AV_SAMPLE_FMT_S16P: {
1857
+ const planes = ensurePlanes(frame.data, channels);
1858
+ for (let ch = 0; ch < channels; ch++) {
1859
+ const plane = asInt16(planes[ch]);
1860
+ for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 32768;
1861
+ }
1862
+ return { data: out, channels, sampleRate };
1863
+ }
1864
+ case AV_SAMPLE_FMT_S16: {
1865
+ const flat = asInt16(frame.data);
1866
+ for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 32768;
1867
+ return { data: out, channels, sampleRate };
1868
+ }
1869
+ case AV_SAMPLE_FMT_S32P: {
1870
+ const planes = ensurePlanes(frame.data, channels);
1871
+ for (let ch = 0; ch < channels; ch++) {
1872
+ const plane = asInt32(planes[ch]);
1873
+ for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 2147483648;
1874
+ }
1875
+ return { data: out, channels, sampleRate };
1876
+ }
1877
+ case AV_SAMPLE_FMT_S32: {
1878
+ const flat = asInt32(frame.data);
1879
+ for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 2147483648;
1880
+ return { data: out, channels, sampleRate };
1881
+ }
1882
+ case AV_SAMPLE_FMT_U8P: {
1883
+ const planes = ensurePlanes(frame.data, channels);
1884
+ for (let ch = 0; ch < channels; ch++) {
1885
+ const plane = asUint8(planes[ch]);
1886
+ for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = (plane[i] - 128) / 128;
1887
+ }
1888
+ return { data: out, channels, sampleRate };
1889
+ }
1890
+ case AV_SAMPLE_FMT_U8: {
1891
+ const flat = asUint8(frame.data);
1892
+ for (let i = 0; i < nbSamples * channels; i++) out[i] = (flat[i] - 128) / 128;
1893
+ return { data: out, channels, sampleRate };
1894
+ }
1895
+ default:
1896
+ return null;
1897
+ }
1898
+ }
1899
+ function ensurePlanes(data, channels) {
1900
+ if (Array.isArray(data)) return data;
1901
+ const arr = data;
1902
+ const len = arr.length;
1903
+ const perChannel = Math.floor(len / channels);
1904
+ const planes = [];
1905
+ for (let ch = 0; ch < channels; ch++) {
1906
+ planes.push(arr.subarray ? arr.subarray(ch * perChannel, (ch + 1) * perChannel) : arr);
1907
+ }
1908
+ return planes;
1909
+ }
1910
+ function asFloat32(x) {
1911
+ if (x instanceof Float32Array) return x;
1912
+ const ta = x;
1913
+ return new Float32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
1914
+ }
1915
+ function asInt16(x) {
1916
+ if (x instanceof Int16Array) return x;
1917
+ const ta = x;
1918
+ return new Int16Array(ta.buffer, ta.byteOffset, ta.byteLength / 2);
1919
+ }
1920
+ function asInt32(x) {
1921
+ if (x instanceof Int32Array) return x;
1922
+ const ta = x;
1923
+ return new Int32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
1924
+ }
1925
+ function asUint8(x) {
1926
+ if (x instanceof Uint8Array) return x;
1927
+ const ta = x;
1928
+ return new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength);
1929
+ }
1930
+ function sanitizeFrameTimestamp(frame, nextUs, fallbackTimeBase) {
1931
+ const lo = frame.pts ?? 0;
1932
+ const hi = frame.ptshi ?? 0;
1933
+ const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
1934
+ if (isInvalid) {
1935
+ const us2 = nextUs();
1936
+ frame.pts = us2;
1937
+ frame.ptshi = 0;
1938
+ return;
1939
+ }
1940
+ const tb = fallbackTimeBase ?? [1, 1e6];
1941
+ const pts64 = hi * 4294967296 + lo;
1942
+ const us = Math.round(pts64 * 1e6 * tb[0] / tb[1]);
1943
+ if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
1944
+ frame.pts = us;
1945
+ frame.ptshi = us < 0 ? -1 : 0;
1946
+ return;
1947
+ }
1948
+ const fallback = nextUs();
1949
+ frame.pts = fallback;
1950
+ frame.ptshi = 0;
1951
+ }
1952
+
1653
1953
  // src/strategies/hybrid/decoder.ts
1654
1954
  async function startHybridDecoder(opts) {
1655
1955
  const variant = pickLibavVariant(opts.context);
@@ -1660,7 +1960,8 @@ async function startHybridDecoder(opts) {
1660
1960
  const readPkt = await libav.av_packet_alloc();
1661
1961
  const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
1662
1962
  const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
1663
- const audioStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO) ?? null;
1963
+ const firstAudioTrackId = opts.context.audioTracks[0]?.id;
1964
+ let audioStream = (firstAudioTrackId != null ? streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO && s.index === firstAudioTrackId) : void 0) ?? streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO) ?? null;
1664
1965
  if (!videoStream && !audioStream) {
1665
1966
  throw new Error("hybrid decoder: file has no decodable streams");
1666
1967
  }
@@ -1956,7 +2257,15 @@ async function startHybridDecoder(opts) {
1956
2257
  } catch {
1957
2258
  }
1958
2259
  },
1959
- async seek(timeSec) {
2260
+ async setAudioTrack(trackId, timeSec) {
2261
+ if (audioStream && audioStream.index === trackId) return;
2262
+ const newStream = streams.find(
2263
+ (s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO && s.index === trackId
2264
+ );
2265
+ if (!newStream) {
2266
+ console.warn("[avbridge] hybrid: setAudioTrack \u2014 no stream with id", trackId);
2267
+ return;
2268
+ }
1960
2269
  const newToken = ++pumpToken;
1961
2270
  if (pumpRunning) {
1962
2271
  try {
@@ -1965,6 +2274,28 @@ async function startHybridDecoder(opts) {
1965
2274
  }
1966
2275
  }
1967
2276
  if (destroyed) return;
2277
+ if (audioDec) {
2278
+ try {
2279
+ await libav.ff_free_decoder?.(audioDec.c, audioDec.pkt, audioDec.frame);
2280
+ } catch {
2281
+ }
2282
+ audioDec = null;
2283
+ }
2284
+ try {
2285
+ const [, c, pkt, frame] = await libav.ff_init_decoder(newStream.codec_id, {
2286
+ codecpar: newStream.codecpar
2287
+ });
2288
+ audioDec = { c, pkt, frame };
2289
+ audioTimeBase = newStream.time_base_num && newStream.time_base_den ? [newStream.time_base_num, newStream.time_base_den] : void 0;
2290
+ } catch (err) {
2291
+ console.warn(
2292
+ "[avbridge] hybrid: setAudioTrack init failed \u2014 switching to no-audio:",
2293
+ err.message
2294
+ );
2295
+ audioDec = null;
2296
+ opts.audio.setNoAudio();
2297
+ }
2298
+ audioStream = newStream;
1968
2299
  try {
1969
2300
  const tsUs = Math.floor(timeSec * 1e6);
1970
2301
  const [tsLo, tsHi] = libav.f64toi64 ? libav.f64toi64(tsUs) : [tsUs | 0, Math.floor(tsUs / 4294967296)];
@@ -1976,7 +2307,7 @@ async function startHybridDecoder(opts) {
1976
2307
  libav.AVSEEK_FLAG_BACKWARD ?? 0
1977
2308
  );
1978
2309
  } catch (err) {
1979
- console.warn("[avbridge] hybrid av_seek_frame failed:", err);
2310
+ console.warn("[avbridge] hybrid: setAudioTrack seek failed:", err);
1980
2311
  }
1981
2312
  try {
1982
2313
  if (videoDecoder && videoDecoder.state === "configured") {
@@ -1984,190 +2315,73 @@ async function startHybridDecoder(opts) {
1984
2315
  }
1985
2316
  } catch {
1986
2317
  }
1987
- try {
1988
- if (audioDec) await libav.avcodec_flush_buffers?.(audioDec.c);
1989
- } catch {
1990
- }
1991
2318
  await flushBSF();
1992
2319
  syntheticVideoUs = Math.round(timeSec * 1e6);
1993
2320
  syntheticAudioUs = Math.round(timeSec * 1e6);
1994
2321
  pumpRunning = pumpLoop(newToken).catch(
1995
- (err) => console.error("[avbridge] hybrid pump failed (post-seek):", err)
2322
+ (err) => console.error("[avbridge] hybrid pump failed (post-setAudioTrack):", err)
1996
2323
  );
1997
2324
  },
1998
- stats() {
1999
- return {
2000
- decoderType: "webcodecs-hybrid",
2001
- packetsRead,
2002
- videoFramesDecoded,
2003
- videoChunksFed,
2004
- audioFramesDecoded,
2005
- bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
2006
- videoDecodeQueueSize: videoDecoder?.decodeQueueSize ?? 0,
2007
- // Confirmed transport info — see fallback decoder for the pattern.
2008
- _transport: inputHandle.transport === "http-range" ? "http-range" : "memory",
2009
- _rangeSupported: inputHandle.transport === "http-range",
2010
- ...opts.renderer.stats(),
2011
- ...opts.audio.stats()
2012
- };
2013
- }
2014
- };
2015
- }
2016
- function sanitizePacketTimestamp(pkt, nextUs, fallbackTimeBase) {
2017
- const lo = pkt.pts ?? 0;
2018
- const hi = pkt.ptshi ?? 0;
2019
- const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
2020
- if (isInvalid) {
2021
- const us2 = nextUs();
2022
- pkt.pts = us2;
2023
- pkt.ptshi = 0;
2024
- pkt.time_base_num = 1;
2025
- pkt.time_base_den = 1e6;
2026
- return;
2027
- }
2028
- const tb = fallbackTimeBase ?? [1, 1e6];
2029
- const pts64 = hi * 4294967296 + lo;
2030
- const us = Math.round(pts64 * 1e6 * tb[0] / tb[1]);
2031
- if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
2032
- pkt.pts = us;
2033
- pkt.ptshi = us < 0 ? -1 : 0;
2034
- pkt.time_base_num = 1;
2035
- pkt.time_base_den = 1e6;
2036
- return;
2037
- }
2038
- const fallback = nextUs();
2039
- pkt.pts = fallback;
2040
- pkt.ptshi = 0;
2041
- pkt.time_base_num = 1;
2042
- pkt.time_base_den = 1e6;
2043
- }
2044
- function sanitizeFrameTimestamp(frame, nextUs, fallbackTimeBase) {
2045
- const lo = frame.pts ?? 0;
2046
- const hi = frame.ptshi ?? 0;
2047
- const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
2048
- if (isInvalid) {
2049
- const us2 = nextUs();
2050
- frame.pts = us2;
2051
- frame.ptshi = 0;
2052
- return;
2053
- }
2054
- const tb = fallbackTimeBase ?? [1, 1e6];
2055
- const pts64 = hi * 4294967296 + lo;
2056
- const us = Math.round(pts64 * 1e6 * tb[0] / tb[1]);
2057
- if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
2058
- frame.pts = us;
2059
- frame.ptshi = us < 0 ? -1 : 0;
2060
- return;
2061
- }
2062
- const fallback = nextUs();
2063
- frame.pts = fallback;
2064
- frame.ptshi = 0;
2065
- }
2066
- var AV_SAMPLE_FMT_U8 = 0;
2067
- var AV_SAMPLE_FMT_S16 = 1;
2068
- var AV_SAMPLE_FMT_S32 = 2;
2069
- var AV_SAMPLE_FMT_FLT = 3;
2070
- var AV_SAMPLE_FMT_U8P = 5;
2071
- var AV_SAMPLE_FMT_S16P = 6;
2072
- var AV_SAMPLE_FMT_S32P = 7;
2073
- var AV_SAMPLE_FMT_FLTP = 8;
2074
- function libavFrameToInterleavedFloat32(frame) {
2075
- const channels = frame.channels ?? frame.ch_layout_nb_channels ?? 1;
2076
- const sampleRate = frame.sample_rate ?? 44100;
2077
- const nbSamples = frame.nb_samples ?? 0;
2078
- if (nbSamples === 0) return null;
2079
- const out = new Float32Array(nbSamples * channels);
2080
- switch (frame.format) {
2081
- case AV_SAMPLE_FMT_FLTP: {
2082
- const planes = ensurePlanes(frame.data, channels);
2083
- for (let ch = 0; ch < channels; ch++) {
2084
- const plane = asFloat32(planes[ch]);
2085
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i];
2325
+ async seek(timeSec) {
2326
+ const newToken = ++pumpToken;
2327
+ if (pumpRunning) {
2328
+ try {
2329
+ await pumpRunning;
2330
+ } catch {
2331
+ }
2086
2332
  }
2087
- return { data: out, channels, sampleRate };
2088
- }
2089
- case AV_SAMPLE_FMT_FLT: {
2090
- const flat = asFloat32(frame.data);
2091
- for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i];
2092
- return { data: out, channels, sampleRate };
2093
- }
2094
- case AV_SAMPLE_FMT_S16P: {
2095
- const planes = ensurePlanes(frame.data, channels);
2096
- for (let ch = 0; ch < channels; ch++) {
2097
- const plane = asInt16(planes[ch]);
2098
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 32768;
2333
+ if (destroyed) return;
2334
+ try {
2335
+ const tsUs = Math.floor(timeSec * 1e6);
2336
+ const [tsLo, tsHi] = libav.f64toi64 ? libav.f64toi64(tsUs) : [tsUs | 0, Math.floor(tsUs / 4294967296)];
2337
+ await libav.av_seek_frame(
2338
+ fmt_ctx,
2339
+ -1,
2340
+ tsLo,
2341
+ tsHi,
2342
+ libav.AVSEEK_FLAG_BACKWARD ?? 0
2343
+ );
2344
+ } catch (err) {
2345
+ console.warn("[avbridge] hybrid av_seek_frame failed:", err);
2099
2346
  }
2100
- return { data: out, channels, sampleRate };
2101
- }
2102
- case AV_SAMPLE_FMT_S16: {
2103
- const flat = asInt16(frame.data);
2104
- for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 32768;
2105
- return { data: out, channels, sampleRate };
2106
- }
2107
- case AV_SAMPLE_FMT_S32P: {
2108
- const planes = ensurePlanes(frame.data, channels);
2109
- for (let ch = 0; ch < channels; ch++) {
2110
- const plane = asInt32(planes[ch]);
2111
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 2147483648;
2347
+ try {
2348
+ if (videoDecoder && videoDecoder.state === "configured") {
2349
+ await videoDecoder.flush();
2350
+ }
2351
+ } catch {
2112
2352
  }
2113
- return { data: out, channels, sampleRate };
2114
- }
2115
- case AV_SAMPLE_FMT_S32: {
2116
- const flat = asInt32(frame.data);
2117
- for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 2147483648;
2118
- return { data: out, channels, sampleRate };
2119
- }
2120
- case AV_SAMPLE_FMT_U8P: {
2121
- const planes = ensurePlanes(frame.data, channels);
2122
- for (let ch = 0; ch < channels; ch++) {
2123
- const plane = asUint8(planes[ch]);
2124
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = (plane[i] - 128) / 128;
2353
+ try {
2354
+ if (audioDec) await libav.avcodec_flush_buffers?.(audioDec.c);
2355
+ } catch {
2125
2356
  }
2126
- return { data: out, channels, sampleRate };
2127
- }
2128
- case AV_SAMPLE_FMT_U8: {
2129
- const flat = asUint8(frame.data);
2130
- for (let i = 0; i < nbSamples * channels; i++) out[i] = (flat[i] - 128) / 128;
2131
- return { data: out, channels, sampleRate };
2132
- }
2133
- default:
2134
- return null;
2135
- }
2136
- }
2137
- function ensurePlanes(data, channels) {
2138
- if (Array.isArray(data)) return data;
2139
- const arr = data;
2140
- const len = arr.length;
2141
- const perChannel = Math.floor(len / channels);
2142
- const planes = [];
2143
- for (let ch = 0; ch < channels; ch++) {
2144
- planes.push(arr.subarray ? arr.subarray(ch * perChannel, (ch + 1) * perChannel) : arr);
2145
- }
2146
- return planes;
2147
- }
2148
- function asFloat32(x) {
2149
- if (x instanceof Float32Array) return x;
2150
- const ta = x;
2151
- return new Float32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
2152
- }
2153
- function asInt16(x) {
2154
- if (x instanceof Int16Array) return x;
2155
- const ta = x;
2156
- return new Int16Array(ta.buffer, ta.byteOffset, ta.byteLength / 2);
2157
- }
2158
- function asInt32(x) {
2159
- if (x instanceof Int32Array) return x;
2160
- const ta = x;
2161
- return new Int32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
2162
- }
2163
- function asUint8(x) {
2164
- if (x instanceof Uint8Array) return x;
2165
- const ta = x;
2166
- return new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength);
2357
+ await flushBSF();
2358
+ syntheticVideoUs = Math.round(timeSec * 1e6);
2359
+ syntheticAudioUs = Math.round(timeSec * 1e6);
2360
+ pumpRunning = pumpLoop(newToken).catch(
2361
+ (err) => console.error("[avbridge] hybrid pump failed (post-seek):", err)
2362
+ );
2363
+ },
2364
+ stats() {
2365
+ return {
2366
+ decoderType: "webcodecs-hybrid",
2367
+ packetsRead,
2368
+ videoFramesDecoded,
2369
+ videoChunksFed,
2370
+ audioFramesDecoded,
2371
+ bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
2372
+ videoDecodeQueueSize: videoDecoder?.decodeQueueSize ?? 0,
2373
+ // Confirmed transport info — see fallback decoder for the pattern.
2374
+ _transport: inputHandle.transport === "http-range" ? "http-range" : "memory",
2375
+ _rangeSupported: inputHandle.transport === "http-range",
2376
+ ...opts.renderer.stats(),
2377
+ ...opts.audio.stats()
2378
+ };
2379
+ }
2380
+ };
2167
2381
  }
2168
2382
  async function loadBridge() {
2169
2383
  try {
2170
- const wrapper = await import('./libav-import-GST2AMPL.cjs');
2384
+ const wrapper = await import('./libav-import-2ZVKV2E7.cjs');
2171
2385
  return wrapper.libavBridge;
2172
2386
  } catch (err) {
2173
2387
  throw new Error(
@@ -2176,11 +2390,40 @@ async function loadBridge() {
2176
2390
  }
2177
2391
  }
2178
2392
 
2393
+ // src/util/time-ranges.ts
2394
+ function makeTimeRanges(ranges) {
2395
+ const frozen = ranges.slice();
2396
+ const impl = {
2397
+ get length() {
2398
+ return frozen.length;
2399
+ },
2400
+ start(index) {
2401
+ if (index < 0 || index >= frozen.length) {
2402
+ throw new DOMException(
2403
+ `TimeRanges.start: index ${index} out of range (length=${frozen.length})`,
2404
+ "IndexSizeError"
2405
+ );
2406
+ }
2407
+ return frozen[index][0];
2408
+ },
2409
+ end(index) {
2410
+ if (index < 0 || index >= frozen.length) {
2411
+ throw new DOMException(
2412
+ `TimeRanges.end: index ${index} out of range (length=${frozen.length})`,
2413
+ "IndexSizeError"
2414
+ );
2415
+ }
2416
+ return frozen[index][1];
2417
+ }
2418
+ };
2419
+ return impl;
2420
+ }
2421
+
2179
2422
  // src/strategies/hybrid/index.ts
2180
2423
  var READY_AUDIO_BUFFER_SECONDS = 0.3;
2181
2424
  var READY_TIMEOUT_SECONDS = 10;
2182
2425
  async function createHybridSession(ctx, target, transport) {
2183
- const { normalizeSource: normalizeSource2 } = await import('./source-VB74JQ7Z.cjs');
2426
+ const { normalizeSource: normalizeSource2 } = await import('./source-7YLO6E7X.cjs');
2184
2427
  const source = await normalizeSource2(ctx.source);
2185
2428
  const fps = ctx.videoTracks[0]?.fps ?? 30;
2186
2429
  const audio = new AudioOutput();
@@ -2233,6 +2476,18 @@ async function createHybridSession(ctx, target, transport) {
2233
2476
  get: () => ctx.duration ?? NaN
2234
2477
  });
2235
2478
  }
2479
+ Object.defineProperty(target, "readyState", {
2480
+ configurable: true,
2481
+ get: () => {
2482
+ if (!renderer.hasFrames()) return 0;
2483
+ if (!audio.isPlaying() && audio.bufferAhead() <= 0 && !audio.isNoAudio()) return 1;
2484
+ return 2;
2485
+ }
2486
+ });
2487
+ Object.defineProperty(target, "seekable", {
2488
+ configurable: true,
2489
+ get: () => makeTimeRanges(ctx.duration && Number.isFinite(ctx.duration) && ctx.duration > 0 ? [[0, ctx.duration]] : [])
2490
+ });
2236
2491
  async function waitForBuffer() {
2237
2492
  const start = performance.now();
2238
2493
  while (true) {
@@ -2277,7 +2532,24 @@ async function createHybridSession(ctx, target, transport) {
2277
2532
  async seek(time) {
2278
2533
  await doSeek(time);
2279
2534
  },
2280
- async setAudioTrack(_id) {
2535
+ async setAudioTrack(id) {
2536
+ if (!ctx.audioTracks.some((t) => t.id === id)) {
2537
+ console.warn("[avbridge] hybrid: setAudioTrack \u2014 unknown track id", id);
2538
+ return;
2539
+ }
2540
+ const wasPlaying = audio.isPlaying();
2541
+ const currentTime = audio.now();
2542
+ await audio.pause().catch(() => {
2543
+ });
2544
+ await handles.setAudioTrack(id, currentTime).catch(
2545
+ (err) => console.warn("[avbridge] hybrid: handles.setAudioTrack failed:", err)
2546
+ );
2547
+ await audio.reset(currentTime);
2548
+ renderer.flush();
2549
+ if (wasPlaying) {
2550
+ await waitForBuffer();
2551
+ await audio.start();
2552
+ }
2281
2553
  },
2282
2554
  async setSubtitleTrack(_id) {
2283
2555
  },
@@ -2297,6 +2569,8 @@ async function createHybridSession(ctx, target, transport) {
2297
2569
  delete target.paused;
2298
2570
  delete target.volume;
2299
2571
  delete target.muted;
2572
+ delete target.readyState;
2573
+ delete target.seekable;
2300
2574
  } catch {
2301
2575
  }
2302
2576
  },
@@ -2316,7 +2590,8 @@ async function startDecoder(opts) {
2316
2590
  const readPkt = await libav.av_packet_alloc();
2317
2591
  const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
2318
2592
  const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
2319
- const audioStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO) ?? null;
2593
+ const firstAudioTrackId = opts.context.audioTracks[0]?.id;
2594
+ let audioStream = (firstAudioTrackId != null ? streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO && s.index === firstAudioTrackId) : void 0) ?? streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO) ?? null;
2320
2595
  if (!videoStream && !audioStream) {
2321
2596
  throw new Error("fallback decoder: file has no decodable streams");
2322
2597
  }
@@ -2532,7 +2807,7 @@ async function startDecoder(opts) {
2532
2807
  if (myToken !== pumpToken || destroyed) return;
2533
2808
  for (const f of frames) {
2534
2809
  if (myToken !== pumpToken || destroyed) return;
2535
- const bridgeOpts = sanitizeFrameTimestamp2(
2810
+ sanitizeFrameTimestamp(
2536
2811
  f,
2537
2812
  () => {
2538
2813
  const ts = syntheticVideoUs;
@@ -2542,7 +2817,7 @@ async function startDecoder(opts) {
2542
2817
  videoTimeBase
2543
2818
  );
2544
2819
  try {
2545
- const vf = bridge.laFrameToVideoFrame(f, bridgeOpts);
2820
+ const vf = bridge.laFrameToVideoFrame(f, { timeBase: [1, 1e6] });
2546
2821
  opts.renderer.enqueue(vf);
2547
2822
  videoFramesDecoded++;
2548
2823
  } catch (err) {
@@ -2570,7 +2845,7 @@ async function startDecoder(opts) {
2570
2845
  if (myToken !== pumpToken || destroyed) return;
2571
2846
  for (const f of frames) {
2572
2847
  if (myToken !== pumpToken || destroyed) return;
2573
- sanitizeFrameTimestamp2(
2848
+ sanitizeFrameTimestamp(
2574
2849
  f,
2575
2850
  () => {
2576
2851
  const ts = syntheticAudioUs;
@@ -2581,7 +2856,7 @@ async function startDecoder(opts) {
2581
2856
  },
2582
2857
  audioTimeBase
2583
2858
  );
2584
- const samples = libavFrameToInterleavedFloat322(f);
2859
+ const samples = libavFrameToInterleavedFloat32(f);
2585
2860
  if (samples) {
2586
2861
  opts.audio.schedule(samples.data, samples.channels, samples.sampleRate);
2587
2862
  audioFramesDecoded++;
@@ -2629,6 +2904,69 @@ async function startDecoder(opts) {
2629
2904
  } catch {
2630
2905
  }
2631
2906
  },
2907
+ async setAudioTrack(trackId, timeSec) {
2908
+ if (audioStream && audioStream.index === trackId) return;
2909
+ const newStream = streams.find(
2910
+ (s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO && s.index === trackId
2911
+ );
2912
+ if (!newStream) {
2913
+ console.warn("[avbridge] fallback: setAudioTrack \u2014 no stream with id", trackId);
2914
+ return;
2915
+ }
2916
+ const newToken = ++pumpToken;
2917
+ if (pumpRunning) {
2918
+ try {
2919
+ await pumpRunning;
2920
+ } catch {
2921
+ }
2922
+ }
2923
+ if (destroyed) return;
2924
+ if (audioDec) {
2925
+ try {
2926
+ await libav.ff_free_decoder?.(audioDec.c, audioDec.pkt, audioDec.frame);
2927
+ } catch {
2928
+ }
2929
+ audioDec = null;
2930
+ }
2931
+ try {
2932
+ const [, c, pkt, frame] = await libav.ff_init_decoder(newStream.codec_id, {
2933
+ codecpar: newStream.codecpar
2934
+ });
2935
+ audioDec = { c, pkt, frame };
2936
+ audioTimeBase = newStream.time_base_num && newStream.time_base_den ? [newStream.time_base_num, newStream.time_base_den] : void 0;
2937
+ } catch (err) {
2938
+ console.warn(
2939
+ "[avbridge] fallback: setAudioTrack init failed \u2014 falling back to no-audio mode:",
2940
+ err.message
2941
+ );
2942
+ audioDec = null;
2943
+ opts.audio.setNoAudio();
2944
+ }
2945
+ audioStream = newStream;
2946
+ try {
2947
+ const tsUs = Math.floor(timeSec * 1e6);
2948
+ const [tsLo, tsHi] = libav.f64toi64 ? libav.f64toi64(tsUs) : [tsUs | 0, Math.floor(tsUs / 4294967296)];
2949
+ await libav.av_seek_frame(
2950
+ fmt_ctx,
2951
+ -1,
2952
+ tsLo,
2953
+ tsHi,
2954
+ libav.AVSEEK_FLAG_BACKWARD ?? 0
2955
+ );
2956
+ } catch (err) {
2957
+ console.warn("[avbridge] fallback: setAudioTrack seek failed:", err);
2958
+ }
2959
+ try {
2960
+ if (videoDec) await libav.avcodec_flush_buffers?.(videoDec.c);
2961
+ } catch {
2962
+ }
2963
+ await flushBSF();
2964
+ syntheticVideoUs = Math.round(timeSec * 1e6);
2965
+ syntheticAudioUs = Math.round(timeSec * 1e6);
2966
+ pumpRunning = pumpLoop(newToken).catch(
2967
+ (err) => console.error("[avbridge] fallback pump failed (post-setAudioTrack):", err)
2968
+ );
2969
+ },
2632
2970
  async seek(timeSec) {
2633
2971
  const newToken = ++pumpToken;
2634
2972
  if (pumpRunning) {
@@ -2685,138 +3023,9 @@ async function startDecoder(opts) {
2685
3023
  }
2686
3024
  };
2687
3025
  }
2688
- function sanitizeFrameTimestamp2(frame, nextUs, fallbackTimeBase) {
2689
- const lo = frame.pts ?? 0;
2690
- const hi = frame.ptshi ?? 0;
2691
- const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
2692
- if (isInvalid) {
2693
- const us2 = nextUs();
2694
- frame.pts = us2;
2695
- frame.ptshi = 0;
2696
- return { timeBase: [1, 1e6] };
2697
- }
2698
- const tb = fallbackTimeBase ?? [1, 1e6];
2699
- const pts64 = hi * 4294967296 + lo;
2700
- const us = Math.round(pts64 * 1e6 * tb[0] / tb[1]);
2701
- if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
2702
- frame.pts = us;
2703
- frame.ptshi = us < 0 ? -1 : 0;
2704
- return { timeBase: [1, 1e6] };
2705
- }
2706
- const fallback = nextUs();
2707
- frame.pts = fallback;
2708
- frame.ptshi = 0;
2709
- return { timeBase: [1, 1e6] };
2710
- }
2711
- var AV_SAMPLE_FMT_U82 = 0;
2712
- var AV_SAMPLE_FMT_S162 = 1;
2713
- var AV_SAMPLE_FMT_S322 = 2;
2714
- var AV_SAMPLE_FMT_FLT2 = 3;
2715
- var AV_SAMPLE_FMT_U8P2 = 5;
2716
- var AV_SAMPLE_FMT_S16P2 = 6;
2717
- var AV_SAMPLE_FMT_S32P2 = 7;
2718
- var AV_SAMPLE_FMT_FLTP2 = 8;
2719
- function libavFrameToInterleavedFloat322(frame) {
2720
- const channels = frame.channels ?? frame.ch_layout_nb_channels ?? 1;
2721
- const sampleRate = frame.sample_rate ?? 44100;
2722
- const nbSamples = frame.nb_samples ?? 0;
2723
- if (nbSamples === 0) return null;
2724
- const out = new Float32Array(nbSamples * channels);
2725
- switch (frame.format) {
2726
- case AV_SAMPLE_FMT_FLTP2: {
2727
- const planes = ensurePlanes2(frame.data, channels);
2728
- for (let ch = 0; ch < channels; ch++) {
2729
- const plane = asFloat322(planes[ch]);
2730
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i];
2731
- }
2732
- return { data: out, channels, sampleRate };
2733
- }
2734
- case AV_SAMPLE_FMT_FLT2: {
2735
- const flat = asFloat322(frame.data);
2736
- for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i];
2737
- return { data: out, channels, sampleRate };
2738
- }
2739
- case AV_SAMPLE_FMT_S16P2: {
2740
- const planes = ensurePlanes2(frame.data, channels);
2741
- for (let ch = 0; ch < channels; ch++) {
2742
- const plane = asInt162(planes[ch]);
2743
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 32768;
2744
- }
2745
- return { data: out, channels, sampleRate };
2746
- }
2747
- case AV_SAMPLE_FMT_S162: {
2748
- const flat = asInt162(frame.data);
2749
- for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 32768;
2750
- return { data: out, channels, sampleRate };
2751
- }
2752
- case AV_SAMPLE_FMT_S32P2: {
2753
- const planes = ensurePlanes2(frame.data, channels);
2754
- for (let ch = 0; ch < channels; ch++) {
2755
- const plane = asInt322(planes[ch]);
2756
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 2147483648;
2757
- }
2758
- return { data: out, channels, sampleRate };
2759
- }
2760
- case AV_SAMPLE_FMT_S322: {
2761
- const flat = asInt322(frame.data);
2762
- for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 2147483648;
2763
- return { data: out, channels, sampleRate };
2764
- }
2765
- case AV_SAMPLE_FMT_U8P2: {
2766
- const planes = ensurePlanes2(frame.data, channels);
2767
- for (let ch = 0; ch < channels; ch++) {
2768
- const plane = asUint82(planes[ch]);
2769
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = (plane[i] - 128) / 128;
2770
- }
2771
- return { data: out, channels, sampleRate };
2772
- }
2773
- case AV_SAMPLE_FMT_U82: {
2774
- const flat = asUint82(frame.data);
2775
- for (let i = 0; i < nbSamples * channels; i++) out[i] = (flat[i] - 128) / 128;
2776
- return { data: out, channels, sampleRate };
2777
- }
2778
- default:
2779
- if (!globalThis.__avbridgeLoggedSampleFmt) {
2780
- globalThis.__avbridgeLoggedSampleFmt = frame.format;
2781
- console.warn(`[avbridge] unsupported audio sample format from libav: ${frame.format}`);
2782
- }
2783
- return null;
2784
- }
2785
- }
2786
- function ensurePlanes2(data, channels) {
2787
- if (Array.isArray(data)) return data;
2788
- const arr = data;
2789
- const len = arr.length;
2790
- const perChannel = Math.floor(len / channels);
2791
- const planes = [];
2792
- for (let ch = 0; ch < channels; ch++) {
2793
- planes.push(arr.subarray ? arr.subarray(ch * perChannel, (ch + 1) * perChannel) : arr);
2794
- }
2795
- return planes;
2796
- }
2797
- function asFloat322(x) {
2798
- if (x instanceof Float32Array) return x;
2799
- const ta = x;
2800
- return new Float32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
2801
- }
2802
- function asInt162(x) {
2803
- if (x instanceof Int16Array) return x;
2804
- const ta = x;
2805
- return new Int16Array(ta.buffer, ta.byteOffset, ta.byteLength / 2);
2806
- }
2807
- function asInt322(x) {
2808
- if (x instanceof Int32Array) return x;
2809
- const ta = x;
2810
- return new Int32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
2811
- }
2812
- function asUint82(x) {
2813
- if (x instanceof Uint8Array) return x;
2814
- const ta = x;
2815
- return new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength);
2816
- }
2817
3026
  async function loadBridge2() {
2818
3027
  try {
2819
- const wrapper = await import('./libav-import-GST2AMPL.cjs');
3028
+ const wrapper = await import('./libav-import-2ZVKV2E7.cjs');
2820
3029
  return wrapper.libavBridge;
2821
3030
  } catch (err) {
2822
3031
  throw new Error(
@@ -2829,7 +3038,7 @@ async function loadBridge2() {
2829
3038
  var READY_AUDIO_BUFFER_SECONDS2 = 0.04;
2830
3039
  var READY_TIMEOUT_SECONDS2 = 3;
2831
3040
  async function createFallbackSession(ctx, target, transport) {
2832
- const { normalizeSource: normalizeSource2 } = await import('./source-VB74JQ7Z.cjs');
3041
+ const { normalizeSource: normalizeSource2 } = await import('./source-7YLO6E7X.cjs');
2833
3042
  const source = await normalizeSource2(ctx.source);
2834
3043
  const fps = ctx.videoTracks[0]?.fps ?? 30;
2835
3044
  const audio = new AudioOutput();
@@ -2882,6 +3091,18 @@ async function createFallbackSession(ctx, target, transport) {
2882
3091
  get: () => ctx.duration ?? NaN
2883
3092
  });
2884
3093
  }
3094
+ Object.defineProperty(target, "readyState", {
3095
+ configurable: true,
3096
+ get: () => {
3097
+ if (!renderer.hasFrames()) return 0;
3098
+ if (!audio.isPlaying() && audio.bufferAhead() <= 0 && !audio.isNoAudio()) return 1;
3099
+ return 2;
3100
+ }
3101
+ });
3102
+ Object.defineProperty(target, "seekable", {
3103
+ configurable: true,
3104
+ get: () => makeTimeRanges(ctx.duration && Number.isFinite(ctx.duration) && ctx.duration > 0 ? [[0, ctx.duration]] : [])
3105
+ });
2885
3106
  async function waitForBuffer() {
2886
3107
  const start = performance.now();
2887
3108
  let firstFrameAtMs = 0;
@@ -2950,7 +3171,24 @@ async function createFallbackSession(ctx, target, transport) {
2950
3171
  async seek(time) {
2951
3172
  await doSeek(time);
2952
3173
  },
2953
- async setAudioTrack(_id) {
3174
+ async setAudioTrack(id) {
3175
+ if (!ctx.audioTracks.some((t) => t.id === id)) {
3176
+ console.warn("[avbridge] fallback: setAudioTrack \u2014 unknown track id", id);
3177
+ return;
3178
+ }
3179
+ const wasPlaying = audio.isPlaying();
3180
+ const currentTime = audio.now();
3181
+ await audio.pause().catch(() => {
3182
+ });
3183
+ await handles.setAudioTrack(id, currentTime).catch(
3184
+ (err) => console.warn("[avbridge] fallback: handles.setAudioTrack failed:", err)
3185
+ );
3186
+ await audio.reset(currentTime);
3187
+ renderer.flush();
3188
+ if (wasPlaying) {
3189
+ await waitForBuffer();
3190
+ await audio.start();
3191
+ }
2954
3192
  },
2955
3193
  async setSubtitleTrack(_id) {
2956
3194
  },
@@ -2967,6 +3205,8 @@ async function createFallbackSession(ctx, target, transport) {
2967
3205
  delete target.paused;
2968
3206
  delete target.volume;
2969
3207
  delete target.muted;
3208
+ delete target.readyState;
3209
+ delete target.seekable;
2970
3210
  } catch {
2971
3211
  }
2972
3212
  },
@@ -3004,119 +3244,6 @@ function registerBuiltins(registry) {
3004
3244
  registry.register(fallbackPlugin);
3005
3245
  }
3006
3246
 
3007
- // src/subtitles/srt.ts
3008
- function srtToVtt(srt) {
3009
- if (srt.charCodeAt(0) === 65279) srt = srt.slice(1);
3010
- const normalized = srt.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
3011
- const blocks = normalized.split(/\n{2,}/);
3012
- const out = ["WEBVTT", ""];
3013
- for (const block of blocks) {
3014
- const lines = block.split("\n");
3015
- if (lines.length > 0 && /^\d+$/.test(lines[0].trim())) {
3016
- lines.shift();
3017
- }
3018
- if (lines.length === 0) continue;
3019
- const timing = lines.shift();
3020
- const vttTiming = convertTiming(timing);
3021
- if (!vttTiming) continue;
3022
- out.push(vttTiming);
3023
- for (const l of lines) out.push(l);
3024
- out.push("");
3025
- }
3026
- return out.join("\n");
3027
- }
3028
- function convertTiming(line) {
3029
- const m = /^(\d{1,2}):(\d{2}):(\d{2})[,.](\d{1,3})\s*-->\s*(\d{1,2}):(\d{2}):(\d{2})[,.](\d{1,3})(.*)$/.exec(
3030
- line.trim()
3031
- );
3032
- if (!m) return null;
3033
- const fmt = (h, mm, s, ms) => `${h.padStart(2, "0")}:${mm}:${s}.${ms.padEnd(3, "0").slice(0, 3)}`;
3034
- return `${fmt(m[1], m[2], m[3], m[4])} --> ${fmt(m[5], m[6], m[7], m[8])}${m[9] ?? ""}`;
3035
- }
3036
-
3037
- // src/subtitles/vtt.ts
3038
- function isVtt(text) {
3039
- const trimmed = text.replace(/^\ufeff/, "").trimStart();
3040
- return trimmed.startsWith("WEBVTT");
3041
- }
3042
-
3043
- // src/subtitles/index.ts
3044
- async function discoverSidecars(file, directory) {
3045
- const baseName = file.name.replace(/\.[^.]+$/, "");
3046
- const found = [];
3047
- for await (const [name, handle] of directory) {
3048
- if (handle.kind !== "file") continue;
3049
- if (!name.startsWith(baseName)) continue;
3050
- const lower = name.toLowerCase();
3051
- let format = null;
3052
- if (lower.endsWith(".srt")) format = "srt";
3053
- else if (lower.endsWith(".vtt")) format = "vtt";
3054
- if (!format) continue;
3055
- const sidecarFile = await handle.getFile();
3056
- const url = URL.createObjectURL(sidecarFile);
3057
- const langMatch = name.slice(baseName.length).match(/[._-]([a-z]{2,3})(?:[._-]|\.)/i);
3058
- found.push({
3059
- url,
3060
- format,
3061
- language: langMatch?.[1]
3062
- });
3063
- }
3064
- return found;
3065
- }
3066
- var SubtitleResourceBag = class {
3067
- urls = /* @__PURE__ */ new Set();
3068
- /** Track an externally-created blob URL (e.g. from `discoverSidecars`). */
3069
- track(url) {
3070
- this.urls.add(url);
3071
- }
3072
- /** Convenience: create a blob URL and track it in one call. */
3073
- createObjectURL(blob) {
3074
- const url = URL.createObjectURL(blob);
3075
- this.urls.add(url);
3076
- return url;
3077
- }
3078
- /** Revoke every tracked URL. Idempotent — safe to call multiple times. */
3079
- revokeAll() {
3080
- for (const u of this.urls) URL.revokeObjectURL(u);
3081
- this.urls.clear();
3082
- }
3083
- };
3084
- async function attachSubtitleTracks(video, tracks, bag, onError, transport) {
3085
- const doFetch = chunkXKPSTC34_cjs.fetchWith(transport);
3086
- for (const t of Array.from(video.querySelectorAll("track[data-avbridge]"))) {
3087
- t.remove();
3088
- }
3089
- for (const t of tracks) {
3090
- if (!t.sidecarUrl) continue;
3091
- try {
3092
- let url = t.sidecarUrl;
3093
- if (t.format === "srt") {
3094
- const res = await doFetch(t.sidecarUrl, transport?.requestInit);
3095
- const text = await res.text();
3096
- const vtt = srtToVtt(text);
3097
- const blob = new Blob([vtt], { type: "text/vtt" });
3098
- url = bag ? bag.createObjectURL(blob) : URL.createObjectURL(blob);
3099
- } else if (t.format === "vtt") {
3100
- const res = await doFetch(t.sidecarUrl, transport?.requestInit);
3101
- const text = await res.text();
3102
- if (!isVtt(text)) {
3103
- console.warn("[avbridge] subtitle missing WEBVTT header:", t.sidecarUrl);
3104
- }
3105
- }
3106
- const trackEl = document.createElement("track");
3107
- trackEl.kind = "subtitles";
3108
- trackEl.src = url;
3109
- trackEl.srclang = t.language ?? "und";
3110
- trackEl.label = t.language ?? `Subtitle ${t.id}`;
3111
- trackEl.dataset.avbridge = "true";
3112
- video.appendChild(trackEl);
3113
- } catch (err) {
3114
- const e = err instanceof Error ? err : new Error(String(err));
3115
- onError?.(e, t);
3116
- }
3117
- }
3118
- }
3119
-
3120
3247
  // src/player.ts
3121
3248
  var UnifiedPlayer = class _UnifiedPlayer {
3122
3249
  /**
@@ -3160,7 +3287,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
3160
3287
  switchingPromise = Promise.resolve();
3161
3288
  // Owns blob URLs created during sidecar discovery + SRT->VTT conversion.
3162
3289
  // Revoked at destroy() so repeated source swaps don't leak.
3163
- subtitleResources = new SubtitleResourceBag();
3290
+ subtitleResources = new chunkS4WAZC2T_cjs.SubtitleResourceBag();
3164
3291
  // Transport config extracted from CreatePlayerOptions. Threaded to probe,
3165
3292
  // subtitle fetches, and strategy session creators. Not stored on MediaContext
3166
3293
  // because it's runtime config, not media analysis.
@@ -3206,7 +3333,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
3206
3333
  }
3207
3334
  }
3208
3335
  if (this.options.directory && this.options.source instanceof File) {
3209
- const found = await discoverSidecars(this.options.source, this.options.directory);
3336
+ const found = await chunkS4WAZC2T_cjs.discoverSidecars(this.options.source, this.options.directory);
3210
3337
  for (const s of found) {
3211
3338
  this.subtitleResources.track(s.url);
3212
3339
  ctx.subtitleTracks.push({
@@ -3229,17 +3356,15 @@ var UnifiedPlayer = class _UnifiedPlayer {
3229
3356
  reason: decision.reason
3230
3357
  });
3231
3358
  await this.startSession(decision.strategy, decision.reason);
3232
- if (this.session.strategy !== "fallback" && this.session.strategy !== "hybrid") {
3233
- await attachSubtitleTracks(
3234
- this.options.target,
3235
- ctx.subtitleTracks,
3236
- this.subtitleResources,
3237
- (err, track) => {
3238
- console.warn(`[avbridge] subtitle ${track.id} failed: ${err.message}`);
3239
- },
3240
- this.transport
3241
- );
3242
- }
3359
+ await chunkS4WAZC2T_cjs.attachSubtitleTracks(
3360
+ this.options.target,
3361
+ ctx.subtitleTracks,
3362
+ this.subtitleResources,
3363
+ (err, track) => {
3364
+ console.warn(`[avbridge] subtitle ${track.id} failed: ${err.message}`);
3365
+ },
3366
+ this.transport
3367
+ );
3243
3368
  this.emitter.emitSticky("tracks", {
3244
3369
  video: ctx.videoTracks,
3245
3370
  audio: ctx.audioTracks,
@@ -3374,8 +3499,8 @@ var UnifiedPlayer = class _UnifiedPlayer {
3374
3499
  }
3375
3500
  return;
3376
3501
  }
3377
- this.emitter.emit("error", new chunkXKPSTC34_cjs.AvbridgeError(
3378
- chunkXKPSTC34_cjs.ERR_ALL_STRATEGIES_EXHAUSTED,
3502
+ this.emitter.emit("error", new chunk2XW2O3YI_cjs.AvbridgeError(
3503
+ chunk2XW2O3YI_cjs.ERR_ALL_STRATEGIES_EXHAUSTED,
3379
3504
  `All playback strategies failed: ${errors.join("; ")}`,
3380
3505
  "This file may require a codec or container that isn't available in this browser. Try the fallback strategy or check browser codec support."
3381
3506
  ));
@@ -3429,7 +3554,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
3429
3554
  // ── Public: manual strategy switch ────────────────────────────────────
3430
3555
  /** Manually switch to a different playback strategy. Preserves current position and play/pause state. Concurrent calls are serialized. */
3431
3556
  async setStrategy(strategy, reason) {
3432
- if (!this.mediaContext) throw new chunkXKPSTC34_cjs.AvbridgeError(chunkXKPSTC34_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.");
3557
+ if (!this.mediaContext) throw new chunk2XW2O3YI_cjs.AvbridgeError(chunk2XW2O3YI_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.");
3433
3558
  if (this.session?.strategy === strategy) return;
3434
3559
  this.switchingPromise = this.switchingPromise.then(
3435
3560
  () => this.doSetStrategy(strategy, reason)
@@ -3492,7 +3617,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
3492
3617
  }
3493
3618
  /** Begin or resume playback. Throws if the player is not ready. */
3494
3619
  async play() {
3495
- if (!this.session) throw new chunkXKPSTC34_cjs.AvbridgeError(chunkXKPSTC34_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.");
3620
+ if (!this.session) throw new chunk2XW2O3YI_cjs.AvbridgeError(chunk2XW2O3YI_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.");
3496
3621
  this.userIntent = "play";
3497
3622
  this.autoPausedForVisibility = false;
3498
3623
  await this.session.play();
@@ -3531,17 +3656,17 @@ var UnifiedPlayer = class _UnifiedPlayer {
3531
3656
  }
3532
3657
  /** Seek to the given time in seconds. Throws if the player is not ready. */
3533
3658
  async seek(time) {
3534
- if (!this.session) throw new chunkXKPSTC34_cjs.AvbridgeError(chunkXKPSTC34_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.");
3659
+ if (!this.session) throw new chunk2XW2O3YI_cjs.AvbridgeError(chunk2XW2O3YI_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.");
3535
3660
  await this.session.seek(time);
3536
3661
  }
3537
3662
  /** Switch the active audio track by track ID. Throws if the player is not ready. */
3538
3663
  async setAudioTrack(id) {
3539
- if (!this.session) throw new chunkXKPSTC34_cjs.AvbridgeError(chunkXKPSTC34_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.");
3664
+ if (!this.session) throw new chunk2XW2O3YI_cjs.AvbridgeError(chunk2XW2O3YI_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.");
3540
3665
  await this.session.setAudioTrack(id);
3541
3666
  }
3542
3667
  /** Switch the active subtitle track by track ID, or pass `null` to disable subtitles. */
3543
3668
  async setSubtitleTrack(id) {
3544
- if (!this.session) throw new chunkXKPSTC34_cjs.AvbridgeError(chunkXKPSTC34_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.");
3669
+ if (!this.session) throw new chunk2XW2O3YI_cjs.AvbridgeError(chunk2XW2O3YI_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.");
3545
3670
  await this.session.setSubtitleTrack(id);
3546
3671
  }
3547
3672
  /** Return a snapshot of current diagnostics: container, codecs, strategy, runtime stats, and strategy history. */
@@ -3705,7 +3830,20 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
3705
3830
  _strategy = null;
3706
3831
  _strategyClass = null;
3707
3832
  _audioTracks = [];
3833
+ /** Subtitle tracks reported by the active UnifiedPlayer (options.subtitles
3834
+ * + embedded container tracks + programmatic addSubtitle calls). */
3708
3835
  _subtitleTracks = [];
3836
+ /** Subtitle tracks derived from light-DOM `<track>` children. Maintained
3837
+ * by _syncTextTracks on every mutation. Merged into the public
3838
+ * `subtitleTracks` getter so the player's settings menu sees them. */
3839
+ _htmlTrackInfo = [];
3840
+ /**
3841
+ * External subtitle list forwarded to `createPlayer()` on the next
3842
+ * bootstrap. Setting this after bootstrap queues it for the next
3843
+ * source change; consumers that need to swap subtitles mid-playback
3844
+ * should set `source` to reload.
3845
+ */
3846
+ _subtitles = null;
3709
3847
  /**
3710
3848
  * Initial strategy preference. `"auto"` means "let the classifier decide";
3711
3849
  * any other value is passed to `createPlayer({ initialStrategy })` and
@@ -3818,12 +3956,28 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
3818
3956
  _syncTextTracks() {
3819
3957
  const existing = this._videoEl.querySelectorAll("track");
3820
3958
  for (const t of Array.from(existing)) t.remove();
3959
+ this._htmlTrackInfo = [];
3960
+ let htmlIdx = 0;
3821
3961
  for (const child of Array.from(this.children)) {
3822
3962
  if (child.tagName === "TRACK") {
3823
- const clone = child.cloneNode(true);
3963
+ const track = child;
3964
+ const clone = track.cloneNode(true);
3824
3965
  this._videoEl.appendChild(clone);
3966
+ const src = track.getAttribute("src") ?? void 0;
3967
+ const format = src?.toLowerCase().endsWith(".srt") ? "srt" : "vtt";
3968
+ this._htmlTrackInfo.push({
3969
+ id: 1e4 + htmlIdx,
3970
+ format,
3971
+ language: track.srclang || track.getAttribute("label") || void 0,
3972
+ sidecarUrl: src
3973
+ });
3974
+ htmlIdx++;
3825
3975
  }
3826
3976
  }
3977
+ this._dispatch("trackschange", {
3978
+ audioTracks: this._audioTracks,
3979
+ subtitleTracks: this.subtitleTracks
3980
+ });
3827
3981
  }
3828
3982
  /** Internal src setter — separate from the property setter so the
3829
3983
  * attributeChangedCallback can use it without re-entering reflection. */
@@ -3861,7 +4015,8 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
3861
4015
  // Honor the consumer's preferred initial strategy. "auto" means
3862
4016
  // "let the classifier decide" — the createPlayer call simply doesn't
3863
4017
  // pass initialStrategy in that case.
3864
- ...this._preferredStrategy !== "auto" ? { initialStrategy: this._preferredStrategy } : {}
4018
+ ...this._preferredStrategy !== "auto" ? { initialStrategy: this._preferredStrategy } : {},
4019
+ ...this._subtitles ? { subtitles: this._subtitles } : {}
3865
4020
  });
3866
4021
  } catch (err) {
3867
4022
  if (id !== this._bootstrapId || this._destroyed) return;
@@ -4150,7 +4305,48 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
4150
4305
  return this._audioTracks;
4151
4306
  }
4152
4307
  get subtitleTracks() {
4153
- return this._subtitleTracks;
4308
+ return this._htmlTrackInfo.length === 0 ? this._subtitleTracks : [...this._subtitleTracks, ...this._htmlTrackInfo];
4309
+ }
4310
+ /**
4311
+ * External subtitle files to attach when the source loads. Takes effect
4312
+ * on the next bootstrap — set before assigning `source`, or reload via
4313
+ * `load()` after changing. For dynamic post-bootstrap addition, use
4314
+ * `addSubtitle()` instead.
4315
+ *
4316
+ * @example
4317
+ * el.subtitles = [{ url: "/en.srt", format: "srt", language: "en" }];
4318
+ * el.src = "/movie.mp4";
4319
+ */
4320
+ get subtitles() {
4321
+ return this._subtitles;
4322
+ }
4323
+ set subtitles(value) {
4324
+ this._subtitles = value;
4325
+ }
4326
+ /**
4327
+ * Attach a subtitle track to the current playback without rebuilding
4328
+ * the player. Works while the element is playing — converts SRT to
4329
+ * VTT if needed, adds a `<track>` to the inner `<video>`. Canvas
4330
+ * strategies pick up the new track via their textTracks watcher.
4331
+ */
4332
+ async addSubtitle(subtitle) {
4333
+ const { attachSubtitleTracks: attachSubtitleTracks2 } = await import('./subtitles-QUH4LPI4.cjs');
4334
+ const format = subtitle.format ?? (subtitle.url.endsWith(".srt") ? "srt" : "vtt");
4335
+ const track = {
4336
+ id: this._subtitleTracks.length,
4337
+ format,
4338
+ language: subtitle.language,
4339
+ sidecarUrl: subtitle.url
4340
+ };
4341
+ this._subtitleTracks.push(track);
4342
+ await attachSubtitleTracks2(
4343
+ this._videoEl,
4344
+ this._subtitleTracks,
4345
+ void 0,
4346
+ (err, t) => {
4347
+ console.warn(`[avbridge] subtitle ${t.id} failed: ${err.message}`);
4348
+ }
4349
+ );
4154
4350
  }
4155
4351
  // ── Public methods ─────────────────────────────────────────────────────
4156
4352
  /** Force a (re-)bootstrap if a source is currently set. */
@@ -4198,6 +4394,12 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
4198
4394
  getDiagnostics() {
4199
4395
  return this._player?.getDiagnostics() ?? null;
4200
4396
  }
4397
+ addEventListener(type, listener, options) {
4398
+ super.addEventListener(type, listener, options);
4399
+ }
4400
+ removeEventListener(type, listener, options) {
4401
+ super.removeEventListener(type, listener, options);
4402
+ }
4201
4403
  // ── Event helpers ──────────────────────────────────────────────────────
4202
4404
  _dispatch(name, detail) {
4203
4405
  this.dispatchEvent(new CustomEvent(name, { detail, bubbles: false }));
@@ -4246,6 +4448,18 @@ var PLAYER_STYLES = (
4246
4448
  height: 100%;
4247
4449
  }
4248
4450
 
4451
+ /* Drag-and-drop file target highlight. */
4452
+ .avp.avp-dragover::after {
4453
+ content: "";
4454
+ position: absolute;
4455
+ inset: 8px;
4456
+ border: 2px dashed rgba(255, 255, 255, 0.75);
4457
+ border-radius: 4px;
4458
+ background: rgba(0, 0, 0, 0.25);
4459
+ pointer-events: none;
4460
+ z-index: 10;
4461
+ }
4462
+
4249
4463
  /* \u2500\u2500 Center overlay \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4250
4464
 
4251
4465
  .avp-overlay {
@@ -4937,6 +5151,31 @@ var AvbridgePlayerElement = class extends HTMLElement {
4937
5151
  on(container, "pointerdown", (e) => this._onPointerDown(e));
4938
5152
  on(container, "pointerup", (e) => this._onPointerUp(e));
4939
5153
  on(container, "pointercancel", () => this._cancelHold());
5154
+ on(container, "dragenter", (e) => {
5155
+ e.preventDefault();
5156
+ const dt = e.dataTransfer;
5157
+ if (!dt || !Array.from(dt.types).includes("Files")) return;
5158
+ container.classList.add("avp-dragover");
5159
+ });
5160
+ on(container, "dragover", (e) => {
5161
+ e.preventDefault();
5162
+ const dt = e.dataTransfer;
5163
+ if (dt) dt.dropEffect = "copy";
5164
+ });
5165
+ on(container, "dragleave", (e) => {
5166
+ if (e.target === container) {
5167
+ container.classList.remove("avp-dragover");
5168
+ }
5169
+ });
5170
+ on(container, "drop", (e) => {
5171
+ e.preventDefault();
5172
+ container.classList.remove("avp-dragover");
5173
+ const file = e.dataTransfer?.files?.[0];
5174
+ if (!file) return;
5175
+ this._video.source = file;
5176
+ void this._video.play().catch(() => {
5177
+ });
5178
+ });
4940
5179
  on(this, "keydown", (e) => this._onKeydown(e));
4941
5180
  if (!this.hasAttribute("tabindex")) {
4942
5181
  this.setAttribute("tabindex", "0");
@@ -5454,6 +5693,20 @@ var AvbridgePlayerElement = class extends HTMLElement {
5454
5693
  get subtitleTracks() {
5455
5694
  return this._video.subtitleTracks ?? [];
5456
5695
  }
5696
+ /**
5697
+ * External subtitle files to attach when the source loads. Forwarded
5698
+ * to the inner <avbridge-video>. Takes effect on next bootstrap.
5699
+ */
5700
+ get subtitles() {
5701
+ return this._video.subtitles;
5702
+ }
5703
+ set subtitles(value) {
5704
+ this._video.subtitles = value;
5705
+ }
5706
+ /** Attach a subtitle track to the current playback without a reload. */
5707
+ async addSubtitle(subtitle) {
5708
+ return this._video.addSubtitle(subtitle);
5709
+ }
5457
5710
  get player() {
5458
5711
  return this._video.player;
5459
5712
  }
@@ -5488,6 +5741,12 @@ var AvbridgePlayerElement = class extends HTMLElement {
5488
5741
  canPlayType(mime) {
5489
5742
  return this._video.canPlayType(mime);
5490
5743
  }
5744
+ addEventListener(type, listener, options) {
5745
+ super.addEventListener(type, listener, options);
5746
+ }
5747
+ removeEventListener(type, listener, options) {
5748
+ super.removeEventListener(type, listener, options);
5749
+ }
5491
5750
  };
5492
5751
 
5493
5752
  // src/player-element.ts