avbridge 2.3.0 → 2.5.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 (104) hide show
  1. package/CHANGELOG.md +73 -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-2PGRFCWB.js → chunk-CPJLFFCC.js} +8 -18
  9. package/dist/chunk-CPJLFFCC.js.map +1 -0
  10. package/dist/chunk-CPZ7PXAM.cjs +240 -0
  11. package/dist/chunk-CPZ7PXAM.cjs.map +1 -0
  12. package/dist/{chunk-QQXBPW72.js → chunk-E76AMWI4.js} +4 -18
  13. package/dist/chunk-E76AMWI4.js.map +1 -0
  14. package/dist/{chunk-NV7ILLWH.js → chunk-KY2GPCT7.js} +347 -665
  15. package/dist/chunk-KY2GPCT7.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-Q2VUO52Z.cjs +374 -0
  19. package/dist/chunk-Q2VUO52Z.cjs.map +1 -0
  20. package/dist/chunk-QDJLQR53.cjs +22 -0
  21. package/dist/chunk-QDJLQR53.cjs.map +1 -0
  22. package/dist/chunk-S4WAZC2T.cjs +173 -0
  23. package/dist/chunk-S4WAZC2T.cjs.map +1 -0
  24. package/dist/chunk-SMH6IOP2.js +368 -0
  25. package/dist/chunk-SMH6IOP2.js.map +1 -0
  26. package/dist/chunk-SR3MPV4D.js +237 -0
  27. package/dist/chunk-SR3MPV4D.js.map +1 -0
  28. package/dist/{chunk-7RGG6ME7.cjs → chunk-TBW26OPP.cjs} +365 -688
  29. package/dist/chunk-TBW26OPP.cjs.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 +799 -493
  35. package/dist/element-browser.js.map +1 -1
  36. package/dist/element.cjs +58 -4
  37. package/dist/element.cjs.map +1 -1
  38. package/dist/element.d.cts +38 -0
  39. package/dist/element.d.ts +38 -0
  40. package/dist/element.js +57 -3
  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.js +494 -366
  45. package/dist/index.js.map +1 -1
  46. package/dist/libav-demux-H2GS46GH.cjs +27 -0
  47. package/dist/libav-demux-H2GS46GH.cjs.map +1 -0
  48. package/dist/libav-demux-OWZ4T2YW.js +6 -0
  49. package/dist/libav-demux-OWZ4T2YW.js.map +1 -0
  50. package/dist/{libav-import-GST2AMPL.cjs → libav-import-2ZVKV2E7.cjs} +2 -2
  51. package/dist/{libav-import-GST2AMPL.cjs.map → libav-import-2ZVKV2E7.cjs.map} +1 -1
  52. package/dist/{libav-import-2JURFHEW.js → libav-import-6MGLCXVQ.js} +2 -2
  53. package/dist/{libav-import-2JURFHEW.js.map → libav-import-6MGLCXVQ.js.map} +1 -1
  54. package/dist/player.cjs +601 -470
  55. package/dist/player.cjs.map +1 -1
  56. package/dist/player.d.cts +50 -0
  57. package/dist/player.d.ts +50 -0
  58. package/dist/player.js +580 -449
  59. package/dist/player.js.map +1 -1
  60. package/dist/remux-OBSMIENG.cjs +35 -0
  61. package/dist/remux-OBSMIENG.cjs.map +1 -0
  62. package/dist/remux-WBYIZBBX.js +10 -0
  63. package/dist/remux-WBYIZBBX.js.map +1 -0
  64. package/dist/source-4TZ6KMNV.js +4 -0
  65. package/dist/{source-F656KYYV.js.map → source-4TZ6KMNV.js.map} +1 -1
  66. package/dist/source-7YLO6E7X.cjs +29 -0
  67. package/dist/{source-73CAH6HW.cjs.map → source-7YLO6E7X.cjs.map} +1 -1
  68. package/dist/source-MTX5ELUZ.js +4 -0
  69. package/dist/{source-QJR3OHTW.js.map → source-MTX5ELUZ.js.map} +1 -1
  70. package/dist/source-VFLXLOCN.cjs +29 -0
  71. package/dist/{source-VB74JQ7Z.cjs.map → source-VFLXLOCN.cjs.map} +1 -1
  72. package/dist/subtitles-4T74JRGT.js +4 -0
  73. package/dist/subtitles-4T74JRGT.js.map +1 -0
  74. package/dist/subtitles-QUH4LPI4.cjs +29 -0
  75. package/dist/subtitles-QUH4LPI4.cjs.map +1 -0
  76. package/package.json +1 -1
  77. package/src/convert/remux.ts +1 -35
  78. package/src/convert/transcode-libav.ts +691 -0
  79. package/src/convert/transcode.ts +12 -4
  80. package/src/element/avbridge-player.ts +16 -0
  81. package/src/element/avbridge-video.ts +54 -0
  82. package/src/errors.ts +6 -0
  83. package/src/player.ts +15 -16
  84. package/src/strategies/fallback/decoder.ts +96 -173
  85. package/src/strategies/fallback/index.ts +19 -2
  86. package/src/strategies/fallback/libav-import.ts +9 -1
  87. package/src/strategies/fallback/video-renderer.ts +107 -0
  88. package/src/strategies/hybrid/decoder.ts +88 -180
  89. package/src/strategies/hybrid/index.ts +17 -2
  90. package/src/strategies/native.ts +6 -3
  91. package/src/strategies/remux/index.ts +14 -2
  92. package/src/strategies/remux/pipeline.ts +72 -12
  93. package/src/subtitles/render.ts +8 -0
  94. package/src/util/libav-demux.ts +405 -0
  95. package/dist/chunk-2PGRFCWB.js.map +0 -1
  96. package/dist/chunk-6UUT4BEA.cjs.map +0 -1
  97. package/dist/chunk-7RGG6ME7.cjs.map +0 -1
  98. package/dist/chunk-NV7ILLWH.js.map +0 -1
  99. package/dist/chunk-QQXBPW72.js.map +0 -1
  100. package/dist/chunk-XKPSTC34.cjs.map +0 -1
  101. package/dist/source-73CAH6HW.cjs +0 -28
  102. package/dist/source-F656KYYV.js +0 -3
  103. package/dist/source-QJR3OHTW.js +0 -3
  104. package/dist/source-VB74JQ7Z.cjs +0 -28
package/dist/player.js CHANGED
@@ -1,5 +1,8 @@
1
- import { AvbridgeError, ERR_ALL_STRATEGIES_EXHAUSTED, ERR_PLAYER_NOT_READY, normalizeSource, sniffNormalizedSource, ERR_PROBE_FAILED, ERR_PROBE_UNKNOWN_CONTAINER, ERR_LIBAV_NOT_REACHABLE, fetchWith, ERR_MSE_NOT_SUPPORTED, ERR_MSE_CODEC_NOT_SUPPORTED } from './chunk-QQXBPW72.js';
1
+ import { AvbridgeError, ERR_ALL_STRATEGIES_EXHAUSTED, ERR_PLAYER_NOT_READY, normalizeSource, sniffNormalizedSource, ERR_PROBE_FAILED, ERR_PROBE_UNKNOWN_CONTAINER, ERR_LIBAV_NOT_REACHABLE, ERR_MSE_NOT_SUPPORTED, ERR_MSE_CODEC_NOT_SUPPORTED } from './chunk-E76AMWI4.js';
2
2
  import { dbg, loadLibav } from './chunk-IAYKFGFG.js';
3
+ import './chunk-DCSOQH2N.js';
4
+ import { SubtitleResourceBag, discoverSidecars, attachSubtitleTracks, SubtitleOverlay } from './chunk-5KVLE6YI.js';
5
+ import './chunk-LUFA47FP.js';
3
6
 
4
7
  // src/events.ts
