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