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
@@ -1,239 +1,10 @@
1
- import { normalizeSource, sniffNormalizedSource, AvbridgeError, ERR_PROBE_FAILED, ERR_PROBE_UNKNOWN_CONTAINER, ERR_LIBAV_NOT_REACHABLE, ERR_ALL_STRATEGIES_EXHAUSTED, ERR_PLAYER_NOT_READY, fetchWith, ERR_MSE_NOT_SUPPORTED, ERR_MSE_CODEC_NOT_SUPPORTED } from './chunk-2PGRFCWB.js';
1
+ import { SubtitleResourceBag, discoverSidecars, attachSubtitleTracks, SubtitleOverlay } from './chunk-5KVLE6YI.js';
2
+ import { probe, avbridgeVideoToMediabunny, buildMediabunnySourceFromInput, avbridgeAudioToMediabunny } from './chunk-SR3MPV4D.js';
3
+ import { AvbridgeError, ERR_ALL_STRATEGIES_EXHAUSTED, ERR_PLAYER_NOT_READY, ERR_MSE_NOT_SUPPORTED, ERR_MSE_CODEC_NOT_SUPPORTED } from './chunk-CPJLFFCC.js';
4
+ import { sanitizePacketTimestamp, sanitizeFrameTimestamp, libavFrameToInterleavedFloat32 } from './chunk-X2K3GIWE.js';
2
5
  import { dbg, loadLibav } from './chunk-5DMTJVIU.js';
3
6
  import { pickLibavVariant } from './chunk-5YAWWKA3.js';
4
7
 
5
- // src/probe/mediabunny.ts
6
- async function probeWithMediabunny(source, sniffedContainer) {
7
- const mb = await import('mediabunny');
8
- const input = new mb.Input({
9
- source: await buildMediabunnySource(mb, source),
10
- formats: mb.ALL_FORMATS
11
- });
12
- const allTracks = await input.getTracks();
13
- const duration = await safeNumber(() => input.computeDuration());
14
- const videoTracks = [];
15
- const audioTracks = [];
16
- for (const track of allTracks) {
17
- if (track.isVideoTrack()) {
18
- const codecParam = await safe(() => track.getCodecParameterString());
19
- videoTracks.push({
20
- id: track.id,
21
- codec: mediabunnyVideoToAvbridge(track.codec),
22
- width: track.displayWidth ?? track.codedWidth ?? 0,
23
- height: track.displayHeight ?? track.codedHeight ?? 0,
24
- codecString: codecParam ?? void 0
25
- });
26
- } else if (track.isAudioTrack()) {
27
- const codecParam = await safe(() => track.getCodecParameterString());
28
- audioTracks.push({
29
- id: track.id,
30
- codec: mediabunnyAudioToAvbridge(track.codec),
31
- channels: track.numberOfChannels ?? 0,
32
- sampleRate: track.sampleRate ?? 0,
33
- language: track.languageCode,
34
- codecString: codecParam ?? void 0
35
- });
36
- }
37
- }
38
- const format = await safe(() => input.getFormat());
39
- const container = resolveContainer(format?.name, sniffedContainer);
40
- return {
41
- source: source.original,
42
- name: source.name,
43
- byteLength: source.byteLength,
44
- container,
45
- videoTracks,
46
- audioTracks,
47
- subtitleTracks: [],
48
- probedBy: "mediabunny",
49
- duration
50
- };
51
- }
52
- async function buildMediabunnySource(mb, source) {
53
- if (source.kind === "url") {
54
- return new mb.UrlSource(source.url);
55
- }
56
- return new mb.BlobSource(source.blob);
57
- }
58
- async function buildMediabunnySourceFromInput(mb, source) {
59
- if (typeof source === "string") return new mb.UrlSource(source);
60
- if (source instanceof URL) return new mb.UrlSource(source.toString());
61
- if (source instanceof Blob) return new mb.BlobSource(source);
62
- if (source instanceof ArrayBuffer) return new mb.BlobSource(new Blob([source]));
63
- if (source instanceof Uint8Array) return new mb.BlobSource(new Blob([source]));
64
- throw new TypeError("unsupported source type for mediabunny");
65
- }
66
- function resolveContainer(formatName, sniffed) {
67
- const name = (formatName ?? "").toLowerCase();
68
- if (name.includes("matroska") || name.includes("mkv")) return "mkv";
69
- if (name.includes("webm")) return "webm";
70
- if (name.includes("mp4") || name.includes("isom")) return "mp4";
71
- if (name.includes("mov") || name.includes("quicktime")) return "mov";
72
- if (name.includes("ogg")) return "ogg";
73
- if (name.includes("wav")) return "wav";
74
- if (name.includes("flac")) return "flac";
75
- if (name.includes("mp3")) return "mp3";
76
- if (name.includes("adts") || name.includes("aac")) return "adts";
77
- if (name.includes("mpegts") || name.includes("mpeg-ts") || name.includes("transport")) return "mpegts";
78
- return sniffed;
79
- }
80
- function mediabunnyVideoToAvbridge(c) {
81
- switch (c) {
82
- case "avc":
83
- return "h264";
84
- case "hevc":
85
- return "h265";
86
- case "vp8":
87
- return "vp8";
88
- case "vp9":
89
- return "vp9";
90
- case "av1":
91
- return "av1";
92
- default:
93
- return c ? c : "unknown";
94
- }
95
- }
96
- function avbridgeVideoToMediabunny(c) {
97
- switch (c) {
98
- case "h264":
99
- return "avc";
100
- case "h265":
101
- return "hevc";
102
- case "vp8":
103
- return "vp8";
104
- case "vp9":
105
- return "vp9";
106
- case "av1":
107
- return "av1";
108
- default:
109
- return null;
110
- }
111
- }
112
- function mediabunnyAudioToAvbridge(c) {
113
- switch (c) {
114
- case "aac":
115
- return "aac";
116
- case "mp3":
117
- return "mp3";
118
- case "opus":
119
- return "opus";
120
- case "vorbis":
121
- return "vorbis";
122
- case "flac":
123
- return "flac";
124
- case "ac3":
125
- return "ac3";
126
- case "eac3":
127
- return "eac3";
128
- default:
129
- return c ? c : "unknown";
130
- }
131
- }
132
- function avbridgeAudioToMediabunny(c) {
133
- switch (c) {
134
- case "aac":
135
- return "aac";
136
- case "mp3":
137
- return "mp3";
138
- case "opus":
139
- return "opus";
140
- case "vorbis":
141
- return "vorbis";
142
- case "flac":
143
- return "flac";
144
- case "ac3":
145
- return "ac3";
146
- case "eac3":
147
- return "eac3";
148
- default:
149
- return null;
150
- }
151
- }
152
- async function safeNumber(fn) {
153
- try {
154
- const v = await fn();
155
- return typeof v === "number" && Number.isFinite(v) ? v : void 0;
156
- } catch {
157
- return void 0;
158
- }
159
- }
160
- async function safe(fn) {
161
- try {
162
- return await fn();
163
- } catch {
164
- return void 0;
165
- }
166
- }
167
-
168
- // src/probe/index.ts
169
- var MEDIABUNNY_CONTAINERS = /* @__PURE__ */ new Set([
170
- "mp4",
171
- "mov",
172
- "mkv",
173
- "webm",
174
- "ogg",
175
- "wav",
176
- "mp3",
177
- "flac",
178
- "adts",
179
- "mpegts"
180
- ]);
181
- async function probe(source, transport) {
182
- const normalized = await normalizeSource(source, transport);
183
- const sniffed = await sniffNormalizedSource(normalized);
184
- if (MEDIABUNNY_CONTAINERS.has(sniffed)) {
185
- try {
186
- const result = await probeWithMediabunny(normalized, sniffed);
187
- const hasUnknownCodec = result.videoTracks.some((t) => t.codec === "unknown") || result.audioTracks.some((t) => t.codec === "unknown");
188
- if (hasUnknownCodec) {
189
- try {
190
- const { probeWithLibav } = await import('./avi-NJXAXUXK.js');
191
- return await probeWithLibav(normalized, sniffed);
192
- } catch {
193
- return result;
194
- }
195
- }
196
- return result;
197
- } catch (mediabunnyErr) {
198
- console.warn(
199
- `[avbridge] mediabunny rejected ${sniffed} file, falling back to libav:`,
200
- mediabunnyErr.message
201
- );
202
- try {
203
- const { probeWithLibav } = await import('./avi-NJXAXUXK.js');
204
- return await probeWithLibav(normalized, sniffed);
205
- } catch (libavErr) {
206
- const mbMsg = mediabunnyErr.message || String(mediabunnyErr);
207
- const lvMsg = libavErr instanceof Error ? libavErr.message : String(libavErr);
208
- throw new AvbridgeError(
209
- ERR_PROBE_FAILED,
210
- `Failed to probe ${sniffed.toUpperCase()} file. mediabunny: ${mbMsg}. libav: ${lvMsg}.`,
211
- "The file may be corrupt, truncated, or in an unsupported format. Enable AVBRIDGE_DEBUG for detailed logs."
212
- );
213
- }
214
- }
215
- }
216
- try {
217
- const { probeWithLibav } = await import('./avi-NJXAXUXK.js');
218
- return await probeWithLibav(normalized, sniffed);
219
- } catch (err) {
220
- const inner = err instanceof Error ? err.message : String(err);
221
- console.error("[avbridge] libav probe failed for", sniffed, "file:", err);
222
- if (sniffed === "unknown") {
223
- throw new AvbridgeError(
224
- ERR_PROBE_UNKNOWN_CONTAINER,
225
- `Unable to probe source: container format could not be identified. libav fallback: ${inner || "(no details)"}`,
226
- "The file may be corrupt or in a format avbridge doesn't recognize. Check the file plays in VLC or ffprobe."
227
- );
228
- }
229
- throw new AvbridgeError(
230
- ERR_LIBAV_NOT_REACHABLE,
231
- `${sniffed.toUpperCase()} files require libav.js, which failed to load: ${inner || "(no details)"}`,
232
- "Install @libav.js/variant-webcodecs, or check that AVBRIDGE_LIBAV_BASE points to the correct path."
233
- );
234
- }
235
- }
236
-
237
8
  // src/util/codec-strings.ts