5
8
  var TypedEmitter = class {
@@ -687,7 +690,7 @@ async function createNativeSession(context, video) {
687
690
  },
688
691
  async setAudioTrack(id) {
689
692
  const tracks = video.audioTracks;
690
- if (!tracks) return;
693
+ if (!tracks || tracks.length === 0) return;
691
694
  for (let i = 0; i < tracks.length; i++) {
692
695
  tracks[i].enabled = tracks[i].id === String(id) || i === id;
693
696
  }
@@ -935,30 +938,49 @@ var MseSink = class {
935
938
  async function createRemuxPipeline(ctx, video) {
936
939
  const mb = await import('mediabunny');
937
940
  const videoTrackInfo = ctx.videoTracks[0];
938
- const audioTrackInfo = ctx.audioTracks[0];
939
941
  if (!videoTrackInfo) throw new Error("remux: source has no video track");
940
942
  const mbVideoCodec = avbridgeVideoToMediabunny(videoTrackInfo.codec);
941
943
  if (!mbVideoCodec) {
942
944
  throw new Error(`remux: video codec "${videoTrackInfo.codec}" is not supported by mediabunny output`);
943
945
  }
944
- const mbAudioCodec = audioTrackInfo ? avbridgeAudioToMediabunny(audioTrackInfo.codec) : null;
945
946
  const input = new mb.Input({
946
947
  source: await buildMediabunnySourceFromInput(mb, ctx.source),
947
948
  formats: mb.ALL_FORMATS
948
949
  });
949
950
  const allTracks = await input.getTracks();
950
951
  const inputVideo = allTracks.find((t) => t.id === videoTrackInfo.id && t.isVideoTrack());
951
- const inputAudio = audioTrackInfo ? allTracks.find((t) => t.id === audioTrackInfo.id && t.isAudioTrack()) : null;
952
952
  if (!inputVideo || !inputVideo.isVideoTrack()) {
953
953
  throw new Error("remux: video track not found in input");
954
954
  }
955
- if (audioTrackInfo && (!inputAudio || !inputAudio.isAudioTrack())) {
956
- throw new Error("remux: audio track not found in input");
957
- }
958
955
  const videoConfig = await inputVideo.getDecoderConfig();
959
- const audioConfig = inputAudio && inputAudio.isAudioTrack() ? await inputAudio.getDecoderConfig() : null;
960
956
  const videoSink = new mb.EncodedPacketSink(inputVideo);
961
- const audioSink = inputAudio?.isAudioTrack() ? new mb.EncodedPacketSink(inputAudio) : null;
957
+ let selectedAudioTrackId = ctx.audioTracks[0]?.id ?? null;
958
+ let inputAudio = null;
959
+ let mbAudioCodec = null;
960
+ let audioSink = null;
961
+ let audioConfig = null;
962
+ async function rebuildAudio() {
963
+ if (selectedAudioTrackId == null) {
964
+ inputAudio = null;
965
+ mbAudioCodec = null;
966
+ audioSink = null;
967
+ audioConfig = null;
968
+ return;
969
+ }
970
+ const trackInfo = ctx.audioTracks.find((t) => t.id === selectedAudioTrackId);
971
+ if (!trackInfo) {
972
+ throw new Error(`remux: no audio track with id ${selectedAudioTrackId}`);
973
+ }
974
+ const newInput = allTracks.find((t) => t.id === trackInfo.id && t.isAudioTrack());
975
+ if (!newInput || !newInput.isAudioTrack()) {
976
+ throw new Error("remux: audio track not found in input");
977
+ }
978
+ inputAudio = newInput;
979
+ mbAudioCodec = avbridgeAudioToMediabunny(trackInfo.codec);
980
+ audioSink = new mb.EncodedPacketSink(newInput);
981
+ audioConfig = await newInput.getDecoderConfig();
982
+ }
983
+ await rebuildAudio();
962
984
  let sink = null;
963
985
  const stats = { videoPackets: 0, audioPackets: 0, bytesWritten: 0, fragments: 0 };
964
986
  let destroyed = false;
@@ -1083,6 +1105,30 @@ async function createRemuxPipeline(ctx, video) {
1083
1105
  pendingAutoPlay = autoPlay;
1084
1106
  if (sink) sink.setPlayOnSeek(autoPlay);
1085
1107
  },
1108
+ async setAudioTrack(trackId, time, autoPlay) {
1109
+ if (selectedAudioTrackId === trackId) return;
1110
+ if (!ctx.audioTracks.some((t) => t.id === trackId)) {
1111
+ console.warn("[avbridge] remux: setAudioTrack \u2014 unknown track id", trackId);
1112
+ return;
1113
+ }
1114
+ pumpToken++;
1115
+ selectedAudioTrackId = trackId;
1116
+ await rebuildAudio().catch((err) => {
1117
+ console.warn("[avbridge] remux: rebuildAudio failed:", err.message);
1118
+ });
1119
+ if (sink) {
1120
+ try {
1121
+ sink.destroy();
1122
+ } catch {
1123
+ }
1124
+ sink = null;
1125
+ }
1126
+ pendingAutoPlay = autoPlay;
1127
+ pendingStartTime = time;
1128
+ pumpLoop(++pumpToken, time).catch((err) => {
1129
+ console.error("[avbridge] remux pipeline setAudioTrack pump failed:", err);
1130
+ });
1131
+ },
1086
1132
  async destroy() {
1087
1133
  destroyed = true;
1088
1134
  pumpToken++;
@@ -1142,7 +1188,19 @@ async function createRemuxSession(context, video) {
1142
1188
  const wasPlaying = !video.paused;
1143
1189
  await pipeline.seek(time, wasPlaying || wantPlay);
1144
1190
  },
1145
- async setAudioTrack(_id) {
1191
+ async setAudioTrack(id) {
1192
+ if (!context.audioTracks.some((t) => t.id === id)) {
1193
+ console.warn("[avbridge] remux: setAudioTrack \u2014 unknown track id", id);
1194
+ return;
1195
+ }
1196
+ const wasPlaying = !video.paused;
1197
+ const time = video.currentTime || 0;
1198
+ if (!started) {
1199
+ started = true;
1200
+ await pipeline.setAudioTrack(id, time, wantPlay || wasPlaying);
1201
+ return;
1202
+ }
1203
+ await pipeline.setAudioTrack(id, time, wasPlaying || wantPlay);
1146
1204
  },
1147
1205
  async setSubtitleTrack(id) {
1148
1206
  const tracks = video.textTracks;
@@ -1195,6 +1253,9 @@ var VideoRenderer = class {
1195
1253
  document.body.appendChild(this.canvas);
1196
1254
  }
1197
1255
  target.style.visibility = "hidden";
1256
+ const overlayParent = parent instanceof HTMLElement ? parent : document.body;
1257
+ this.subtitleOverlay = new SubtitleOverlay(overlayParent);
1258
+ this.watchTextTracks(target);
1198
1259
  const ctx = this.canvas.getContext("2d");
1199
1260
  if (!ctx) throw new Error("video renderer: failed to acquire 2D context");
1200
1261
  this.ctx = ctx;
@@ -1220,6 +1281,15 @@ var VideoRenderer = class {
1220
1281
  ticksWaiting = 0;
1221
1282
  /** Cumulative count of ticks where PTS mode painted a frame. */
1222
1283
  ticksPainted = 0;
1284
+ /**
1285
+ * Subtitle overlay div attached to the stage wrapper alongside the
1286
+ * canvas. Created lazily when subtitle tracks are attached via the
1287
+ * target's `<track>` children. Canvas strategies (hybrid, fallback)
1288
+ * hide the <video>, so we can't rely on the browser's native cue
1289
+ * rendering; we read TextTrack.cues and render into this overlay.
1290
+ */
1291
+ subtitleOverlay = null;
1292
+ subtitleTrack = null;
1223
1293
  /**
1224
1294
  * Calibration offset (microseconds) between video PTS and audio clock.
1225
1295
  * Video PTS and AudioContext.currentTime can drift ~0.1% relative to
@@ -1263,9 +1333,80 @@ var VideoRenderer = class {
1263
1333
  this.framesDroppedOverflow++;
1264
1334
  }
1265
1335
  }
1336
+ /**
1337
+ * Watch the target <video>'s textTracks list. When a track is added,
1338
+ * grab it and start polling cues on each render tick. Existing tracks
1339
+ * (if any) are picked up immediately.
1340
+ */
1341
+ watchTextTracks(target) {
1342
+ const pick = () => {
1343
+ if (this.subtitleTrack) return;
1344
+ const tracks = target.textTracks;
1345
+ if (isDebug()) {
1346
+ console.log(`[avbridge:subs] watchTextTracks pick() \u2014 ${tracks.length} tracks`);
1347
+ }
1348
+ for (let i = 0; i < tracks.length; i++) {
1349
+ const t = tracks[i];
1350
+ if (isDebug()) {
1351
+ console.log(`[avbridge:subs] track ${i}: kind=${t.kind} mode=${t.mode} cues=${t.cues?.length ?? 0}`);
1352
+ }
1353
+ if (t.kind === "subtitles" || t.kind === "captions") {
1354
+ this.subtitleTrack = t;
1355
+ t.mode = "hidden";
1356
+ if (isDebug()) {
1357
+ console.log(`[avbridge:subs] picked track, mode=hidden`);
1358
+ }
1359
+ const trackEl = target.querySelector(`track[srclang="${t.language}"]`);
1360
+ if (trackEl) {
1361
+ trackEl.addEventListener("load", () => {
1362
+ if (isDebug()) {
1363
+ console.log(`[avbridge:subs] track element loaded, cues=${t.cues?.length ?? 0}`);
1364
+ }
1365
+ });
1366
+ trackEl.addEventListener("error", (ev) => {
1367
+ console.warn(`[avbridge:subs] track element error:`, ev);
1368
+ });
1369
+ }
1370
+ break;
1371
+ }
1372
+ }
1373
+ };
1374
+ pick();
1375
+ if (typeof target.textTracks.addEventListener === "function") {
1376
+ target.textTracks.addEventListener("addtrack", (e) => {
1377
+ if (isDebug()) {
1378
+ console.log("[avbridge:subs] addtrack event fired");
1379
+ }
1380
+ pick();
1381
+ });
1382
+ }
1383
+ }
1384
+ _loggedCues = false;
1385
+ /** Find the active cue (if any) for the given media time. */
1386
+ updateSubtitles() {
1387
+ if (!this.subtitleOverlay || !this.subtitleTrack) return;
1388
+ const cues = this.subtitleTrack.cues;
1389
+ if (!cues || cues.length === 0) return;
1390
+ if (isDebug() && !this._loggedCues) {
1391
+ this._loggedCues = true;
1392
+ console.log(`[avbridge:subs] cues available: ${cues.length}, first start=${cues[0].startTime}, last end=${cues[cues.length - 1].endTime}`);
1393
+ }
1394
+ const t = this.clock.now();
1395
+ let activeText = "";
1396
+ for (let i = 0; i < cues.length; i++) {
1397
+ const c = cues[i];
1398
+ if (t >= c.startTime && t <= c.endTime) {
1399
+ const vttCue = c;
1400
+ activeText = vttCue.text ?? "";
1401
+ break;
1402
+ }
1403
+ }
1404
+ this.subtitleOverlay.setText(activeText.replace(/<[^>]+>/g, ""));
1405
+ }
1266
1406
  tick() {
1267
1407
  if (this.destroyed) return;
1268
1408
  this.rafHandle = requestAnimationFrame(this.tick);
1409
+ this.updateSubtitles();
1269
1410
  if (this.queue.length === 0) return;
1270
1411
  const playing = this.clock.isPlaying();
1271
1412
  if (!playing) {
@@ -1394,6 +1535,11 @@ var VideoRenderer = class {
1394
1535
  this.destroyed = true;
1395
1536
  if (this.rafHandle != null) cancelAnimationFrame(this.rafHandle);
1396
1537
  this.flush();
1538
+ if (this.subtitleOverlay) {
1539
+ this.subtitleOverlay.destroy();
1540
+ this.subtitleOverlay = null;
1541
+ }
1542
+ this.subtitleTrack = null;
1397
1543
  this.canvas.remove();
1398
1544
  this.target.style.visibility = "";
1399
1545
  }
@@ -1648,6 +1794,160 @@ function pickLibavVariant(ctx) {
1648
1794
  return "webcodecs";
1649
1795
  }
1650
1796
 
1797
+ // src/util/libav-demux.ts
1798
+ function sanitizePacketTimestamp(pkt, nextUs, fallbackTimeBase) {
1799
+ const lo = pkt.pts ?? 0;
1800
+ const hi = pkt.ptshi ?? 0;
1801
+ const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
1802
+ if (isInvalid) {
1803
+ const us2 = nextUs();
1804
+ pkt.pts = us2;
1805
+ pkt.ptshi = 0;
1806
+ pkt.time_base_num = 1;
1807
+ pkt.time_base_den = 1e6;
1808
+ return;
1809
+ }
1810
+ const tb = fallbackTimeBase ?? [1, 1e6];
1811
+ const pts64 = hi * 4294967296 + lo;
1812
+ const us = Math.round(pts64 * 1e6 * tb[0] / tb[1]);
1813
+ if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
1814
+ pkt.pts = us;
1815
+ pkt.ptshi = us < 0 ? -1 : 0;
1816
+ pkt.time_base_num = 1;
1817
+ pkt.time_base_den = 1e6;
1818
+ return;
1819
+ }
1820
+ const fallback = nextUs();
1821
+ pkt.pts = fallback;
1822
+ pkt.ptshi = 0;
1823
+ pkt.time_base_num = 1;
1824
+ pkt.time_base_den = 1e6;
1825
+ }
1826
+ var AV_SAMPLE_FMT_U8 = 0;
1827
+ var AV_SAMPLE_FMT_S16 = 1;
1828
+ var AV_SAMPLE_FMT_S32 = 2;
1829
+ var AV_SAMPLE_FMT_FLT = 3;
1830
+ var AV_SAMPLE_FMT_U8P = 5;
1831
+ var AV_SAMPLE_FMT_S16P = 6;
1832
+ var AV_SAMPLE_FMT_S32P = 7;
1833
+ var AV_SAMPLE_FMT_FLTP = 8;
1834
+ function libavFrameToInterleavedFloat32(frame) {
1835
+ const channels = frame.channels ?? frame.ch_layout_nb_channels ?? 1;
1836
+ const sampleRate = frame.sample_rate ?? 44100;
1837
+ const nbSamples = frame.nb_samples ?? 0;
1838
+ if (nbSamples === 0) return null;
1839
+ const out = new Float32Array(nbSamples * channels);
1840
+ switch (frame.format) {
1841
+ case AV_SAMPLE_FMT_FLTP: {
1842
+ const planes = ensurePlanes(frame.data, channels);
1843
+ for (let ch = 0; ch < channels; ch++) {
1844
+ const plane = asFloat32(planes[ch]);
1845
+ for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i];
1846
+ }
1847
+ return { data: out, channels, sampleRate };
1848
+ }
1849
+ case AV_SAMPLE_FMT_FLT: {
1850
+ const flat = asFloat32(frame.data);
1851
+ for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i];
1852
+ return { data: out, channels, sampleRate };
1853
+ }
1854
+ case AV_SAMPLE_FMT_S16P: {
1855
+ const planes = ensurePlanes(frame.data, channels);
1856
+ for (let ch = 0; ch < channels; ch++) {
1857
+ const plane = asInt16(planes[ch]);
1858
+ for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 32768;
1859
+ }
1860
+ return { data: out, channels, sampleRate };
1861
+ }
1862
+ case AV_SAMPLE_FMT_S16: {
1863
+ const flat = asInt16(frame.data);
1864
+ for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 32768;
1865
+ return { data: out, channels, sampleRate };
1866
+ }
1867
+ case AV_SAMPLE_FMT_S32P: {
1868
+ const planes = ensurePlanes(frame.data, channels);
1869
+ for (let ch = 0; ch < channels; ch++) {
1870
+ const plane = asInt32(planes[ch]);
1871
+ for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 2147483648;
1872
+ }
1873
+ return { data: out, channels, sampleRate };
1874
+ }
1875
+ case AV_SAMPLE_FMT_S32: {
1876
+ const flat = asInt32(frame.data);
1877
+ for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 2147483648;
1878
+ return { data: out, channels, sampleRate };
1879
+ }
1880
+ case AV_SAMPLE_FMT_U8P: {
1881
+ const planes = ensurePlanes(frame.data, channels);
1882
+ for (let ch = 0; ch < channels; ch++) {
1883
+ const plane = asUint8(planes[ch]);
1884
+ for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = (plane[i] - 128) / 128;
1885
+ }
1886
+ return { data: out, channels, sampleRate };
1887
+ }
1888
+ case AV_SAMPLE_FMT_U8: {
1889
+ const flat = asUint8(frame.data);
1890
+ for (let i = 0; i < nbSamples * channels; i++) out[i] = (flat[i] - 128) / 128;
1891
+ return { data: out, channels, sampleRate };
1892
+ }
1893
+ default:
1894
+ return null;
1895
+ }
1896
+ }
1897
+ function ensurePlanes(data, channels) {
1898
+ if (Array.isArray(data)) return data;
1899
+ const arr = data;
1900
+ const len = arr.length;
1901
+ const perChannel = Math.floor(len / channels);
1902
+ const planes = [];
1903
+ for (let ch = 0; ch < channels; ch++) {
1904
+ planes.push(arr.subarray ? arr.subarray(ch * perChannel, (ch + 1) * perChannel) : arr);
1905
+ }
1906
+ return planes;
1907
+ }
1908
+ function asFloat32(x) {
1909
+ if (x instanceof Float32Array) return x;
1910
+ const ta = x;
1911
+ return new Float32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
1912
+ }
1913
+ function asInt16(x) {
1914
+ if (x instanceof Int16Array) return x;
1915
+ const ta = x;
1916
+ return new Int16Array(ta.buffer, ta.byteOffset, ta.byteLength / 2);
1917
+ }
1918
+ function asInt32(x) {
1919
+ if (x instanceof Int32Array) return x;
1920
+ const ta = x;
1921
+ return new Int32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
1922
+ }
1923
+ function asUint8(x) {
1924
+ if (x instanceof Uint8Array) return x;
1925
+ const ta = x;
1926
+ return new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength);
1927
+ }
1928
+ function sanitizeFrameTimestamp(frame, nextUs, fallbackTimeBase) {
1929
+ const lo = frame.pts ?? 0;
1930
+ const hi = frame.ptshi ?? 0;
1931
+ const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
1932
+ if (isInvalid) {
1933
+ const us2 = nextUs();
1934
+ frame.pts = us2;
1935
+ frame.ptshi = 0;
1936
+ return;
1937
+ }
1938
+ const tb = fallbackTimeBase ?? [1, 1e6];
1939
+ const pts64 = hi * 4294967296 + lo;
1940
+ const us = Math.round(pts64 * 1e6 * tb[0] / tb[1]);
1941
+ if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
1942
+ frame.pts = us;
1943
+ frame.ptshi = us < 0 ? -1 : 0;
1944
+ return;
1945
+ }
1946
+ const fallback = nextUs();
1947
+ frame.pts = fallback;
1948
+ frame.ptshi = 0;
1949
+ }
1950
+
1651
1951
  // src/strategies/hybrid/decoder.ts
1652
1952
  async function startHybridDecoder(opts) {
1653
1953
  const variant = pickLibavVariant(opts.context);
@@ -1658,7 +1958,8 @@ async function startHybridDecoder(opts) {
1658
1958
  const readPkt = await libav.av_packet_alloc();
1659
1959
  const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
1660
1960
  const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
1661
- const audioStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO) ?? null;
1961
+ const firstAudioTrackId = opts.context.audioTracks[0]?.id;
1962
+ 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;
1662
1963
  if (!videoStream && !audioStream) {
1663
1964
  throw new Error("hybrid decoder: file has no decodable streams");
1664
1965
  }
@@ -1954,7 +2255,15 @@ async function startHybridDecoder(opts) {
1954
2255
  } catch {
1955
2256
  }
1956
2257
  },
1957
- async seek(timeSec) {
2258
+ async setAudioTrack(trackId, timeSec) {
2259
+ if (audioStream && audioStream.index === trackId) return;
2260
+ const newStream = streams.find(
2261
+ (s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO && s.index === trackId
2262
+ );
2263
+ if (!newStream) {
2264
+ console.warn("[avbridge] hybrid: setAudioTrack \u2014 no stream with id", trackId);
2265
+ return;
2266
+ }
1958
2267
  const newToken = ++pumpToken;
1959
2268
  if (pumpRunning) {
1960
2269
  try {
@@ -1963,6 +2272,28 @@ async function startHybridDecoder(opts) {
1963
2272
  }
1964
2273
  }
1965
2274
  if (destroyed) return;
2275
+ if (audioDec) {
2276
+ try {
2277
+ await libav.ff_free_decoder?.(audioDec.c, audioDec.pkt, audioDec.frame);
2278
+ } catch {
2279
+ }
2280
+ audioDec = null;
2281
+ }
2282
+ try {
2283
+ const [, c, pkt, frame] = await libav.ff_init_decoder(newStream.codec_id, {
2284
+ codecpar: newStream.codecpar
2285
+ });
2286
+ audioDec = { c, pkt, frame };
2287
+ audioTimeBase = newStream.time_base_num && newStream.time_base_den ? [newStream.time_base_num, newStream.time_base_den] : void 0;
2288
+ } catch (err) {
2289
+ console.warn(
2290
+ "[avbridge] hybrid: setAudioTrack init failed \u2014 switching to no-audio:",
2291
+ err.message
2292
+ );
2293
+ audioDec = null;
2294
+ opts.audio.setNoAudio();
2295
+ }
2296
+ audioStream = newStream;
1966
2297
  try {
1967
2298
  const tsUs = Math.floor(timeSec * 1e6);
1968
2299
  const [tsLo, tsHi] = libav.f64toi64 ? libav.f64toi64(tsUs) : [tsUs | 0, Math.floor(tsUs / 4294967296)];
@@ -1974,7 +2305,7 @@ async function startHybridDecoder(opts) {
1974
2305
  libav.AVSEEK_FLAG_BACKWARD ?? 0
1975
2306
  );
1976
2307
  } catch (err) {
1977
- console.warn("[avbridge] hybrid av_seek_frame failed:", err);
2308
+ console.warn("[avbridge] hybrid: setAudioTrack seek failed:", err);
1978
2309
  }
1979
2310
  try {
1980
2311
  if (videoDecoder && videoDecoder.state === "configured") {
@@ -1982,190 +2313,73 @@ async function startHybridDecoder(opts) {
1982
2313
  }
1983
2314
  } catch {
1984
2315
  }
1985
- try {
1986
- if (audioDec) await libav.avcodec_flush_buffers?.(audioDec.c);
1987
- } catch {
1988
- }
1989
2316
  await flushBSF();
1990
2317
  syntheticVideoUs = Math.round(timeSec * 1e6);
1991
2318
  syntheticAudioUs = Math.round(timeSec * 1e6);
1992
2319
  pumpRunning = pumpLoop(newToken).catch(
1993
- (err) => console.error("[avbridge] hybrid pump failed (post-seek):", err)
2320
+ (err) => console.error("[avbridge] hybrid pump failed (post-setAudioTrack):", err)
1994
2321
  );
1995
2322
  },
1996
- stats() {
1997
- return {
1998
- decoderType: "webcodecs-hybrid",
1999
- packetsRead,
2000
- videoFramesDecoded,
2001
- videoChunksFed,
2002
- audioFramesDecoded,
2003
- bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
2004
- videoDecodeQueueSize: videoDecoder?.decodeQueueSize ?? 0,
2005
- // Confirmed transport info — see fallback decoder for the pattern.
2006
- _transport: inputHandle.transport === "http-range" ? "http-range" : "memory",
2007
- _rangeSupported: inputHandle.transport === "http-range",
2008
- ...opts.renderer.stats(),
2009
- ...opts.audio.stats()
2010
- };
2011
- }
2012
- };
2013
- }
2014
- function sanitizePacketTimestamp(pkt, nextUs, fallbackTimeBase) {
2015
- const lo = pkt.pts ?? 0;
2016
- const hi = pkt.ptshi ?? 0;
2017
- const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
2018
- if (isInvalid) {
2019
- const us2 = nextUs();
2020
- pkt.pts = us2;
2021
- pkt.ptshi = 0;
2022
- pkt.time_base_num = 1;
2023
- pkt.time_base_den = 1e6;
2024
- return;
2025
- }
2026
- const tb = fallbackTimeBase ?? [1, 1e6];
2027
- const pts64 = hi * 4294967296 + lo;
2028
- const us = Math.round(pts64 * 1e6 * tb[0] / tb[1]);
2029
- if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
2030
- pkt.pts = us;
2031
- pkt.ptshi = us < 0 ? -1 : 0;
2032
- pkt.time_base_num = 1;
2033
- pkt.time_base_den = 1e6;
2034
- return;
2035
- }
2036
- const fallback = nextUs();
2037
- pkt.pts = fallback;
2038
- pkt.ptshi = 0;
2039
- pkt.time_base_num = 1;
2040
- pkt.time_base_den = 1e6;
2041
- }
2042
- function sanitizeFrameTimestamp(frame, nextUs, fallbackTimeBase) {
2043
- const lo = frame.pts ?? 0;
2044
- const hi = frame.ptshi ?? 0;
2045
- const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
2046
- if (isInvalid) {
2047
- const us2 = nextUs();
2048
- frame.pts = us2;
2049
- frame.ptshi = 0;
2050
- return;
2051
- }
2052
- const tb = fallbackTimeBase ?? [1, 1e6];
2053
- const pts64 = hi * 4294967296 + lo;
2054
- const us = Math.round(pts64 * 1e6 * tb[0] / tb[1]);
2055
- if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
2056
- frame.pts = us;
2057
- frame.ptshi = us < 0 ? -1 : 0;
2058
- return;
2059
- }
2060
- const fallback = nextUs();
2061
- frame.pts = fallback;
2062
- frame.ptshi = 0;
2063
- }
2064
- var AV_SAMPLE_FMT_U8 = 0;
2065
- var AV_SAMPLE_FMT_S16 = 1;
2066
- var AV_SAMPLE_FMT_S32 = 2;
2067
- var AV_SAMPLE_FMT_FLT = 3;
2068
- var AV_SAMPLE_FMT_U8P = 5;
2069
- var AV_SAMPLE_FMT_S16P = 6;
2070
- var AV_SAMPLE_FMT_S32P = 7;
2071
- var AV_SAMPLE_FMT_FLTP = 8;
2072
- function libavFrameToInterleavedFloat32(frame) {
2073
- const channels = frame.channels ?? frame.ch_layout_nb_channels ?? 1;
2074
- const sampleRate = frame.sample_rate ?? 44100;
2075
- const nbSamples = frame.nb_samples ?? 0;
2076
- if (nbSamples === 0) return null;
2077
- const out = new Float32Array(nbSamples * channels);
2078
- switch (frame.format) {
2079
- case AV_SAMPLE_FMT_FLTP: {
2080
- const planes = ensurePlanes(frame.data, channels);
2081
- for (let ch = 0; ch < channels; ch++) {
2082
- const plane = asFloat32(planes[ch]);
2083
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i];
2084
- }
2085
- return { data: out, channels, sampleRate };
2086
- }
2087
- case AV_SAMPLE_FMT_FLT: {
2088
- const flat = asFloat32(frame.data);
2089
- for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i];
2090
- return { data: out, channels, sampleRate };
2091
- }
2092
- case AV_SAMPLE_FMT_S16P: {
2093
- const planes = ensurePlanes(frame.data, channels);
2094
- for (let ch = 0; ch < channels; ch++) {
2095
- const plane = asInt16(planes[ch]);
2096
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 32768;
2323
+ async seek(timeSec) {
2324
+ const newToken = ++pumpToken;
2325
+ if (pumpRunning) {
2326
+ try {
2327
+ await pumpRunning;
2328
+ } catch {
2329
+ }
2097
2330
  }
2098
- return { data: out, channels, sampleRate };
2099
- }
2100
- case AV_SAMPLE_FMT_S16: {
2101
- const flat = asInt16(frame.data);
2102
- for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 32768;
2103
- return { data: out, channels, sampleRate };
2104
- }
2105
- case AV_SAMPLE_FMT_S32P: {
2106
- const planes = ensurePlanes(frame.data, channels);
2107
- for (let ch = 0; ch < channels; ch++) {
2108
- const plane = asInt32(planes[ch]);
2109
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 2147483648;
2331
+ if (destroyed) return;
2332
+ try {
2333
+ const tsUs = Math.floor(timeSec * 1e6);
2334
+ const [tsLo, tsHi] = libav.f64toi64 ? libav.f64toi64(tsUs) : [tsUs | 0, Math.floor(tsUs / 4294967296)];
2335
+ await libav.av_seek_frame(
2336
+ fmt_ctx,
2337
+ -1,
2338
+ tsLo,
2339
+ tsHi,
2340
+ libav.AVSEEK_FLAG_BACKWARD ?? 0
2341
+ );
2342
+ } catch (err) {
2343
+ console.warn("[avbridge] hybrid av_seek_frame failed:", err);
2110
2344
  }
2111
- return { data: out, channels, sampleRate };
2112
- }
2113
- case AV_SAMPLE_FMT_S32: {
2114
- const flat = asInt32(frame.data);
2115
- for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 2147483648;
2116
- return { data: out, channels, sampleRate };
2117
- }
2118
- case AV_SAMPLE_FMT_U8P: {
2119
- const planes = ensurePlanes(frame.data, channels);
2120
- for (let ch = 0; ch < channels; ch++) {
2121
- const plane = asUint8(planes[ch]);
2122
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = (plane[i] - 128) / 128;
2345
+ try {
2346
+ if (videoDecoder && videoDecoder.state === "configured") {
2347
+ await videoDecoder.flush();
2348
+ }
2349
+ } catch {
2123
2350
  }
2124
- return { data: out, channels, sampleRate };
2125
- }
2126
- case AV_SAMPLE_FMT_U8: {
2127
- const flat = asUint8(frame.data);
2128
- for (let i = 0; i < nbSamples * channels; i++) out[i] = (flat[i] - 128) / 128;
2129
- return { data: out, channels, sampleRate };
2351
+ try {
2352
+ if (audioDec) await libav.avcodec_flush_buffers?.(audioDec.c);
2353
+ } catch {
2354
+ }
2355
+ await flushBSF();
2356
+ syntheticVideoUs = Math.round(timeSec * 1e6);
2357
+ syntheticAudioUs = Math.round(timeSec * 1e6);
2358
+ pumpRunning = pumpLoop(newToken).catch(
2359
+ (err) => console.error("[avbridge] hybrid pump failed (post-seek):", err)
2360
+ );
2361
+ },
2362
+ stats() {
2363
+ return {
2364
+ decoderType: "webcodecs-hybrid",
2365
+ packetsRead,
2366
+ videoFramesDecoded,
2367
+ videoChunksFed,
2368
+ audioFramesDecoded,
2369
+ bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
2370
+ videoDecodeQueueSize: videoDecoder?.decodeQueueSize ?? 0,
2371
+ // Confirmed transport info — see fallback decoder for the pattern.
2372
+ _transport: inputHandle.transport === "http-range" ? "http-range" : "memory",
2373
+ _rangeSupported: inputHandle.transport === "http-range",
2374
+ ...opts.renderer.stats(),
2375
+ ...opts.audio.stats()
2376
+ };
2130
2377
  }
2131
- default:
2132
- return null;
2133
- }
2134
- }
2135
- function ensurePlanes(data, channels) {
2136
- if (Array.isArray(data)) return data;
2137
- const arr = data;
2138
- const len = arr.length;
2139
- const perChannel = Math.floor(len / channels);
2140
- const planes = [];
2141
- for (let ch = 0; ch < channels; ch++) {
2142
- planes.push(arr.subarray ? arr.subarray(ch * perChannel, (ch + 1) * perChannel) : arr);
2143
- }
2144
- return planes;
2145
- }
2146
- function asFloat32(x) {
2147
- if (x instanceof Float32Array) return x;
2148
- const ta = x;
2149
- return new Float32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
2150
- }
2151
- function asInt16(x) {
2152
- if (x instanceof Int16Array) return x;
2153
- const ta = x;
2154
- return new Int16Array(ta.buffer, ta.byteOffset, ta.byteLength / 2);
2155
- }
2156
- function asInt32(x) {
2157
- if (x instanceof Int32Array) return x;
2158
- const ta = x;
2159
- return new Int32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
2160
- }
2161
- function asUint8(x) {
2162
- if (x instanceof Uint8Array) return x;
2163
- const ta = x;
2164
- return new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength);
2378
+ };
2165
2379
  }
2166
2380
  async function loadBridge() {
2167
2381
  try {
2168
- const wrapper = await import('./libav-import-2JURFHEW.js');
2382
+ const wrapper = await import('./libav-import-6MGLCXVQ.js');
2169
2383
  return wrapper.libavBridge;
2170
2384
  } catch (err) {
2171
2385
  throw new Error(
@@ -2178,7 +2392,7 @@ async function loadBridge() {
2178
2392
  var READY_AUDIO_BUFFER_SECONDS = 0.3;
2179
2393
  var READY_TIMEOUT_SECONDS = 10;
2180
2394
  async function createHybridSession(ctx, target, transport) {
2181
- const { normalizeSource: normalizeSource2 } = await import('./source-F656KYYV.js');
2395
+ const { normalizeSource: normalizeSource2 } = await import('./source-MTX5ELUZ.js');
2182
2396
  const source = await normalizeSource2(ctx.source);
2183
2397
  const fps = ctx.videoTracks[0]?.fps ?? 30;
2184
2398
  const audio = new AudioOutput();
@@ -2275,7 +2489,24 @@ async function createHybridSession(ctx, target, transport) {
2275
2489
  async seek(time) {
2276
2490
  await doSeek(time);
2277
2491
  },
2278
- async setAudioTrack(_id) {
2492
+ async setAudioTrack(id) {
2493
+ if (!ctx.audioTracks.some((t) => t.id === id)) {
2494
+ console.warn("[avbridge] hybrid: setAudioTrack \u2014 unknown track id", id);
2495
+ return;
2496
+ }
2497
+ const wasPlaying = audio.isPlaying();
2498
+ const currentTime = audio.now();
2499
+ await audio.pause().catch(() => {
2500
+ });
2501
+ await handles.setAudioTrack(id, currentTime).catch(
2502
+ (err) => console.warn("[avbridge] hybrid: handles.setAudioTrack failed:", err)
2503
+ );
2504
+ await audio.reset(currentTime);
2505
+ renderer.flush();
2506
+ if (wasPlaying) {
2507
+ await waitForBuffer();
2508
+ await audio.start();
2509
+ }
2279
2510
  },
2280
2511
  async setSubtitleTrack(_id) {
2281
2512
  },
@@ -2314,7 +2545,8 @@ async function startDecoder(opts) {
2314
2545
  const readPkt = await libav.av_packet_alloc();
2315
2546
  const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
2316
2547
  const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
2317
- const audioStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO) ?? null;
2548
+ const firstAudioTrackId = opts.context.audioTracks[0]?.id;
2549
+ 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;
2318
2550
  if (!videoStream && !audioStream) {
2319
2551
  throw new Error("fallback decoder: file has no decodable streams");
2320
2552
  }
@@ -2530,7 +2762,7 @@ async function startDecoder(opts) {
2530
2762
  if (myToken !== pumpToken || destroyed) return;
2531
2763
  for (const f of frames) {
2532
2764
  if (myToken !== pumpToken || destroyed) return;
2533
- const bridgeOpts = sanitizeFrameTimestamp2(
2765
+ sanitizeFrameTimestamp(
2534
2766
  f,
2535
2767
  () => {
2536
2768
  const ts = syntheticVideoUs;
@@ -2540,7 +2772,7 @@ async function startDecoder(opts) {
2540
2772
  videoTimeBase
2541
2773
  );
2542
2774
  try {
2543
- const vf = bridge.laFrameToVideoFrame(f, bridgeOpts);
2775
+ const vf = bridge.laFrameToVideoFrame(f, { timeBase: [1, 1e6] });
2544
2776
  opts.renderer.enqueue(vf);
2545
2777
  videoFramesDecoded++;
2546
2778
  } catch (err) {
@@ -2568,7 +2800,7 @@ async function startDecoder(opts) {
2568
2800
  if (myToken !== pumpToken || destroyed) return;
2569
2801
  for (const f of frames) {
2570
2802
  if (myToken !== pumpToken || destroyed) return;
2571
- sanitizeFrameTimestamp2(
2803
+ sanitizeFrameTimestamp(
2572
2804
  f,
2573
2805
  () => {
2574
2806
  const ts = syntheticAudioUs;
@@ -2579,7 +2811,7 @@ async function startDecoder(opts) {
2579
2811
  },
2580
2812
  audioTimeBase
2581
2813
  );
2582
- const samples = libavFrameToInterleavedFloat322(f);
2814
+ const samples = libavFrameToInterleavedFloat32(f);
2583
2815
  if (samples) {
2584
2816
  opts.audio.schedule(samples.data, samples.channels, samples.sampleRate);
2585
2817
  audioFramesDecoded++;
@@ -2627,6 +2859,69 @@ async function startDecoder(opts) {
2627
2859
  } catch {
2628
2860
  }
2629
2861
  },
2862
+ async setAudioTrack(trackId, timeSec) {
2863
+ if (audioStream && audioStream.index === trackId) return;
2864
+ const newStream = streams.find(
2865
+ (s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO && s.index === trackId
2866
+ );
2867
+ if (!newStream) {
2868
+ console.warn("[avbridge] fallback: setAudioTrack \u2014 no stream with id", trackId);
2869
+ return;
2870
+ }
2871
+ const newToken = ++pumpToken;
2872
+ if (pumpRunning) {
2873
+ try {
2874
+ await pumpRunning;
2875
+ } catch {
2876
+ }
2877
+ }
2878
+ if (destroyed) return;
2879
+ if (audioDec) {
2880
+ try {
2881
+ await libav.ff_free_decoder?.(audioDec.c, audioDec.pkt, audioDec.frame);
2882
+ } catch {
2883
+ }
2884
+ audioDec = null;
2885
+ }
2886
+ try {
2887
+ const [, c, pkt, frame] = await libav.ff_init_decoder(newStream.codec_id, {
2888
+ codecpar: newStream.codecpar
2889
+ });
2890
+ audioDec = { c, pkt, frame };
2891
+ audioTimeBase = newStream.time_base_num && newStream.time_base_den ? [newStream.time_base_num, newStream.time_base_den] : void 0;
2892
+ } catch (err) {
2893
+ console.warn(
2894
+ "[avbridge] fallback: setAudioTrack init failed \u2014 falling back to no-audio mode:",
2895
+ err.message
2896
+ );
2897
+ audioDec = null;
2898
+ opts.audio.setNoAudio();
2899
+ }
2900
+ audioStream = newStream;
2901
+ try {
2902
+ const tsUs = Math.floor(timeSec * 1e6);
2903
+ const [tsLo, tsHi] = libav.f64toi64 ? libav.f64toi64(tsUs) : [tsUs | 0, Math.floor(tsUs / 4294967296)];
2904
+ await libav.av_seek_frame(
2905
+ fmt_ctx,
2906
+ -1,
2907
+ tsLo,
2908
+ tsHi,
2909
+ libav.AVSEEK_FLAG_BACKWARD ?? 0
2910
+ );
2911
+ } catch (err) {
2912
+ console.warn("[avbridge] fallback: setAudioTrack seek failed:", err);
2913
+ }
2914
+ try {
2915
+ if (videoDec) await libav.avcodec_flush_buffers?.(videoDec.c);
2916
+ } catch {
2917
+ }
2918
+ await flushBSF();
2919
+ syntheticVideoUs = Math.round(timeSec * 1e6);
2920
+ syntheticAudioUs = Math.round(timeSec * 1e6);
2921
+ pumpRunning = pumpLoop(newToken).catch(
2922
+ (err) => console.error("[avbridge] fallback pump failed (post-setAudioTrack):", err)
2923
+ );
2924
+ },
2630
2925
  async seek(timeSec) {
2631
2926
  const newToken = ++pumpToken;
2632
2927
  if (pumpRunning) {
@@ -2683,138 +2978,9 @@ async function startDecoder(opts) {
2683
2978
  }
2684
2979
  };
2685
2980
  }
2686
- function sanitizeFrameTimestamp2(frame, nextUs, fallbackTimeBase) {
2687
- const lo = frame.pts ?? 0;
2688
- const hi = frame.ptshi ?? 0;
2689
- const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
2690
- if (isInvalid) {
2691
- const us2 = nextUs();
2692
- frame.pts = us2;
2693
- frame.ptshi = 0;
2694
- return { timeBase: [1, 1e6] };
2695
- }
2696
- const tb = fallbackTimeBase ?? [1, 1e6];
2697
- const pts64 = hi * 4294967296 + lo;
2698
- const us = Math.round(pts64 * 1e6 * tb[0] / tb[1]);
2699
- if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
2700
- frame.pts = us;
2701
- frame.ptshi = us < 0 ? -1 : 0;
2702
- return { timeBase: [1, 1e6] };
2703
- }
2704
- const fallback = nextUs();
2705
- frame.pts = fallback;
2706
- frame.ptshi = 0;
2707
- return { timeBase: [1, 1e6] };
2708
- }
2709
- var AV_SAMPLE_FMT_U82 = 0;
2710
- var AV_SAMPLE_FMT_S162 = 1;
2711
- var AV_SAMPLE_FMT_S322 = 2;
2712
- var AV_SAMPLE_FMT_FLT2 = 3;
2713
- var AV_SAMPLE_FMT_U8P2 = 5;
2714
- var AV_SAMPLE_FMT_S16P2 = 6;
2715
- var AV_SAMPLE_FMT_S32P2 = 7;
2716
- var AV_SAMPLE_FMT_FLTP2 = 8;
2717
- function libavFrameToInterleavedFloat322(frame) {
2718
- const channels = frame.channels ?? frame.ch_layout_nb_channels ?? 1;
2719
- const sampleRate = frame.sample_rate ?? 44100;
2720
- const nbSamples = frame.nb_samples ?? 0;
2721
- if (nbSamples === 0) return null;
2722
- const out = new Float32Array(nbSamples * channels);
2723
- switch (frame.format) {
2724
- case AV_SAMPLE_FMT_FLTP2: {
2725
- const planes = ensurePlanes2(frame.data, channels);
2726
- for (let ch = 0; ch < channels; ch++) {
2727
- const plane = asFloat322(planes[ch]);
2728
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i];
2729
- }
2730
- return { data: out, channels, sampleRate };
2731
- }
2732
- case AV_SAMPLE_FMT_FLT2: {
2733
- const flat = asFloat322(frame.data);
2734
- for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i];
2735
- return { data: out, channels, sampleRate };
2736
- }
2737
- case AV_SAMPLE_FMT_S16P2: {
2738
- const planes = ensurePlanes2(frame.data, channels);
2739
- for (let ch = 0; ch < channels; ch++) {
2740
- const plane = asInt162(planes[ch]);
2741
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 32768;
2742
- }
2743
- return { data: out, channels, sampleRate };
2744
- }
2745
- case AV_SAMPLE_FMT_S162: {
2746
- const flat = asInt162(frame.data);
2747
- for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 32768;
2748
- return { data: out, channels, sampleRate };
2749
- }
2750
- case AV_SAMPLE_FMT_S32P2: {
2751
- const planes = ensurePlanes2(frame.data, channels);
2752
- for (let ch = 0; ch < channels; ch++) {
2753
- const plane = asInt322(planes[ch]);
2754
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 2147483648;
2755
- }
2756
- return { data: out, channels, sampleRate };
2757
- }
2758
- case AV_SAMPLE_FMT_S322: {
2759
- const flat = asInt322(frame.data);
2760
- for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 2147483648;
2761
- return { data: out, channels, sampleRate };
2762
- }
2763
- case AV_SAMPLE_FMT_U8P2: {
2764
- const planes = ensurePlanes2(frame.data, channels);
2765
- for (let ch = 0; ch < channels; ch++) {
2766
- const plane = asUint82(planes[ch]);
2767
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = (plane[i] - 128) / 128;
2768
- }
2769
- return { data: out, channels, sampleRate };
2770
- }
2771
- case AV_SAMPLE_FMT_U82: {
2772
- const flat = asUint82(frame.data);
2773
- for (let i = 0; i < nbSamples * channels; i++) out[i] = (flat[i] - 128) / 128;
2774
- return { data: out, channels, sampleRate };
2775
- }
2776
- default:
2777
- if (!globalThis.__avbridgeLoggedSampleFmt) {
2778
- globalThis.__avbridgeLoggedSampleFmt = frame.format;
2779
- console.warn(`[avbridge] unsupported audio sample format from libav: ${frame.format}`);
2780
- }
2781
- return null;
2782
- }
2783
- }
2784
- function ensurePlanes2(data, channels) {
2785
- if (Array.isArray(data)) return data;
2786
- const arr = data;
2787
- const len = arr.length;
2788
- const perChannel = Math.floor(len / channels);
2789
- const planes = [];
2790
- for (let ch = 0; ch < channels; ch++) {
2791
- planes.push(arr.subarray ? arr.subarray(ch * perChannel, (ch + 1) * perChannel) : arr);
2792
- }
2793
- return planes;
2794
- }
2795
- function asFloat322(x) {
2796
- if (x instanceof Float32Array) return x;
2797
- const ta = x;
2798
- return new Float32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
2799
- }
2800
- function asInt162(x) {
2801
- if (x instanceof Int16Array) return x;
2802
- const ta = x;
2803
- return new Int16Array(ta.buffer, ta.byteOffset, ta.byteLength / 2);
2804
- }
2805
- function asInt322(x) {
2806
- if (x instanceof Int32Array) return x;
2807
- const ta = x;
2808
- return new Int32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
2809
- }
2810
- function asUint82(x) {
2811
- if (x instanceof Uint8Array) return x;
2812
- const ta = x;
2813
- return new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength);
2814
- }
2815
2981
  async function loadBridge2() {
2816
2982
  try {
2817
- const wrapper = await import('./libav-import-2JURFHEW.js');
2983
+ const wrapper = await import('./libav-import-6MGLCXVQ.js');
2818
2984
  return wrapper.libavBridge;
2819
2985
  } catch (err) {
2820
2986
  throw new Error(
@@ -2827,7 +2993,7 @@ async function loadBridge2() {
2827
2993
  var READY_AUDIO_BUFFER_SECONDS2 = 0.04;
2828
2994
  var READY_TIMEOUT_SECONDS2 = 3;
2829
2995
  async function createFallbackSession(ctx, target, transport) {
2830
- const { normalizeSource: normalizeSource2 } = await import('./source-F656KYYV.js');
2996
+ const { normalizeSource: normalizeSource2 } = await import('./source-MTX5ELUZ.js');
2831
2997
  const source = await normalizeSource2(ctx.source);
2832
2998
  const fps = ctx.videoTracks[0]?.fps ?? 30;
2833
2999
  const audio = new AudioOutput();
@@ -2948,7 +3114,24 @@ async function createFallbackSession(ctx, target, transport) {
2948
3114
  async seek(time) {
2949
3115
  await doSeek(time);
2950
3116
  },
2951
- async setAudioTrack(_id) {
3117
+ async setAudioTrack(id) {
3118
+ if (!ctx.audioTracks.some((t) => t.id === id)) {
3119
+ console.warn("[avbridge] fallback: setAudioTrack \u2014 unknown track id", id);
3120
+ return;
3121
+ }
3122
+ const wasPlaying = audio.isPlaying();
3123
+ const currentTime = audio.now();
3124
+ await audio.pause().catch(() => {
3125
+ });
3126
+ await handles.setAudioTrack(id, currentTime).catch(
3127
+ (err) => console.warn("[avbridge] fallback: handles.setAudioTrack failed:", err)
3128
+ );
3129
+ await audio.reset(currentTime);
3130
+ renderer.flush();
3131
+ if (wasPlaying) {
3132
+ await waitForBuffer();
3133
+ await audio.start();
3134
+ }
2952
3135
  },
2953
3136
  async setSubtitleTrack(_id) {
2954
3137
  },
@@ -3002,119 +3185,6 @@ function registerBuiltins(registry) {
3002
3185
  registry.register(fallbackPlugin);
3003
3186
  }
3004
3187
 
3005
- // src/subtitles/srt.ts
3006
- function srtToVtt(srt) {
3007
- if (srt.charCodeAt(0) === 65279) srt = srt.slice(1);
3008
- const normalized = srt.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
3009
- const blocks = normalized.split(/\n{2,}/);
3010
- const out = ["WEBVTT", ""];
3011
- for (const block of blocks) {
3012
- const lines = block.split("\n");
3013
- if (lines.length > 0 && /^\d+$/.test(lines[0].trim())) {
3014
- lines.shift();
3015
- }
3016
- if (lines.length === 0) continue;
3017
- const timing = lines.shift();
3018
- const vttTiming = convertTiming(timing);
3019
- if (!vttTiming) continue;
3020
- out.push(vttTiming);
3021
- for (const l of lines) out.push(l);
3022
- out.push("");
3023
- }
3024
- return out.join("\n");
3025
- }
3026
- function convertTiming(line) {
3027
- 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(
3028
- line.trim()
3029
- );
3030
- if (!m) return null;
3031
- const fmt = (h, mm, s, ms) => `${h.padStart(2, "0")}:${mm}:${s}.${ms.padEnd(3, "0").slice(0, 3)}`;
3032
- return `${fmt(m[1], m[2], m[3], m[4])} --> ${fmt(m[5], m[6], m[7], m[8])}${m[9] ?? ""}`;
3033
- }
3034
-
3035
- // src/subtitles/vtt.ts
3036
- function isVtt(text) {
3037
- const trimmed = text.replace(/^\ufeff/, "").trimStart();
3038
- return trimmed.startsWith("WEBVTT");
3039
- }
3040
-
3041
- // src/subtitles/index.ts
3042
- async function discoverSidecars(file, directory) {
3043
- const baseName = file.name.replace(/\.[^.]+$/, "");
3044
- const found = [];
3045
- for await (const [name, handle] of directory) {
3046
- if (handle.kind !== "file") continue;
3047
- if (!name.startsWith(baseName)) continue;
3048
- const lower = name.toLowerCase();
3049
- let format = null;
3050
- if (lower.endsWith(".srt")) format = "srt";
3051
- else if (lower.endsWith(".vtt")) format = "vtt";
3052
- if (!format) continue;
3053
- const sidecarFile = await handle.getFile();
3054
- const url = URL.createObjectURL(sidecarFile);
3055
- const langMatch = name.slice(baseName.length).match(/[._-]([a-z]{2,3})(?:[._-]|\.)/i);
3056
- found.push({
3057
- url,
3058
- format,
3059
- language: langMatch?.[1]
3060
- });
3061
- }
3062
- return found;
3063
- }
3064
- var SubtitleResourceBag = class {
3065
- urls = /* @__PURE__ */ new Set();
3066
- /** Track an externally-created blob URL (e.g. from `discoverSidecars`). */
3067
- track(url) {
3068
- this.urls.add(url);
3069
- }
3070
- /** Convenience: create a blob URL and track it in one call. */
3071
- createObjectURL(blob) {
3072
- const url = URL.createObjectURL(blob);
3073
- this.urls.add(url);
3074
- return url;
3075
- }
3076
- /** Revoke every tracked URL. Idempotent — safe to call multiple times. */
3077
- revokeAll() {
3078
- for (const u of this.urls) URL.revokeObjectURL(u);
3079
- this.urls.clear();
3080
- }
3081
- };
3082
- async function attachSubtitleTracks(video, tracks, bag, onError, transport) {
3083
- const doFetch = fetchWith(transport);
3084
- for (const t of Array.from(video.querySelectorAll("track[data-avbridge]"))) {
3085
- t.remove();
3086
- }
3087
- for (const t of tracks) {
3088
- if (!t.sidecarUrl) continue;
3089
- try {
3090
- let url = t.sidecarUrl;
3091
- if (t.format === "srt") {
3092
- const res = await doFetch(t.sidecarUrl, transport?.requestInit);
3093
- const text = await res.text();
3094
- const vtt = srtToVtt(text);
3095
- const blob = new Blob([vtt], { type: "text/vtt" });
3096
- url = bag ? bag.createObjectURL(blob) : URL.createObjectURL(blob);
3097
- } else if (t.format === "vtt") {
3098
- const res = await doFetch(t.sidecarUrl, transport?.requestInit);
3099
- const text = await res.text();
3100
- if (!isVtt(text)) {
3101
- console.warn("[avbridge] subtitle missing WEBVTT header:", t.sidecarUrl);
3102
- }
3103
- }
3104
- const trackEl = document.createElement("track");
3105
- trackEl.kind = "subtitles";
3106
- trackEl.src = url;
3107
- trackEl.srclang = t.language ?? "und";
3108
- trackEl.label = t.language ?? `Subtitle ${t.id}`;
3109
- trackEl.dataset.avbridge = "true";
3110
- video.appendChild(trackEl);
3111
- } catch (err) {
3112
- const e = err instanceof Error ? err : new Error(String(err));
3113
- onError?.(e, t);
3114
- }
3115
- }
3116
- }
3117
-
3118
3188
  // src/player.ts
3119
3189
  var UnifiedPlayer = class _UnifiedPlayer {
3120
3190
  /**
@@ -3227,17 +3297,15 @@ var UnifiedPlayer = class _UnifiedPlayer {
3227
3297
  reason: decision.reason
3228
3298
  });
3229
3299
  await this.startSession(decision.strategy, decision.reason);
3230
- if (this.session.strategy !== "fallback" && this.session.strategy !== "hybrid") {
3231
- await attachSubtitleTracks(
3232
- this.options.target,
3233
- ctx.subtitleTracks,
3234
- this.subtitleResources,
3235
- (err, track) => {
3236
- console.warn(`[avbridge] subtitle ${track.id} failed: ${err.message}`);
3237
- },
3238
- this.transport
3239
- );
3240
- }
3300
+ await attachSubtitleTracks(
3301
+ this.options.target,
3302
+ ctx.subtitleTracks,
3303
+ this.subtitleResources,
3304
+ (err, track) => {
3305
+ console.warn(`[avbridge] subtitle ${track.id} failed: ${err.message}`);
3306
+ },
3307
+ this.transport
3308
+ );
3241
3309
  this.emitter.emitSticky("tracks", {
3242
3310
  video: ctx.videoTracks,
3243
3311
  audio: ctx.audioTracks,
@@ -3704,6 +3772,13 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
3704
3772
  _strategyClass = null;
3705
3773
  _audioTracks = [];
3706
3774
  _subtitleTracks = [];
3775
+ /**
3776
+ * External subtitle list forwarded to `createPlayer()` on the next
3777
+ * bootstrap. Setting this after bootstrap queues it for the next
3778
+ * source change; consumers that need to swap subtitles mid-playback
3779
+ * should set `source` to reload.
3780
+ */
3781
+ _subtitles = null;
3707
3782
  /**
3708
3783
  * Initial strategy preference. `"auto"` means "let the classifier decide";
3709
3784
  * any other value is passed to `createPlayer({ initialStrategy })` and
@@ -3859,7 +3934,8 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
3859
3934
  // Honor the consumer's preferred initial strategy. "auto" means
3860
3935
  // "let the classifier decide" — the createPlayer call simply doesn't
3861
3936
  // pass initialStrategy in that case.
3862
- ...this._preferredStrategy !== "auto" ? { initialStrategy: this._preferredStrategy } : {}
3937
+ ...this._preferredStrategy !== "auto" ? { initialStrategy: this._preferredStrategy } : {},
3938
+ ...this._subtitles ? { subtitles: this._subtitles } : {}
3863
3939
  });
3864
3940
  } catch (err) {
3865
3941
  if (id !== this._bootstrapId || this._destroyed) return;
@@ -4150,6 +4226,47 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
4150
4226
  get subtitleTracks() {
4151
4227
  return this._subtitleTracks;
4152
4228
  }
4229
+ /**
4230
+ * External subtitle files to attach when the source loads. Takes effect
4231
+ * on the next bootstrap — set before assigning `source`, or reload via
4232
+ * `load()` after changing. For dynamic post-bootstrap addition, use
4233
+ * `addSubtitle()` instead.
4234
+ *
4235
+ * @example
4236
+ * el.subtitles = [{ url: "/en.srt", format: "srt", language: "en" }];
4237
+ * el.src = "/movie.mp4";
4238
+ */
4239
+ get subtitles() {
4240
+ return this._subtitles;
4241
+ }
4242
+ set subtitles(value) {
4243
+ this._subtitles = value;
4244
+ }
4245
+ /**
4246
+ * Attach a subtitle track to the current playback without rebuilding
4247
+ * the player. Works while the element is playing — converts SRT to
4248
+ * VTT if needed, adds a `<track>` to the inner `<video>`. Canvas
4249
+ * strategies pick up the new track via their textTracks watcher.
4250
+ */
4251
+ async addSubtitle(subtitle) {
4252
+ const { attachSubtitleTracks: attachSubtitleTracks2 } = await import('./subtitles-4T74JRGT.js');
4253
+ const format = subtitle.format ?? (subtitle.url.endsWith(".srt") ? "srt" : "vtt");
4254
+ const track = {
4255
+ id: this._subtitleTracks.length,
4256
+ format,
4257
+ language: subtitle.language,
4258
+ sidecarUrl: subtitle.url
4259
+ };
4260
+ this._subtitleTracks.push(track);
4261
+ await attachSubtitleTracks2(
4262
+ this._videoEl,
4263
+ this._subtitleTracks,
4264
+ void 0,
4265
+ (err, t) => {
4266
+ console.warn(`[avbridge] subtitle ${t.id} failed: ${err.message}`);
4267
+ }
4268
+ );
4269
+ }
4153
4270
  // ── Public methods ─────────────────────────────────────────────────────
4154
4271
  /** Force a (re-)bootstrap if a source is currently set. */
4155
4272
  async load() {
@@ -5452,6 +5569,20 @@ var AvbridgePlayerElement = class extends HTMLElement {
5452
5569
  get subtitleTracks() {
5453
5570
  return this._video.subtitleTracks ?? [];
5454
5571
  }
5572
+ /**
5573
+ * External subtitle files to attach when the source loads. Forwarded
5574
+ * to the inner <avbridge-video>. Takes effect on next bootstrap.
5575
+ */
5576
+ get subtitles() {
5577
+ return this._video.subtitles;
5578
+ }
5579
+ set subtitles(value) {
5580
+ this._video.subtitles = value;
5581
+ }
5582
+ /** Attach a subtitle track to the current playback without a reload. */
5583
+ async addSubtitle(subtitle) {
5584
+ return this._video.addSubtitle(subtitle);
5585
+ }
5455
5586
  get player() {
5456
5587
  return this._video.player;
5457
5588
  }