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