238
9
  function videoCodecString(track) {
239
10
  if (track.codecString) return track.codecString;
@@ -483,36 +254,6 @@ function isRiskyNative(video) {
483
254
  return false;
484
255
  }
485
256
 
486
- // src/subtitles/srt.ts
487
- function srtToVtt(srt) {
488
- if (srt.charCodeAt(0) === 65279) srt = srt.slice(1);
489
- const normalized = srt.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
490
- const blocks = normalized.split(/\n{2,}/);
491
- const out = ["WEBVTT", ""];
492
- for (const block of blocks) {
493
- const lines = block.split("\n");
494
- if (lines.length > 0 && /^\d+$/.test(lines[0].trim())) {
495
- lines.shift();
496
- }
497
- if (lines.length === 0) continue;
498
- const timing = lines.shift();
499
- const vttTiming = convertTiming(timing);
500
- if (!vttTiming) continue;
501
- out.push(vttTiming);
502
- for (const l of lines) out.push(l);
503
- out.push("");
504
- }
505
- return out.join("\n");
506
- }
507
- function convertTiming(line) {
508
- 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(
509
- line.trim()
510
- );
511
- if (!m) return null;
512
- const fmt = (h, mm, s, ms) => `${h.padStart(2, "0")}:${mm}:${s}.${ms.padEnd(3, "0").slice(0, 3)}`;
513
- return `${fmt(m[1], m[2], m[3], m[4])} --> ${fmt(m[5], m[6], m[7], m[8])}${m[9] ?? ""}`;
514
- }
515
-
516
257
  // src/events.ts
517
258
  var TypedEmitter = class {
518
259
  listeners = {};
@@ -718,7 +459,7 @@ async function createNativeSession(context, video) {
718
459
  },
719
460
  async setAudioTrack(id) {
720
461
  const tracks = video.audioTracks;
721
- if (!tracks) return;
462
+ if (!tracks || tracks.length === 0) return;
722
463
  for (let i = 0; i < tracks.length; i++) {
723
464
  tracks[i].enabled = tracks[i].id === String(id) || i === id;
724
465
  }
@@ -966,30 +707,49 @@ var MseSink = class {
966
707
  async function createRemuxPipeline(ctx, video) {
967
708
  const mb = await import('mediabunny');
968
709
  const videoTrackInfo = ctx.videoTracks[0];
969
- const audioTrackInfo = ctx.audioTracks[0];
970
710
  if (!videoTrackInfo) throw new Error("remux: source has no video track");
971
711
  const mbVideoCodec = avbridgeVideoToMediabunny(videoTrackInfo.codec);
972
712
  if (!mbVideoCodec) {
973
713
  throw new Error(`remux: video codec "${videoTrackInfo.codec}" is not supported by mediabunny output`);
974
714
  }
975
- const mbAudioCodec = audioTrackInfo ? avbridgeAudioToMediabunny(audioTrackInfo.codec) : null;
976
715
  const input = new mb.Input({
977
716
  source: await buildMediabunnySourceFromInput(mb, ctx.source),
978
717
  formats: mb.ALL_FORMATS
979
718
  });
980
719
  const allTracks = await input.getTracks();
981
720
  const inputVideo = allTracks.find((t) => t.id === videoTrackInfo.id && t.isVideoTrack());
982
- const inputAudio = audioTrackInfo ? allTracks.find((t) => t.id === audioTrackInfo.id && t.isAudioTrack()) : null;
983
721
  if (!inputVideo || !inputVideo.isVideoTrack()) {
984
722
  throw new Error("remux: video track not found in input");
985
723
  }
986
- if (audioTrackInfo && (!inputAudio || !inputAudio.isAudioTrack())) {
987
- throw new Error("remux: audio track not found in input");
988
- }
989
724
  const videoConfig = await inputVideo.getDecoderConfig();
990
- const audioConfig = inputAudio && inputAudio.isAudioTrack() ? await inputAudio.getDecoderConfig() : null;
991
725
  const videoSink = new mb.EncodedPacketSink(inputVideo);
992
- const audioSink = inputAudio?.isAudioTrack() ? new mb.EncodedPacketSink(inputAudio) : null;
726
+ let selectedAudioTrackId = ctx.audioTracks[0]?.id ?? null;
727
+ let inputAudio = null;
728
+ let mbAudioCodec = null;
729
+ let audioSink = null;
730
+ let audioConfig = null;
731
+ async function rebuildAudio() {
732
+ if (selectedAudioTrackId == null) {
733
+ inputAudio = null;
734
+ mbAudioCodec = null;
735
+ audioSink = null;
736
+ audioConfig = null;
737
+ return;
738
+ }
739
+ const trackInfo = ctx.audioTracks.find((t) => t.id === selectedAudioTrackId);
740
+ if (!trackInfo) {
741
+ throw new Error(`remux: no audio track with id ${selectedAudioTrackId}`);
742
+ }
743
+ const newInput = allTracks.find((t) => t.id === trackInfo.id && t.isAudioTrack());
744
+ if (!newInput || !newInput.isAudioTrack()) {
745
+ throw new Error("remux: audio track not found in input");
746
+ }
747
+ inputAudio = newInput;
748
+ mbAudioCodec = avbridgeAudioToMediabunny(trackInfo.codec);
749
+ audioSink = new mb.EncodedPacketSink(newInput);
750
+ audioConfig = await newInput.getDecoderConfig();
751
+ }
752
+ await rebuildAudio();
993
753
  let sink = null;
994
754
  const stats = { videoPackets: 0, audioPackets: 0, bytesWritten: 0, fragments: 0 };
995
755
  let destroyed = false;
@@ -1114,6 +874,30 @@ async function createRemuxPipeline(ctx, video) {
1114
874
  pendingAutoPlay = autoPlay;
1115
875
  if (sink) sink.setPlayOnSeek(autoPlay);
1116
876
  },
877
+ async setAudioTrack(trackId, time, autoPlay) {
878
+ if (selectedAudioTrackId === trackId) return;
879
+ if (!ctx.audioTracks.some((t) => t.id === trackId)) {
880
+ console.warn("[avbridge] remux: setAudioTrack \u2014 unknown track id", trackId);
881
+ return;
882
+ }
883
+ pumpToken++;
884
+ selectedAudioTrackId = trackId;
885
+ await rebuildAudio().catch((err) => {
886
+ console.warn("[avbridge] remux: rebuildAudio failed:", err.message);
887
+ });
888
+ if (sink) {
889
+ try {
890
+ sink.destroy();
891
+ } catch {
892
+ }
893
+ sink = null;
894
+ }
895
+ pendingAutoPlay = autoPlay;
896
+ pendingStartTime = time;
897
+ pumpLoop(++pumpToken, time).catch((err) => {
898
+ console.error("[avbridge] remux pipeline setAudioTrack pump failed:", err);
899
+ });
900
+ },
1117
901
  async destroy() {
1118
902
  destroyed = true;
1119
903
  pumpToken++;
@@ -1173,7 +957,19 @@ async function createRemuxSession(context, video) {
1173
957
  const wasPlaying = !video.paused;
1174
958
  await pipeline.seek(time, wasPlaying || wantPlay);
1175
959
  },
1176
- async setAudioTrack(_id) {
960
+ async setAudioTrack(id) {
961
+ if (!context.audioTracks.some((t) => t.id === id)) {
962
+ console.warn("[avbridge] remux: setAudioTrack \u2014 unknown track id", id);
963
+ return;
964
+ }
965
+ const wasPlaying = !video.paused;
966
+ const time = video.currentTime || 0;
967
+ if (!started) {
968
+ started = true;
969
+ await pipeline.setAudioTrack(id, time, wantPlay || wasPlaying);
970
+ return;
971
+ }
972
+ await pipeline.setAudioTrack(id, time, wasPlaying || wantPlay);
1177
973
  },
1178
974
  async setSubtitleTrack(id) {
1179
975
  const tracks = video.textTracks;
@@ -1226,6 +1022,9 @@ var VideoRenderer = class {
1226
1022
  document.body.appendChild(this.canvas);
1227
1023
  }
1228
1024
  target.style.visibility = "hidden";
1025
+ const overlayParent = parent instanceof HTMLElement ? parent : document.body;
1026
+ this.subtitleOverlay = new SubtitleOverlay(overlayParent);
1027
+ this.watchTextTracks(target);
1229
1028
  const ctx = this.canvas.getContext("2d");
1230
1029
  if (!ctx) throw new Error("video renderer: failed to acquire 2D context");
1231
1030
  this.ctx = ctx;
@@ -1251,6 +1050,15 @@ var VideoRenderer = class {
1251
1050
  ticksWaiting = 0;
1252
1051
  /** Cumulative count of ticks where PTS mode painted a frame. */
1253
1052
  ticksPainted = 0;
1053
+ /**
1054
+ * Subtitle overlay div attached to the stage wrapper alongside the
1055
+ * canvas. Created lazily when subtitle tracks are attached via the
1056
+ * target's `<track>` children. Canvas strategies (hybrid, fallback)
1057
+ * hide the <video>, so we can't rely on the browser's native cue
1058
+ * rendering; we read TextTrack.cues and render into this overlay.
1059
+ */
1060
+ subtitleOverlay = null;
1061
+ subtitleTrack = null;
1254
1062
  /**
1255
1063
  * Calibration offset (microseconds) between video PTS and audio clock.
1256
1064
  * Video PTS and AudioContext.currentTime can drift ~0.1% relative to
@@ -1294,9 +1102,80 @@ var VideoRenderer = class {
1294
1102
  this.framesDroppedOverflow++;
1295
1103
  }
1296
1104
  }
1105
+ /**
1106
+ * Watch the target <video>'s textTracks list. When a track is added,
1107
+ * grab it and start polling cues on each render tick. Existing tracks
1108
+ * (if any) are picked up immediately.
1109
+ */
1110
+ watchTextTracks(target) {
1111
+ const pick = () => {
1112
+ if (this.subtitleTrack) return;
1113
+ const tracks = target.textTracks;
1114
+ if (isDebug()) {
1115
+ console.log(`[avbridge:subs] watchTextTracks pick() \u2014 ${tracks.length} tracks`);
1116
+ }
1117
+ for (let i = 0; i < tracks.length; i++) {
1118
+ const t = tracks[i];
1119
+ if (isDebug()) {
1120
+ console.log(`[avbridge:subs] track ${i}: kind=${t.kind} mode=${t.mode} cues=${t.cues?.length ?? 0}`);
1121
+ }
1122
+ if (t.kind === "subtitles" || t.kind === "captions") {
1123
+ this.subtitleTrack = t;
1124
+ t.mode = "hidden";
1125
+ if (isDebug()) {
1126
+ console.log(`[avbridge:subs] picked track, mode=hidden`);
1127
+ }
1128
+ const trackEl = target.querySelector(`track[srclang="${t.language}"]`);
1129
+ if (trackEl) {
1130
+ trackEl.addEventListener("load", () => {
1131
+ if (isDebug()) {
1132
+ console.log(`[avbridge:subs] track element loaded, cues=${t.cues?.length ?? 0}`);
1133
+ }
1134
+ });
1135
+ trackEl.addEventListener("error", (ev) => {
1136
+ console.warn(`[avbridge:subs] track element error:`, ev);
1137
+ });
1138
+ }
1139
+ break;
1140
+ }
1141
+ }
1142
+ };
1143
+ pick();
1144
+ if (typeof target.textTracks.addEventListener === "function") {
1145
+ target.textTracks.addEventListener("addtrack", (e) => {
1146
+ if (isDebug()) {
1147
+ console.log("[avbridge:subs] addtrack event fired");
1148
+ }
1149
+ pick();
1150
+ });
1151
+ }
1152
+ }
1153
+ _loggedCues = false;
1154
+ /** Find the active cue (if any) for the given media time. */
1155
+ updateSubtitles() {
1156
+ if (!this.subtitleOverlay || !this.subtitleTrack) return;
1157
+ const cues = this.subtitleTrack.cues;
1158
+ if (!cues || cues.length === 0) return;
1159
+ if (isDebug() && !this._loggedCues) {
1160
+ this._loggedCues = true;
1161
+ console.log(`[avbridge:subs] cues available: ${cues.length}, first start=${cues[0].startTime}, last end=${cues[cues.length - 1].endTime}`);
1162
+ }
1163
+ const t = this.clock.now();
1164
+ let activeText = "";
1165
+ for (let i = 0; i < cues.length; i++) {
1166
+ const c = cues[i];
1167
+ if (t >= c.startTime && t <= c.endTime) {
1168
+ const vttCue = c;
1169
+ activeText = vttCue.text ?? "";
1170
+ break;
1171
+ }
1172
+ }
1173
+ this.subtitleOverlay.setText(activeText.replace(/<[^>]+>/g, ""));
1174
+ }
1297
1175
  tick() {
1298
1176
  if (this.destroyed) return;
1299
1177
  this.rafHandle = requestAnimationFrame(this.tick);
1178
+ this.updateSubtitles();
1300
1179
  if (this.queue.length === 0) return;
1301
1180
  const playing = this.clock.isPlaying();
1302
1181
  if (!playing) {
@@ -1425,6 +1304,11 @@ var VideoRenderer = class {
1425
1304
  this.destroyed = true;
1426
1305
  if (this.rafHandle != null) cancelAnimationFrame(this.rafHandle);
1427
1306
  this.flush();
1307
+ if (this.subtitleOverlay) {
1308
+ this.subtitleOverlay.destroy();
1309
+ this.subtitleOverlay = null;
1310
+ }
1311
+ this.subtitleTrack = null;
1428
1312
  this.canvas.remove();
1429
1313
  this.target.style.visibility = "";
1430
1314
  }
@@ -1674,7 +1558,8 @@ async function startHybridDecoder(opts) {
1674
1558
  const readPkt = await libav.av_packet_alloc();
1675
1559
  const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
1676
1560
  const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
1677
- const audioStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO) ?? null;
1561
+ const firstAudioTrackId = opts.context.audioTracks[0]?.id;
1562
+ 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;
1678
1563
  if (!videoStream && !audioStream) {
1679
1564
  throw new Error("hybrid decoder: file has no decodable streams");
1680
1565
  }
@@ -1970,6 +1855,71 @@ async function startHybridDecoder(opts) {
1970
1855
  } catch {
1971
1856
  }
1972
1857
  },
1858
+ async setAudioTrack(trackId, timeSec) {
1859
+ if (audioStream && audioStream.index === trackId) return;
1860
+ const newStream = streams.find(
1861
+ (s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO && s.index === trackId
1862
+ );
1863
+ if (!newStream) {
1864
+ console.warn("[avbridge] hybrid: setAudioTrack \u2014 no stream with id", trackId);
1865
+ return;
1866
+ }
1867
+ const newToken = ++pumpToken;
1868
+ if (pumpRunning) {
1869
+ try {
1870
+ await pumpRunning;
1871
+ } catch {
1872
+ }
1873
+ }
1874
+ if (destroyed) return;
1875
+ if (audioDec) {
1876
+ try {
1877
+ await libav.ff_free_decoder?.(audioDec.c, audioDec.pkt, audioDec.frame);
1878
+ } catch {
1879
+ }
1880
+ audioDec = null;
1881
+ }
1882
+ try {
1883
+ const [, c, pkt, frame] = await libav.ff_init_decoder(newStream.codec_id, {
1884
+ codecpar: newStream.codecpar
1885
+ });
1886
+ audioDec = { c, pkt, frame };
1887
+ audioTimeBase = newStream.time_base_num && newStream.time_base_den ? [newStream.time_base_num, newStream.time_base_den] : void 0;
1888
+ } catch (err) {
1889
+ console.warn(
1890
+ "[avbridge] hybrid: setAudioTrack init failed \u2014 switching to no-audio:",
1891
+ err.message
1892
+ );
1893
+ audioDec = null;
1894
+ opts.audio.setNoAudio();
1895
+ }
1896
+ audioStream = newStream;
1897
+ try {
1898
+ const tsUs = Math.floor(timeSec * 1e6);
1899
+ const [tsLo, tsHi] = libav.f64toi64 ? libav.f64toi64(tsUs) : [tsUs | 0, Math.floor(tsUs / 4294967296)];
1900
+ await libav.av_seek_frame(
1901
+ fmt_ctx,
1902
+ -1,
1903
+ tsLo,
1904
+ tsHi,
1905
+ libav.AVSEEK_FLAG_BACKWARD ?? 0
1906
+ );
1907
+ } catch (err) {
1908
+ console.warn("[avbridge] hybrid: setAudioTrack seek failed:", err);
1909
+ }
1910
+ try {
1911
+ if (videoDecoder && videoDecoder.state === "configured") {
1912
+ await videoDecoder.flush();
1913
+ }
1914
+ } catch {
1915
+ }
1916
+ await flushBSF();
1917
+ syntheticVideoUs = Math.round(timeSec * 1e6);
1918
+ syntheticAudioUs = Math.round(timeSec * 1e6);
1919
+ pumpRunning = pumpLoop(newToken).catch(
1920
+ (err) => console.error("[avbridge] hybrid pump failed (post-setAudioTrack):", err)
1921
+ );
1922
+ },
1973
1923
  async seek(timeSec) {
1974
1924
  const newToken = ++pumpToken;
1975
1925
  if (pumpRunning) {
@@ -2027,161 +1977,9 @@ async function startHybridDecoder(opts) {
2027
1977
  }
2028
1978
  };
2029
1979
  }
2030
- function sanitizePacketTimestamp(pkt, nextUs, fallbackTimeBase) {
2031
- const lo = pkt.pts ?? 0;
2032
- const hi = pkt.ptshi ?? 0;
2033
- const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
2034
- if (isInvalid) {
2035
- const us2 = nextUs();
2036
- pkt.pts = us2;
2037
- pkt.ptshi = 0;
2038
- pkt.time_base_num = 1;
2039
- pkt.time_base_den = 1e6;
2040
- return;
2041
- }
2042
- const tb = fallbackTimeBase ?? [1, 1e6];
2043
- const pts64 = hi * 4294967296 + lo;
2044
- const us = Math.round(pts64 * 1e6 * tb[0] / tb[1]);
2045
- if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
2046
- pkt.pts = us;
2047
- pkt.ptshi = us < 0 ? -1 : 0;
2048
- pkt.time_base_num = 1;
2049
- pkt.time_base_den = 1e6;
2050
- return;
2051
- }
2052
- const fallback = nextUs();
2053
- pkt.pts = fallback;
2054
- pkt.ptshi = 0;
2055
- pkt.time_base_num = 1;
2056
- pkt.time_base_den = 1e6;
2057
- }
2058
- function sanitizeFrameTimestamp(frame, nextUs, fallbackTimeBase) {
2059
- const lo = frame.pts ?? 0;
2060
- const hi = frame.ptshi ?? 0;
2061
- const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
2062
- if (isInvalid) {
2063
- const us2 = nextUs();
2064
- frame.pts = us2;
2065
- frame.ptshi = 0;
2066
- return;
2067
- }
2068
- const tb = fallbackTimeBase ?? [1, 1e6];
2069
- const pts64 = hi * 4294967296 + lo;
2070
- const us = Math.round(pts64 * 1e6 * tb[0] / tb[1]);
2071
- if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
2072
- frame.pts = us;
2073
- frame.ptshi = us < 0 ? -1 : 0;
2074
- return;
2075
- }
2076
- const fallback = nextUs();
2077
- frame.pts = fallback;
2078
- frame.ptshi = 0;
2079
- }
2080
- var AV_SAMPLE_FMT_U8 = 0;
2081
- var AV_SAMPLE_FMT_S16 = 1;
2082
- var AV_SAMPLE_FMT_S32 = 2;
2083
- var AV_SAMPLE_FMT_FLT = 3;
2084
- var AV_SAMPLE_FMT_U8P = 5;
2085
- var AV_SAMPLE_FMT_S16P = 6;
2086
- var AV_SAMPLE_FMT_S32P = 7;
2087
- var AV_SAMPLE_FMT_FLTP = 8;
2088
- function libavFrameToInterleavedFloat32(frame) {
2089
- const channels = frame.channels ?? frame.ch_layout_nb_channels ?? 1;
2090
- const sampleRate = frame.sample_rate ?? 44100;
2091
- const nbSamples = frame.nb_samples ?? 0;
2092
- if (nbSamples === 0) return null;
2093
- const out = new Float32Array(nbSamples * channels);
2094
- switch (frame.format) {
2095
- case AV_SAMPLE_FMT_FLTP: {
2096
- const planes = ensurePlanes(frame.data, channels);
2097
- for (let ch = 0; ch < channels; ch++) {
2098
- const plane = asFloat32(planes[ch]);
2099
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i];
2100
- }
2101
- return { data: out, channels, sampleRate };
2102
- }
2103
- case AV_SAMPLE_FMT_FLT: {
2104
- const flat = asFloat32(frame.data);
2105
- for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i];
2106
- return { data: out, channels, sampleRate };
2107
- }
2108
- case AV_SAMPLE_FMT_S16P: {
2109
- const planes = ensurePlanes(frame.data, channels);
2110
- for (let ch = 0; ch < channels; ch++) {
2111
- const plane = asInt16(planes[ch]);
2112
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 32768;
2113
- }
2114
- return { data: out, channels, sampleRate };
2115
- }
2116
- case AV_SAMPLE_FMT_S16: {
2117
- const flat = asInt16(frame.data);
2118
- for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 32768;
2119
- return { data: out, channels, sampleRate };
2120
- }
2121
- case AV_SAMPLE_FMT_S32P: {
2122
- const planes = ensurePlanes(frame.data, channels);
2123
- for (let ch = 0; ch < channels; ch++) {
2124
- const plane = asInt32(planes[ch]);
2125
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 2147483648;
2126
- }
2127
- return { data: out, channels, sampleRate };
2128
- }
2129
- case AV_SAMPLE_FMT_S32: {
2130
- const flat = asInt32(frame.data);
2131
- for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 2147483648;
2132
- return { data: out, channels, sampleRate };
2133
- }
2134
- case AV_SAMPLE_FMT_U8P: {
2135
- const planes = ensurePlanes(frame.data, channels);
2136
- for (let ch = 0; ch < channels; ch++) {
2137
- const plane = asUint8(planes[ch]);
2138
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = (plane[i] - 128) / 128;
2139
- }
2140
- return { data: out, channels, sampleRate };
2141
- }
2142
- case AV_SAMPLE_FMT_U8: {
2143
- const flat = asUint8(frame.data);
2144
- for (let i = 0; i < nbSamples * channels; i++) out[i] = (flat[i] - 128) / 128;
2145
- return { data: out, channels, sampleRate };
2146
- }
2147
- default:
2148
- return null;
2149
- }
2150
- }
2151
- function ensurePlanes(data, channels) {
2152
- if (Array.isArray(data)) return data;
2153
- const arr = data;
2154
- const len = arr.length;
2155
- const perChannel = Math.floor(len / channels);
2156
- const planes = [];
2157
- for (let ch = 0; ch < channels; ch++) {
2158
- planes.push(arr.subarray ? arr.subarray(ch * perChannel, (ch + 1) * perChannel) : arr);
2159
- }
2160
- return planes;
2161
- }
2162
- function asFloat32(x) {
2163
- if (x instanceof Float32Array) return x;
2164
- const ta = x;
2165
- return new Float32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
2166
- }
2167
- function asInt16(x) {
2168
- if (x instanceof Int16Array) return x;
2169
- const ta = x;
2170
- return new Int16Array(ta.buffer, ta.byteOffset, ta.byteLength / 2);
2171
- }
2172
- function asInt32(x) {
2173
- if (x instanceof Int32Array) return x;
2174
- const ta = x;
2175
- return new Int32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
2176
- }
2177
- function asUint8(x) {
2178
- if (x instanceof Uint8Array) return x;
2179
- const ta = x;
2180
- return new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength);
2181
- }
2182
1980
  async function loadBridge() {
2183
1981
  try {
2184
- const wrapper = await import('./libav-import-2JURFHEW.js');
1982
+ const wrapper = await import('./libav-import-6MGLCXVQ.js');
2185
1983
  return wrapper.libavBridge;
2186
1984
  } catch (err) {
2187
1985
  throw new Error(
@@ -2190,12 +1988,41 @@ async function loadBridge() {
2190
1988
  }
2191
1989
  }
2192
1990
 
1991
+ // src/util/time-ranges.ts
1992
+ function makeTimeRanges(ranges) {
1993
+ const frozen = ranges.slice();
1994
+ const impl = {
1995
+ get length() {
1996
+ return frozen.length;
1997
+ },
1998
+ start(index) {
1999
+ if (index < 0 || index >= frozen.length) {
2000
+ throw new DOMException(
2001
+ `TimeRanges.start: index ${index} out of range (length=${frozen.length})`,
2002
+ "IndexSizeError"
2003
+ );
2004
+ }
2005
+ return frozen[index][0];
2006
+ },
2007
+ end(index) {
2008
+ if (index < 0 || index >= frozen.length) {
2009
+ throw new DOMException(
2010
+ `TimeRanges.end: index ${index} out of range (length=${frozen.length})`,
2011
+ "IndexSizeError"
2012
+ );
2013
+ }
2014
+ return frozen[index][1];
2015
+ }
2016
+ };
2017
+ return impl;
2018
+ }
2019
+
2193
2020
  // src/strategies/hybrid/index.ts
2194
2021
  var READY_AUDIO_BUFFER_SECONDS = 0.3;
2195
2022
  var READY_TIMEOUT_SECONDS = 10;
2196
2023
  async function createHybridSession(ctx, target, transport) {
2197
- const { normalizeSource: normalizeSource2 } = await import('./source-QJR3OHTW.js');
2198
- const source = await normalizeSource2(ctx.source);
2024
+ const { normalizeSource } = await import('./source-4TZ6KMNV.js');
2025
+ const source = await normalizeSource(ctx.source);
2199
2026
  const fps = ctx.videoTracks[0]?.fps ?? 30;
2200
2027
  const audio = new AudioOutput();
2201
2028
  const renderer = new VideoRenderer(target, audio, fps);
@@ -2247,6 +2074,18 @@ async function createHybridSession(ctx, target, transport) {
2247
2074
  get: () => ctx.duration ?? NaN
2248
2075
  });
2249
2076
  }
2077
+ Object.defineProperty(target, "readyState", {
2078
+ configurable: true,
2079
+ get: () => {
2080
+ if (!renderer.hasFrames()) return 0;
2081
+ if (!audio.isPlaying() && audio.bufferAhead() <= 0 && !audio.isNoAudio()) return 1;
2082
+ return 2;
2083
+ }
2084
+ });
2085
+ Object.defineProperty(target, "seekable", {
2086
+ configurable: true,
2087
+ get: () => makeTimeRanges(ctx.duration && Number.isFinite(ctx.duration) && ctx.duration > 0 ? [[0, ctx.duration]] : [])
2088
+ });
2250
2089
  async function waitForBuffer() {
2251
2090
  const start = performance.now();
2252
2091
  while (true) {
@@ -2291,7 +2130,24 @@ async function createHybridSession(ctx, target, transport) {
2291
2130
  async seek(time) {
2292
2131
  await doSeek(time);
2293
2132
  },
2294
- async setAudioTrack(_id) {
2133
+ async setAudioTrack(id) {
2134
+ if (!ctx.audioTracks.some((t) => t.id === id)) {
2135
+ console.warn("[avbridge] hybrid: setAudioTrack \u2014 unknown track id", id);
2136
+ return;
2137
+ }
2138
+ const wasPlaying = audio.isPlaying();
2139
+ const currentTime = audio.now();
2140
+ await audio.pause().catch(() => {
2141
+ });
2142
+ await handles.setAudioTrack(id, currentTime).catch(
2143
+ (err) => console.warn("[avbridge] hybrid: handles.setAudioTrack failed:", err)
2144
+ );
2145
+ await audio.reset(currentTime);
2146
+ renderer.flush();
2147
+ if (wasPlaying) {
2148
+ await waitForBuffer();
2149
+ await audio.start();
2150
+ }
2295
2151
  },
2296
2152
  async setSubtitleTrack(_id) {
2297
2153
  },
@@ -2311,6 +2167,8 @@ async function createHybridSession(ctx, target, transport) {
2311
2167
  delete target.paused;
2312
2168
  delete target.volume;
2313
2169
  delete target.muted;
2170
+ delete target.readyState;
2171
+ delete target.seekable;
2314
2172
  } catch {
2315
2173
  }
2316
2174
  },
@@ -2330,7 +2188,8 @@ async function startDecoder(opts) {
2330
2188
  const readPkt = await libav.av_packet_alloc();
2331
2189
  const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
2332
2190
  const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
2333
- const audioStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO) ?? null;
2191
+ const firstAudioTrackId = opts.context.audioTracks[0]?.id;
2192
+ 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;
2334
2193
  if (!videoStream && !audioStream) {
2335
2194
  throw new Error("fallback decoder: file has no decodable streams");
2336
2195
  }
@@ -2546,7 +2405,7 @@ async function startDecoder(opts) {
2546
2405
  if (myToken !== pumpToken || destroyed) return;
2547
2406
  for (const f of frames) {
2548
2407
  if (myToken !== pumpToken || destroyed) return;
2549
- const bridgeOpts = sanitizeFrameTimestamp2(
2408
+ sanitizeFrameTimestamp(
2550
2409
  f,
2551
2410
  () => {
2552
2411
  const ts = syntheticVideoUs;
@@ -2556,7 +2415,7 @@ async function startDecoder(opts) {
2556
2415
  videoTimeBase
2557
2416
  );
2558
2417
  try {
2559
- const vf = bridge.laFrameToVideoFrame(f, bridgeOpts);
2418
+ const vf = bridge.laFrameToVideoFrame(f, { timeBase: [1, 1e6] });
2560
2419
  opts.renderer.enqueue(vf);
2561
2420
  videoFramesDecoded++;
2562
2421
  } catch (err) {
@@ -2584,7 +2443,7 @@ async function startDecoder(opts) {
2584
2443
  if (myToken !== pumpToken || destroyed) return;
2585
2444
  for (const f of frames) {
2586
2445
  if (myToken !== pumpToken || destroyed) return;
2587
- sanitizeFrameTimestamp2(
2446
+ sanitizeFrameTimestamp(
2588
2447
  f,
2589
2448
  () => {
2590
2449
  const ts = syntheticAudioUs;
@@ -2595,7 +2454,7 @@ async function startDecoder(opts) {
2595
2454
  },
2596
2455
  audioTimeBase
2597
2456
  );
2598
- const samples = libavFrameToInterleavedFloat322(f);
2457
+ const samples = libavFrameToInterleavedFloat32(f);
2599
2458
  if (samples) {
2600
2459
  opts.audio.schedule(samples.data, samples.channels, samples.sampleRate);
2601
2460
  audioFramesDecoded++;
@@ -2643,6 +2502,69 @@ async function startDecoder(opts) {
2643
2502
  } catch {
2644
2503
  }
2645
2504
  },
2505
+ async setAudioTrack(trackId, timeSec) {
2506
+ if (audioStream && audioStream.index === trackId) return;
2507
+ const newStream = streams.find(
2508
+ (s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO && s.index === trackId
2509
+ );
2510
+ if (!newStream) {
2511
+ console.warn("[avbridge] fallback: setAudioTrack \u2014 no stream with id", trackId);
2512
+ return;
2513
+ }
2514
+ const newToken = ++pumpToken;
2515
+ if (pumpRunning) {
2516
+ try {
2517
+ await pumpRunning;
2518
+ } catch {
2519
+ }
2520
+ }
2521
+ if (destroyed) return;
2522
+ if (audioDec) {
2523
+ try {
2524
+ await libav.ff_free_decoder?.(audioDec.c, audioDec.pkt, audioDec.frame);
2525
+ } catch {
2526
+ }
2527
+ audioDec = null;
2528
+ }
2529
+ try {
2530
+ const [, c, pkt, frame] = await libav.ff_init_decoder(newStream.codec_id, {
2531
+ codecpar: newStream.codecpar
2532
+ });
2533
+ audioDec = { c, pkt, frame };
2534
+ audioTimeBase = newStream.time_base_num && newStream.time_base_den ? [newStream.time_base_num, newStream.time_base_den] : void 0;
2535
+ } catch (err) {
2536
+ console.warn(
2537
+ "[avbridge] fallback: setAudioTrack init failed \u2014 falling back to no-audio mode:",
2538
+ err.message
2539
+ );
2540
+ audioDec = null;
2541
+ opts.audio.setNoAudio();
2542
+ }
2543
+ audioStream = newStream;
2544
+ try {
2545
+ const tsUs = Math.floor(timeSec * 1e6);
2546
+ const [tsLo, tsHi] = libav.f64toi64 ? libav.f64toi64(tsUs) : [tsUs | 0, Math.floor(tsUs / 4294967296)];
2547
+ await libav.av_seek_frame(
2548
+ fmt_ctx,
2549
+ -1,
2550
+ tsLo,
2551
+ tsHi,
2552
+ libav.AVSEEK_FLAG_BACKWARD ?? 0
2553
+ );
2554
+ } catch (err) {
2555
+ console.warn("[avbridge] fallback: setAudioTrack seek failed:", err);
2556
+ }
2557
+ try {
2558
+ if (videoDec) await libav.avcodec_flush_buffers?.(videoDec.c);
2559
+ } catch {
2560
+ }
2561
+ await flushBSF();
2562
+ syntheticVideoUs = Math.round(timeSec * 1e6);
2563
+ syntheticAudioUs = Math.round(timeSec * 1e6);
2564
+ pumpRunning = pumpLoop(newToken).catch(
2565
+ (err) => console.error("[avbridge] fallback pump failed (post-setAudioTrack):", err)
2566
+ );
2567
+ },
2646
2568
  async seek(timeSec) {
2647
2569
  const newToken = ++pumpToken;
2648
2570
  if (pumpRunning) {
@@ -2699,138 +2621,9 @@ async function startDecoder(opts) {
2699
2621
  }
2700
2622
  };
2701
2623
  }
2702
- function sanitizeFrameTimestamp2(frame, nextUs, fallbackTimeBase) {
2703
- const lo = frame.pts ?? 0;
2704
- const hi = frame.ptshi ?? 0;
2705
- const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
2706
- if (isInvalid) {
2707
- const us2 = nextUs();
2708
- frame.pts = us2;
2709
- frame.ptshi = 0;
2710
- return { timeBase: [1, 1e6] };
2711
- }
2712
- const tb = fallbackTimeBase ?? [1, 1e6];
2713
- const pts64 = hi * 4294967296 + lo;
2714
- const us = Math.round(pts64 * 1e6 * tb[0] / tb[1]);
2715
- if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
2716
- frame.pts = us;
2717
- frame.ptshi = us < 0 ? -1 : 0;
2718
- return { timeBase: [1, 1e6] };
2719
- }
2720
- const fallback = nextUs();
2721
- frame.pts = fallback;
2722
- frame.ptshi = 0;
2723
- return { timeBase: [1, 1e6] };
2724
- }
2725
- var AV_SAMPLE_FMT_U82 = 0;
2726
- var AV_SAMPLE_FMT_S162 = 1;
2727
- var AV_SAMPLE_FMT_S322 = 2;
2728
- var AV_SAMPLE_FMT_FLT2 = 3;
2729
- var AV_SAMPLE_FMT_U8P2 = 5;
2730
- var AV_SAMPLE_FMT_S16P2 = 6;
2731
- var AV_SAMPLE_FMT_S32P2 = 7;
2732
- var AV_SAMPLE_FMT_FLTP2 = 8;
2733
- function libavFrameToInterleavedFloat322(frame) {
2734
- const channels = frame.channels ?? frame.ch_layout_nb_channels ?? 1;
2735
- const sampleRate = frame.sample_rate ?? 44100;
2736
- const nbSamples = frame.nb_samples ?? 0;
2737
- if (nbSamples === 0) return null;
2738
- const out = new Float32Array(nbSamples * channels);
2739
- switch (frame.format) {
2740
- case AV_SAMPLE_FMT_FLTP2: {
2741
- const planes = ensurePlanes2(frame.data, channels);
2742
- for (let ch = 0; ch < channels; ch++) {
2743
- const plane = asFloat322(planes[ch]);
2744
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i];
2745
- }
2746
- return { data: out, channels, sampleRate };
2747
- }
2748
- case AV_SAMPLE_FMT_FLT2: {
2749
- const flat = asFloat322(frame.data);
2750
- for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i];
2751
- return { data: out, channels, sampleRate };
2752
- }
2753
- case AV_SAMPLE_FMT_S16P2: {
2754
- const planes = ensurePlanes2(frame.data, channels);
2755
- for (let ch = 0; ch < channels; ch++) {
2756
- const plane = asInt162(planes[ch]);
2757
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 32768;
2758
- }
2759
- return { data: out, channels, sampleRate };
2760
- }
2761
- case AV_SAMPLE_FMT_S162: {
2762
- const flat = asInt162(frame.data);
2763
- for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 32768;
2764
- return { data: out, channels, sampleRate };
2765
- }
2766
- case AV_SAMPLE_FMT_S32P2: {
2767
- const planes = ensurePlanes2(frame.data, channels);
2768
- for (let ch = 0; ch < channels; ch++) {
2769
- const plane = asInt322(planes[ch]);
2770
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 2147483648;
2771
- }
2772
- return { data: out, channels, sampleRate };
2773
- }
2774
- case AV_SAMPLE_FMT_S322: {
2775
- const flat = asInt322(frame.data);
2776
- for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 2147483648;
2777
- return { data: out, channels, sampleRate };
2778
- }
2779
- case AV_SAMPLE_FMT_U8P2: {
2780
- const planes = ensurePlanes2(frame.data, channels);
2781
- for (let ch = 0; ch < channels; ch++) {
2782
- const plane = asUint82(planes[ch]);
2783
- for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = (plane[i] - 128) / 128;
2784
- }
2785
- return { data: out, channels, sampleRate };
2786
- }
2787
- case AV_SAMPLE_FMT_U82: {
2788
- const flat = asUint82(frame.data);
2789
- for (let i = 0; i < nbSamples * channels; i++) out[i] = (flat[i] - 128) / 128;
2790
- return { data: out, channels, sampleRate };
2791
- }
2792
- default:
2793
- if (!globalThis.__avbridgeLoggedSampleFmt) {
2794
- globalThis.__avbridgeLoggedSampleFmt = frame.format;
2795
- console.warn(`[avbridge] unsupported audio sample format from libav: ${frame.format}`);
2796
- }
2797
- return null;
2798
- }
2799
- }
2800
- function ensurePlanes2(data, channels) {
2801
- if (Array.isArray(data)) return data;
2802
- const arr = data;
2803
- const len = arr.length;
2804
- const perChannel = Math.floor(len / channels);
2805
- const planes = [];
2806
- for (let ch = 0; ch < channels; ch++) {
2807
- planes.push(arr.subarray ? arr.subarray(ch * perChannel, (ch + 1) * perChannel) : arr);
2808
- }
2809
- return planes;
2810
- }
2811
- function asFloat322(x) {
2812
- if (x instanceof Float32Array) return x;
2813
- const ta = x;
2814
- return new Float32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
2815
- }
2816
- function asInt162(x) {
2817
- if (x instanceof Int16Array) return x;
2818
- const ta = x;
2819
- return new Int16Array(ta.buffer, ta.byteOffset, ta.byteLength / 2);
2820
- }
2821
- function asInt322(x) {
2822
- if (x instanceof Int32Array) return x;
2823
- const ta = x;
2824
- return new Int32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
2825
- }
2826
- function asUint82(x) {
2827
- if (x instanceof Uint8Array) return x;
2828
- const ta = x;
2829
- return new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength);
2830
- }
2831
2624
  async function loadBridge2() {
2832
2625
  try {
2833
- const wrapper = await import('./libav-import-2JURFHEW.js');
2626
+ const wrapper = await import('./libav-import-6MGLCXVQ.js');
2834
2627
  return wrapper.libavBridge;
2835
2628
  } catch (err) {
2836
2629
  throw new Error(
@@ -2843,8 +2636,8 @@ async function loadBridge2() {
2843
2636
  var READY_AUDIO_BUFFER_SECONDS2 = 0.04;
2844
2637
  var READY_TIMEOUT_SECONDS2 = 3;
2845
2638
  async function createFallbackSession(ctx, target, transport) {
2846
- const { normalizeSource: normalizeSource2 } = await import('./source-QJR3OHTW.js');
2847
- const source = await normalizeSource2(ctx.source);
2639
+ const { normalizeSource } = await import('./source-4TZ6KMNV.js');
2640
+ const source = await normalizeSource(ctx.source);
2848
2641
  const fps = ctx.videoTracks[0]?.fps ?? 30;
2849
2642
  const audio = new AudioOutput();
2850
2643
  const renderer = new VideoRenderer(target, audio, fps);
@@ -2896,6 +2689,18 @@ async function createFallbackSession(ctx, target, transport) {
2896
2689
  get: () => ctx.duration ?? NaN
2897
2690
  });
2898
2691
  }
2692
+ Object.defineProperty(target, "readyState", {
2693
+ configurable: true,
2694
+ get: () => {
2695
+ if (!renderer.hasFrames()) return 0;
2696
+ if (!audio.isPlaying() && audio.bufferAhead() <= 0 && !audio.isNoAudio()) return 1;
2697
+ return 2;
2698
+ }
2699
+ });
2700
+ Object.defineProperty(target, "seekable", {
2701
+ configurable: true,
2702
+ get: () => makeTimeRanges(ctx.duration && Number.isFinite(ctx.duration) && ctx.duration > 0 ? [[0, ctx.duration]] : [])
2703
+ });
2899
2704
  async function waitForBuffer() {
2900
2705
  const start = performance.now();
2901
2706
  let firstFrameAtMs = 0;
@@ -2964,7 +2769,24 @@ async function createFallbackSession(ctx, target, transport) {
2964
2769
  async seek(time) {
2965
2770
  await doSeek(time);
2966
2771
  },
2967
- async setAudioTrack(_id) {
2772
+ async setAudioTrack(id) {
2773
+ if (!ctx.audioTracks.some((t) => t.id === id)) {
2774
+ console.warn("[avbridge] fallback: setAudioTrack \u2014 unknown track id", id);
2775
+ return;
2776
+ }
2777
+ const wasPlaying = audio.isPlaying();
2778
+ const currentTime = audio.now();
2779
+ await audio.pause().catch(() => {
2780
+ });
2781
+ await handles.setAudioTrack(id, currentTime).catch(
2782
+ (err) => console.warn("[avbridge] fallback: handles.setAudioTrack failed:", err)
2783
+ );
2784
+ await audio.reset(currentTime);
2785
+ renderer.flush();
2786
+ if (wasPlaying) {
2787
+ await waitForBuffer();
2788
+ await audio.start();
2789
+ }
2968
2790
  },
2969
2791
  async setSubtitleTrack(_id) {
2970
2792
  },
@@ -2981,6 +2803,8 @@ async function createFallbackSession(ctx, target, transport) {
2981
2803
  delete target.paused;
2982
2804
  delete target.volume;
2983
2805
  delete target.muted;
2806
+ delete target.readyState;
2807
+ delete target.seekable;
2984
2808
  } catch {
2985
2809
  }
2986
2810
  },
@@ -3018,89 +2842,6 @@ function registerBuiltins(registry) {
3018
2842
  registry.register(fallbackPlugin);
3019
2843
  }
3020
2844
 
3021
- // src/subtitles/vtt.ts
3022
- function isVtt(text) {
3023
- const trimmed = text.replace(/^\ufeff/, "").trimStart();
3024
- return trimmed.startsWith("WEBVTT");
3025
- }
3026
-
3027
- // src/subtitles/index.ts
3028
- async function discoverSidecars(file, directory) {
3029
- const baseName = file.name.replace(/\.[^.]+$/, "");
3030
- const found = [];
3031
- for await (const [name, handle] of directory) {
3032
- if (handle.kind !== "file") continue;
3033
- if (!name.startsWith(baseName)) continue;
3034
- const lower = name.toLowerCase();
3035
- let format = null;
3036
- if (lower.endsWith(".srt")) format = "srt";
3037
- else if (lower.endsWith(".vtt")) format = "vtt";
3038
- if (!format) continue;
3039
- const sidecarFile = await handle.getFile();
3040
- const url = URL.createObjectURL(sidecarFile);
3041
- const langMatch = name.slice(baseName.length).match(/[._-]([a-z]{2,3})(?:[._-]|\.)/i);
3042
- found.push({
3043
- url,
3044
- format,
3045
- language: langMatch?.[1]
3046
- });
3047
- }
3048
- return found;
3049
- }
3050
- var SubtitleResourceBag = class {
3051
- urls = /* @__PURE__ */ new Set();
3052
- /** Track an externally-created blob URL (e.g. from `discoverSidecars`). */
3053
- track(url) {
3054
- this.urls.add(url);
3055
- }
3056
- /** Convenience: create a blob URL and track it in one call. */
3057
- createObjectURL(blob) {
3058
- const url = URL.createObjectURL(blob);
3059
- this.urls.add(url);
3060
- return url;
3061
- }
3062
- /** Revoke every tracked URL. Idempotent — safe to call multiple times. */
3063
- revokeAll() {
3064
- for (const u of this.urls) URL.revokeObjectURL(u);
3065
- this.urls.clear();
3066
- }
3067
- };
3068
- async function attachSubtitleTracks(video, tracks, bag, onError, transport) {
3069
- const doFetch = fetchWith(transport);
3070
- for (const t of Array.from(video.querySelectorAll("track[data-avbridge]"))) {
3071
- t.remove();
3072
- }
3073
- for (const t of tracks) {
3074
- if (!t.sidecarUrl) continue;
3075
- try {
3076
- let url = t.sidecarUrl;
3077
- if (t.format === "srt") {
3078
- const res = await doFetch(t.sidecarUrl, transport?.requestInit);
3079
- const text = await res.text();
3080
- const vtt = srtToVtt(text);
3081
- const blob = new Blob([vtt], { type: "text/vtt" });
3082
- url = bag ? bag.createObjectURL(blob) : URL.createObjectURL(blob);
3083
- } else if (t.format === "vtt") {
3084
- const res = await doFetch(t.sidecarUrl, transport?.requestInit);
3085
- const text = await res.text();
3086
- if (!isVtt(text)) {
3087
- console.warn("[avbridge] subtitle missing WEBVTT header:", t.sidecarUrl);
3088
- }
3089
- }
3090
- const trackEl = document.createElement("track");
3091
- trackEl.kind = "subtitles";
3092
- trackEl.src = url;
3093
- trackEl.srclang = t.language ?? "und";
3094
- trackEl.label = t.language ?? `Subtitle ${t.id}`;
3095
- trackEl.dataset.avbridge = "true";
3096
- video.appendChild(trackEl);
3097
- } catch (err) {
3098
- const e = err instanceof Error ? err : new Error(String(err));
3099
- onError?.(e, t);
3100
- }
3101
- }
3102
- }
3103
-
3104
2845
  // src/player.ts
3105
2846
  var UnifiedPlayer = class _UnifiedPlayer {
3106
2847
  /**
@@ -3213,17 +2954,15 @@ var UnifiedPlayer = class _UnifiedPlayer {
3213
2954
  reason: decision.reason
3214
2955
  });
3215
2956
  await this.startSession(decision.strategy, decision.reason);
3216
- if (this.session.strategy !== "fallback" && this.session.strategy !== "hybrid") {
3217
- await attachSubtitleTracks(
3218
- this.options.target,
3219
- ctx.subtitleTracks,
3220
- this.subtitleResources,
3221
- (err, track) => {
3222
- console.warn(`[avbridge] subtitle ${track.id} failed: ${err.message}`);
3223
- },
3224
- this.transport
3225
- );
3226
- }
2957
+ await attachSubtitleTracks(
2958
+ this.options.target,
2959
+ ctx.subtitleTracks,
2960
+ this.subtitleResources,
2961
+ (err, track) => {
2962
+ console.warn(`[avbridge] subtitle ${track.id} failed: ${err.message}`);
2963
+ },
2964
+ this.transport
2965
+ );
3227
2966
  this.emitter.emitSticky("tracks", {
3228
2967
  video: ctx.videoTracks,
3229
2968
  audio: ctx.audioTracks,
@@ -3618,6 +3357,6 @@ function defaultFallbackChain(strategy) {
3618
3357
  }
3619
3358
  }
3620
3359
 
3621
- export { FALLBACK_AUDIO_CODECS, FALLBACK_VIDEO_CODECS, NATIVE_AUDIO_CODECS, NATIVE_VIDEO_CODECS, UnifiedPlayer, avbridgeAudioToMediabunny, avbridgeVideoToMediabunny, buildMediabunnySourceFromInput, classifyContext, createPlayer, probe, srtToVtt };
3622
- //# sourceMappingURL=chunk-NV7ILLWH.js.map
3623
- //# sourceMappingURL=chunk-NV7ILLWH.js.map
3360
+ export { FALLBACK_AUDIO_CODECS, FALLBACK_VIDEO_CODECS, NATIVE_AUDIO_CODECS, NATIVE_VIDEO_CODECS, UnifiedPlayer, classifyContext, createPlayer };
3361
+ //# sourceMappingURL=chunk-OGYHFY6K.js.map
3362
+ //# sourceMappingURL=chunk-OGYHFY6K.js.map