avbridge 2.2.0 → 2.3.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 +125 -1
- package/NOTICE.md +2 -2
- package/README.md +100 -74
- package/THIRD_PARTY_LICENSES.md +2 -2
- package/dist/avi-2JPBSHGA.js +183 -0
- package/dist/avi-2JPBSHGA.js.map +1 -0
- package/dist/avi-F6WZJK5T.cjs +185 -0
- package/dist/avi-F6WZJK5T.cjs.map +1 -0
- package/dist/{avi-GCGM7OJI.js → avi-NJXAXUXK.js} +9 -3
- package/dist/avi-NJXAXUXK.js.map +1 -0
- package/dist/{avi-6SJLWIWW.cjs → avi-W6L3BTWU.cjs} +10 -4
- package/dist/avi-W6L3BTWU.cjs.map +1 -0
- package/dist/{chunk-ILKDNBSE.js → chunk-2PGRFCWB.js} +59 -10
- package/dist/chunk-2PGRFCWB.js.map +1 -0
- package/dist/chunk-5YAWWKA3.js +18 -0
- package/dist/chunk-5YAWWKA3.js.map +1 -0
- package/dist/chunk-6UUT4BEA.cjs +219 -0
- package/dist/chunk-6UUT4BEA.cjs.map +1 -0
- package/dist/{chunk-OE66B34H.cjs → chunk-7RGG6ME7.cjs} +562 -94
- package/dist/chunk-7RGG6ME7.cjs.map +1 -0
- package/dist/{chunk-WD2ZNQA7.js → chunk-DCSOQH2N.js} +7 -4
- package/dist/chunk-DCSOQH2N.js.map +1 -0
- package/dist/chunk-F3LQJKXK.cjs +20 -0
- package/dist/chunk-F3LQJKXK.cjs.map +1 -0
- package/dist/chunk-IAYKFGFG.js +200 -0
- package/dist/chunk-IAYKFGFG.js.map +1 -0
- package/dist/chunk-NNVOHKXJ.cjs +204 -0
- package/dist/chunk-NNVOHKXJ.cjs.map +1 -0
- package/dist/{chunk-C5VA5U5O.js → chunk-NV7ILLWH.js} +556 -92
- package/dist/chunk-NV7ILLWH.js.map +1 -0
- package/dist/{chunk-HZLQNKFN.cjs → chunk-QQXBPW72.js} +54 -15
- package/dist/chunk-QQXBPW72.js.map +1 -0
- package/dist/chunk-XKPSTC34.cjs +210 -0
- package/dist/chunk-XKPSTC34.cjs.map +1 -0
- package/dist/{chunk-L4NPOJ36.cjs → chunk-Z33SBWL5.cjs} +7 -4
- package/dist/chunk-Z33SBWL5.cjs.map +1 -0
- package/dist/element-browser.js +631 -103
- package/dist/element-browser.js.map +1 -1
- package/dist/element.cjs +4 -4
- package/dist/element.d.cts +1 -1
- package/dist/element.d.ts +1 -1
- package/dist/element.js +3 -3
- package/dist/index.cjs +174 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +48 -4
- package/dist/index.d.ts +48 -4
- package/dist/index.js +93 -12
- package/dist/index.js.map +1 -1
- package/dist/libav-http-reader-AZLE7YFS.cjs +16 -0
- package/dist/{libav-http-reader-FPYDBMYK.cjs.map → libav-http-reader-AZLE7YFS.cjs.map} +1 -1
- package/dist/libav-http-reader-WXG3Z7AI.js +3 -0
- package/dist/{libav-http-reader-NQJVY273.js.map → libav-http-reader-WXG3Z7AI.js.map} +1 -1
- package/dist/{player-DUyvltvy.d.cts → player-B6WB74RD.d.cts} +63 -3
- package/dist/{player-DUyvltvy.d.ts → player-B6WB74RD.d.ts} +63 -3
- package/dist/player.cjs +5500 -0
- package/dist/player.cjs.map +1 -0
- package/dist/player.d.cts +649 -0
- package/dist/player.d.ts +649 -0
- package/dist/player.js +5498 -0
- package/dist/player.js.map +1 -0
- package/dist/source-73CAH6HW.cjs +28 -0
- package/dist/{source-CN43EI7Z.cjs.map → source-73CAH6HW.cjs.map} +1 -1
- package/dist/source-F656KYYV.js +3 -0
- package/dist/{source-FFZ7TW2B.js.map → source-F656KYYV.js.map} +1 -1
- package/dist/source-QJR3OHTW.js +3 -0
- package/dist/source-QJR3OHTW.js.map +1 -0
- package/dist/source-VB74JQ7Z.cjs +28 -0
- package/dist/source-VB74JQ7Z.cjs.map +1 -0
- package/dist/variant-routing-434STYAB.js +3 -0
- package/dist/{variant-routing-JOBWXYKD.js.map → variant-routing-434STYAB.js.map} +1 -1
- package/dist/variant-routing-HONNAA6R.cjs +12 -0
- package/dist/{variant-routing-GOHB2RZN.cjs.map → variant-routing-HONNAA6R.cjs.map} +1 -1
- package/package.json +9 -1
- package/src/classify/rules.ts +27 -5
- package/src/convert/remux.ts +8 -0
- package/src/convert/transcode.ts +41 -8
- package/src/element/avbridge-player.ts +845 -0
- package/src/element/player-icons.ts +25 -0
- package/src/element/player-styles.ts +472 -0
- package/src/errors.ts +47 -0
- package/src/index.ts +23 -0
- package/src/player-element.ts +18 -0
- package/src/player.ts +127 -27
- package/src/plugins/builtin.ts +2 -2
- package/src/probe/avi.ts +4 -0
- package/src/probe/index.ts +40 -10
- package/src/strategies/fallback/audio-output.ts +31 -0
- package/src/strategies/fallback/decoder.ts +83 -2
- package/src/strategies/fallback/index.ts +34 -1
- package/src/strategies/fallback/variant-routing.ts +7 -13
- package/src/strategies/fallback/video-renderer.ts +129 -33
- package/src/strategies/hybrid/decoder.ts +131 -20
- package/src/strategies/hybrid/index.ts +36 -2
- package/src/strategies/remux/index.ts +13 -1
- package/src/strategies/remux/mse.ts +12 -2
- package/src/strategies/remux/pipeline.ts +6 -0
- package/src/subtitles/index.ts +7 -3
- package/src/types.ts +53 -1
- package/src/util/libav-http-reader.ts +5 -1
- package/src/util/source.ts +28 -8
- package/src/util/transport.ts +26 -0
- package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.mjs +1 -1
- package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.wasm +0 -0
- package/dist/avi-6SJLWIWW.cjs.map +0 -1
- package/dist/avi-GCGM7OJI.js.map +0 -1
- package/dist/chunk-C5VA5U5O.js.map +0 -1
- package/dist/chunk-HZLQNKFN.cjs.map +0 -1
- package/dist/chunk-ILKDNBSE.js.map +0 -1
- package/dist/chunk-J5MCMN3S.js +0 -27
- package/dist/chunk-J5MCMN3S.js.map +0 -1
- package/dist/chunk-L4NPOJ36.cjs.map +0 -1
- package/dist/chunk-NZU7W256.cjs +0 -29
- package/dist/chunk-NZU7W256.cjs.map +0 -1
- package/dist/chunk-OE66B34H.cjs.map +0 -1
- package/dist/chunk-WD2ZNQA7.js.map +0 -1
- package/dist/libav-http-reader-FPYDBMYK.cjs +0 -16
- package/dist/libav-http-reader-NQJVY273.js +0 -3
- package/dist/source-CN43EI7Z.cjs +0 -28
- package/dist/source-FFZ7TW2B.js +0 -3
- package/dist/variant-routing-GOHB2RZN.cjs +0 -12
- package/dist/variant-routing-JOBWXYKD.js +0 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { normalizeSource, sniffNormalizedSource } from './chunk-
|
|
1
|
+
import { normalizeSource, sniffNormalizedSource, AvbridgeError, ERR_PROBE_FAILED, ERR_PROBE_UNKNOWN_CONTAINER, ERR_LIBAV_NOT_REACHABLE, ERR_ALL_STRATEGIES_EXHAUSTED, ERR_PLAYER_NOT_READY, fetchWith, ERR_MSE_NOT_SUPPORTED, ERR_MSE_CODEC_NOT_SUPPORTED } from './chunk-2PGRFCWB.js';
|
|
2
2
|
import { dbg, loadLibav } from './chunk-5DMTJVIU.js';
|
|
3
|
-
import { pickLibavVariant } from './chunk-
|
|
3
|
+
import { pickLibavVariant } from './chunk-5YAWWKA3.js';
|
|
4
4
|
|
|
5
5
|
// src/probe/mediabunny.ts
|
|
6
6
|
async function probeWithMediabunny(source, sniffedContainer) {
|
|
@@ -178,37 +178,58 @@ var MEDIABUNNY_CONTAINERS = /* @__PURE__ */ new Set([
|
|
|
178
178
|
"adts",
|
|
179
179
|
"mpegts"
|
|
180
180
|
]);
|
|
181
|
-
async function probe(source) {
|
|
182
|
-
const normalized = await normalizeSource(source);
|
|
181
|
+
async function probe(source, transport) {
|
|
182
|
+
const normalized = await normalizeSource(source, transport);
|
|
183
183
|
const sniffed = await sniffNormalizedSource(normalized);
|
|
184
184
|
if (MEDIABUNNY_CONTAINERS.has(sniffed)) {
|
|
185
185
|
try {
|
|
186
|
-
|
|
186
|
+
const result = await probeWithMediabunny(normalized, sniffed);
|
|
187
|
+
const hasUnknownCodec = result.videoTracks.some((t) => t.codec === "unknown") || result.audioTracks.some((t) => t.codec === "unknown");
|
|
188
|
+
if (hasUnknownCodec) {
|
|
189
|
+
try {
|
|
190
|
+
const { probeWithLibav } = await import('./avi-NJXAXUXK.js');
|
|
191
|
+
return await probeWithLibav(normalized, sniffed);
|
|
192
|
+
} catch {
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return result;
|
|
187
197
|
} catch (mediabunnyErr) {
|
|
188
198
|
console.warn(
|
|
189
199
|
`[avbridge] mediabunny rejected ${sniffed} file, falling back to libav:`,
|
|
190
200
|
mediabunnyErr.message
|
|
191
201
|
);
|
|
192
202
|
try {
|
|
193
|
-
const { probeWithLibav } = await import('./avi-
|
|
203
|
+
const { probeWithLibav } = await import('./avi-NJXAXUXK.js');
|
|
194
204
|
return await probeWithLibav(normalized, sniffed);
|
|
195
205
|
} catch (libavErr) {
|
|
196
206
|
const mbMsg = mediabunnyErr.message || String(mediabunnyErr);
|
|
197
207
|
const lvMsg = libavErr instanceof Error ? libavErr.message : String(libavErr);
|
|
198
|
-
throw new
|
|
199
|
-
|
|
208
|
+
throw new AvbridgeError(
|
|
209
|
+
ERR_PROBE_FAILED,
|
|
210
|
+
`Failed to probe ${sniffed.toUpperCase()} file. mediabunny: ${mbMsg}. libav: ${lvMsg}.`,
|
|
211
|
+
"The file may be corrupt, truncated, or in an unsupported format. Enable AVBRIDGE_DEBUG for detailed logs."
|
|
200
212
|
);
|
|
201
213
|
}
|
|
202
214
|
}
|
|
203
215
|
}
|
|
204
216
|
try {
|
|
205
|
-
const { probeWithLibav } = await import('./avi-
|
|
217
|
+
const { probeWithLibav } = await import('./avi-NJXAXUXK.js');
|
|
206
218
|
return await probeWithLibav(normalized, sniffed);
|
|
207
219
|
} catch (err) {
|
|
208
220
|
const inner = err instanceof Error ? err.message : String(err);
|
|
209
221
|
console.error("[avbridge] libav probe failed for", sniffed, "file:", err);
|
|
210
|
-
|
|
211
|
-
|
|
222
|
+
if (sniffed === "unknown") {
|
|
223
|
+
throw new AvbridgeError(
|
|
224
|
+
ERR_PROBE_UNKNOWN_CONTAINER,
|
|
225
|
+
`Unable to probe source: container format could not be identified. libav fallback: ${inner || "(no details)"}`,
|
|
226
|
+
"The file may be corrupt or in a format avbridge doesn't recognize. Check the file plays in VLC or ffprobe."
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
throw new AvbridgeError(
|
|
230
|
+
ERR_LIBAV_NOT_REACHABLE,
|
|
231
|
+
`${sniffed.toUpperCase()} files require libav.js, which failed to load: ${inner || "(no details)"}`,
|
|
232
|
+
"Install @libav.js/variant-webcodecs, or check that AVBRIDGE_LIBAV_BASE points to the correct path."
|
|
212
233
|
);
|
|
213
234
|
}
|
|
214
235
|
}
|
|
@@ -309,7 +330,9 @@ var FALLBACK_AUDIO_CODECS = /* @__PURE__ */ new Set([
|
|
|
309
330
|
"ra_144",
|
|
310
331
|
"ra_288",
|
|
311
332
|
"sipr",
|
|
312
|
-
"atrac3"
|
|
333
|
+
"atrac3",
|
|
334
|
+
"dts",
|
|
335
|
+
"truehd"
|
|
313
336
|
]);
|
|
314
337
|
var NATIVE_CONTAINERS = /* @__PURE__ */ new Set([
|
|
315
338
|
"mp4",
|
|
@@ -371,7 +394,16 @@ function classifyContext(ctx) {
|
|
|
371
394
|
reason: `video codec "${video.codec}" has no browser decoder; WASM fallback required`
|
|
372
395
|
};
|
|
373
396
|
}
|
|
374
|
-
|
|
397
|
+
const audioNeedsFallback = audio && (FALLBACK_AUDIO_CODECS.has(audio.codec) || !NATIVE_AUDIO_CODECS.has(audio.codec));
|
|
398
|
+
if (audioNeedsFallback) {
|
|
399
|
+
if (NATIVE_VIDEO_CODECS.has(video.codec) && webCodecsAvailable()) {
|
|
400
|
+
return {
|
|
401
|
+
class: "HYBRID_CANDIDATE",
|
|
402
|
+
strategy: "hybrid",
|
|
403
|
+
reason: `video "${video.codec}" is hardware-decodable via WebCodecs; audio "${audio.codec}" decoded in software by libav`,
|
|
404
|
+
fallbackChain: ["fallback"]
|
|
405
|
+
};
|
|
406
|
+
}
|
|
375
407
|
return {
|
|
376
408
|
class: "FALLBACK_REQUIRED",
|
|
377
409
|
strategy: "fallback",
|
|
@@ -738,10 +770,18 @@ var MseSink = class {
|
|
|
738
770
|
constructor(options) {
|
|
739
771
|
this.options = options;
|
|
740
772
|
if (typeof MediaSource === "undefined") {
|
|
741
|
-
throw new
|
|
773
|
+
throw new AvbridgeError(
|
|
774
|
+
ERR_MSE_NOT_SUPPORTED,
|
|
775
|
+
"MediaSource Extensions (MSE) are not supported in this environment.",
|
|
776
|
+
"MSE is required for the remux strategy. Use a browser that supports MSE, or try the fallback strategy."
|
|
777
|
+
);
|
|
742
778
|
}
|
|
743
779
|
if (!MediaSource.isTypeSupported(options.mime)) {
|
|
744
|
-
throw new
|
|
780
|
+
throw new AvbridgeError(
|
|
781
|
+
ERR_MSE_CODEC_NOT_SUPPORTED,
|
|
782
|
+
`This browser's MSE does not support "${options.mime}".`,
|
|
783
|
+
"The codec combination can't be played via remux in this browser. The player will try the next strategy automatically."
|
|
784
|
+
);
|
|
745
785
|
}
|
|
746
786
|
this.mediaSource = new MediaSource();
|
|
747
787
|
this.objectUrl = URL.createObjectURL(this.mediaSource);
|
|
@@ -1070,6 +1110,10 @@ async function createRemuxPipeline(ctx, video) {
|
|
|
1070
1110
|
console.error("[avbridge] remux pipeline reseek failed:", err);
|
|
1071
1111
|
});
|
|
1072
1112
|
},
|
|
1113
|
+
setAutoPlay(autoPlay) {
|
|
1114
|
+
pendingAutoPlay = autoPlay;
|
|
1115
|
+
if (sink) sink.setPlayOnSeek(autoPlay);
|
|
1116
|
+
},
|
|
1073
1117
|
async destroy() {
|
|
1074
1118
|
destroyed = true;
|
|
1075
1119
|
pumpToken++;
|
|
@@ -1110,7 +1154,11 @@ async function createRemuxSession(context, video) {
|
|
|
1110
1154
|
await pipeline.start(video.currentTime || 0, true);
|
|
1111
1155
|
return;
|
|
1112
1156
|
}
|
|
1113
|
-
|
|
1157
|
+
pipeline.setAutoPlay(true);
|
|
1158
|
+
try {
|
|
1159
|
+
await video.play();
|
|
1160
|
+
} catch {
|
|
1161
|
+
}
|
|
1114
1162
|
},
|
|
1115
1163
|
pause() {
|
|
1116
1164
|
wantPlay = false;
|
|
@@ -1149,6 +1197,10 @@ async function createRemuxSession(context, video) {
|
|
|
1149
1197
|
}
|
|
1150
1198
|
|
|
1151
1199
|
// src/strategies/fallback/video-renderer.ts
|
|
1200
|
+
function isDebug() {
|
|
1201
|
+
return typeof globalThis !== "undefined" && !!globalThis.AVBRIDGE_DEBUG;
|
|
1202
|
+
}
|
|
1203
|
+
var lastDebugLog = 0;
|
|
1152
1204
|
var VideoRenderer = class {
|
|
1153
1205
|
constructor(target, clock, fps = 30) {
|
|
1154
1206
|
this.target = target;
|
|
@@ -1158,7 +1210,7 @@ var VideoRenderer = class {
|
|
|
1158
1210
|
this.resolveFirstFrame = resolve;
|
|
1159
1211
|
});
|
|
1160
1212
|
this.canvas = document.createElement("canvas");
|
|
1161
|
-
this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;";
|
|
1213
|
+
this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;object-fit:contain;";
|
|
1162
1214
|
const parent = target.parentElement ?? target.parentNode;
|
|
1163
1215
|
if (parent && parent instanceof HTMLElement) {
|
|
1164
1216
|
if (getComputedStyle(parent).position === "static") {
|
|
@@ -1195,6 +1247,20 @@ var VideoRenderer = class {
|
|
|
1195
1247
|
lastPaintWall = 0;
|
|
1196
1248
|
/** Minimum ms between paints — paces video at roughly source fps. */
|
|
1197
1249
|
paintIntervalMs;
|
|
1250
|
+
/** Cumulative count of frames skipped because all PTS are in the future. */
|
|
1251
|
+
ticksWaiting = 0;
|
|
1252
|
+
/** Cumulative count of ticks where PTS mode painted a frame. */
|
|
1253
|
+
ticksPainted = 0;
|
|
1254
|
+
/**
|
|
1255
|
+
* Calibration offset (microseconds) between video PTS and audio clock.
|
|
1256
|
+
* Video PTS and AudioContext.currentTime can drift ~0.1% relative to
|
|
1257
|
+
* each other (different clock domains). Over 45 minutes that's 2.6s.
|
|
1258
|
+
* We measure the offset on the first painted frame and update it
|
|
1259
|
+
* periodically so the PTS comparison stays calibrated.
|
|
1260
|
+
*/
|
|
1261
|
+
ptsCalibrationUs = 0;
|
|
1262
|
+
ptsCalibrated = false;
|
|
1263
|
+
lastCalibrationWall = 0;
|
|
1198
1264
|
/** Resolves once the first decoded frame has been enqueued. */
|
|
1199
1265
|
firstFrameReady;
|
|
1200
1266
|
resolveFirstFrame;
|
|
@@ -1243,21 +1309,81 @@ var VideoRenderer = class {
|
|
|
1243
1309
|
}
|
|
1244
1310
|
return;
|
|
1245
1311
|
}
|
|
1246
|
-
const
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
if (
|
|
1250
|
-
const
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
this.
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1312
|
+
const rawAudioNowUs = this.clock.now() * 1e6;
|
|
1313
|
+
const headTs = this.queue[0].timestamp ?? 0;
|
|
1314
|
+
const hasPts = headTs > 0 || this.queue.length > 1;
|
|
1315
|
+
if (hasPts) {
|
|
1316
|
+
const wallNow2 = performance.now();
|
|
1317
|
+
if (!this.ptsCalibrated || wallNow2 - this.lastCalibrationWall > 1e4) {
|
|
1318
|
+
this.ptsCalibrationUs = headTs - rawAudioNowUs;
|
|
1319
|
+
this.ptsCalibrated = true;
|
|
1320
|
+
this.lastCalibrationWall = wallNow2;
|
|
1321
|
+
}
|
|
1322
|
+
const audioNowUs = rawAudioNowUs + this.ptsCalibrationUs;
|
|
1323
|
+
const frameDurationUs = this.paintIntervalMs * 1e3;
|
|
1324
|
+
const deadlineUs = audioNowUs + frameDurationUs;
|
|
1325
|
+
let bestIdx = -1;
|
|
1326
|
+
for (let i = 0; i < this.queue.length; i++) {
|
|
1327
|
+
const ts = this.queue[i].timestamp ?? 0;
|
|
1328
|
+
if (ts <= deadlineUs) {
|
|
1329
|
+
bestIdx = i;
|
|
1330
|
+
} else {
|
|
1331
|
+
break;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
if (bestIdx < 0) {
|
|
1335
|
+
this.ticksWaiting++;
|
|
1336
|
+
if (isDebug()) {
|
|
1337
|
+
const now = performance.now();
|
|
1338
|
+
if (now - lastDebugLog > 1e3) {
|
|
1339
|
+
const headPtsMs = (headTs / 1e3).toFixed(1);
|
|
1340
|
+
const audioMs = (audioNowUs / 1e3).toFixed(1);
|
|
1341
|
+
const rawDriftMs = ((headTs - rawAudioNowUs) / 1e3).toFixed(1);
|
|
1342
|
+
const calibMs = (this.ptsCalibrationUs / 1e3).toFixed(1);
|
|
1343
|
+
console.log(
|
|
1344
|
+
`[avbridge:renderer] WAIT q=${this.queue.length} headPTS=${headPtsMs}ms calibAudio=${audioMs}ms rawDrift=${rawDriftMs}ms calib=${calibMs}ms painted=${this.framesPainted} dropped=${this.framesDroppedLate}`
|
|
1345
|
+
);
|
|
1346
|
+
lastDebugLog = now;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1258
1349
|
return;
|
|
1259
1350
|
}
|
|
1351
|
+
const dropThresholdUs = audioNowUs - frameDurationUs * 2;
|
|
1352
|
+
let dropped = 0;
|
|
1353
|
+
while (bestIdx > 0) {
|
|
1354
|
+
const ts = this.queue[0].timestamp ?? 0;
|
|
1355
|
+
if (ts < dropThresholdUs) {
|
|
1356
|
+
this.queue.shift()?.close();
|
|
1357
|
+
this.framesDroppedLate++;
|
|
1358
|
+
bestIdx--;
|
|
1359
|
+
dropped++;
|
|
1360
|
+
} else {
|
|
1361
|
+
break;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
this.ticksPainted++;
|
|
1365
|
+
if (isDebug()) {
|
|
1366
|
+
const now = performance.now();
|
|
1367
|
+
if (now - lastDebugLog > 1e3) {
|
|
1368
|
+
const paintedTs = this.queue[0]?.timestamp ?? 0;
|
|
1369
|
+
const audioMs = (audioNowUs / 1e3).toFixed(1);
|
|
1370
|
+
const ptsMs = (paintedTs / 1e3).toFixed(1);
|
|
1371
|
+
const rawDriftMs = ((paintedTs - rawAudioNowUs) / 1e3).toFixed(1);
|
|
1372
|
+
const calibMs = (this.ptsCalibrationUs / 1e3).toFixed(1);
|
|
1373
|
+
console.log(
|
|
1374
|
+
`[avbridge:renderer] PAINT q=${this.queue.length} calibAudio=${audioMs}ms nextPTS=${ptsMs}ms rawDrift=${rawDriftMs}ms calib=${calibMs}ms dropped=${dropped} total_drops=${this.framesDroppedLate} painted=${this.framesPainted}`
|
|
1375
|
+
);
|
|
1376
|
+
lastDebugLog = now;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
const frame2 = this.queue.shift();
|
|
1380
|
+
this.paint(frame2);
|
|
1381
|
+
frame2.close();
|
|
1382
|
+
this.lastPaintWall = performance.now();
|
|
1383
|
+
return;
|
|
1260
1384
|
}
|
|
1385
|
+
const wallNow = performance.now();
|
|
1386
|
+
if (wallNow - this.lastPaintWall < this.paintIntervalMs - 2) return;
|
|
1261
1387
|
const frame = this.queue.shift();
|
|
1262
1388
|
this.paint(frame);
|
|
1263
1389
|
frame.close();
|
|
@@ -1279,8 +1405,13 @@ var VideoRenderer = class {
|
|
|
1279
1405
|
}
|
|
1280
1406
|
/** Discard all queued frames. Used by seek to drop stale buffers. */
|
|
1281
1407
|
flush() {
|
|
1408
|
+
const count = this.queue.length;
|
|
1282
1409
|
while (this.queue.length > 0) this.queue.shift()?.close();
|
|
1283
1410
|
this.prerolled = false;
|
|
1411
|
+
this.ptsCalibrated = false;
|
|
1412
|
+
if (isDebug() && count > 0) {
|
|
1413
|
+
console.log(`[avbridge:renderer] FLUSH discarded=${count} painted=${this.framesPainted} drops=${this.framesDroppedLate}`);
|
|
1414
|
+
}
|
|
1284
1415
|
}
|
|
1285
1416
|
stats() {
|
|
1286
1417
|
return {
|
|
@@ -1325,11 +1456,38 @@ var AudioOutput = class {
|
|
|
1325
1456
|
pendingQueue = [];
|
|
1326
1457
|
framesScheduled = 0;
|
|
1327
1458
|
destroyed = false;
|
|
1459
|
+
/** User-set volume (0..1). Applied to the gain node. */
|
|
1460
|
+
_volume = 1;
|
|
1461
|
+
/** User-set muted flag. When true, gain is forced to 0. */
|
|
1462
|
+
_muted = false;
|
|
1328
1463
|
constructor() {
|
|
1329
1464
|
this.ctx = new AudioContext();
|
|
1330
1465
|
this.gain = this.ctx.createGain();
|
|
1331
1466
|
this.gain.connect(this.ctx.destination);
|
|
1332
1467
|
}
|
|
1468
|
+
/** Set volume (0..1). Applied immediately to the gain node. */
|
|
1469
|
+
setVolume(v) {
|
|
1470
|
+
this._volume = Math.max(0, Math.min(1, v));
|
|
1471
|
+
this.applyGain();
|
|
1472
|
+
}
|
|
1473
|
+
getVolume() {
|
|
1474
|
+
return this._volume;
|
|
1475
|
+
}
|
|
1476
|
+
/** Set muted. When true, output is silenced regardless of volume. */
|
|
1477
|
+
setMuted(m) {
|
|
1478
|
+
this._muted = m;
|
|
1479
|
+
this.applyGain();
|
|
1480
|
+
}
|
|
1481
|
+
getMuted() {
|
|
1482
|
+
return this._muted;
|
|
1483
|
+
}
|
|
1484
|
+
applyGain() {
|
|
1485
|
+
const target = this._muted ? 0 : this._volume;
|
|
1486
|
+
try {
|
|
1487
|
+
this.gain.gain.value = target;
|
|
1488
|
+
} catch {
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1333
1491
|
/**
|
|
1334
1492
|
* Switch into wall-clock fallback mode. Called by the decoder when no
|
|
1335
1493
|
* audio decoder could be initialized for the source. Once set, this
|
|
@@ -1400,9 +1558,13 @@ var AudioOutput = class {
|
|
|
1400
1558
|
const node = this.ctx.createBufferSource();
|
|
1401
1559
|
node.buffer = buffer;
|
|
1402
1560
|
node.connect(this.gain);
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1561
|
+
let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor);
|
|
1562
|
+
if (ctxStart < this.ctx.currentTime) {
|
|
1563
|
+
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
1564
|
+
this.mediaTimeOfAnchor = this.mediaTimeOfNext;
|
|
1565
|
+
ctxStart = this.ctx.currentTime;
|
|
1566
|
+
}
|
|
1567
|
+
node.start(ctxStart);
|
|
1406
1568
|
this.mediaTimeOfNext += frameCount / sampleRate;
|
|
1407
1569
|
this.framesScheduled++;
|
|
1408
1570
|
}
|
|
@@ -1475,6 +1637,7 @@ var AudioOutput = class {
|
|
|
1475
1637
|
}
|
|
1476
1638
|
this.gain = this.ctx.createGain();
|
|
1477
1639
|
this.gain.connect(this.ctx.destination);
|
|
1640
|
+
this.applyGain();
|
|
1478
1641
|
this.pendingQueue = [];
|
|
1479
1642
|
this.mediaTimeOfAnchor = newMediaTime;
|
|
1480
1643
|
this.mediaTimeOfNext = newMediaTime;
|
|
@@ -1506,8 +1669,8 @@ async function startHybridDecoder(opts) {
|
|
|
1506
1669
|
const variant = pickLibavVariant(opts.context);
|
|
1507
1670
|
const libav = await loadLibav(variant);
|
|
1508
1671
|
const bridge = await loadBridge();
|
|
1509
|
-
const { prepareLibavInput } = await import('./libav-http-reader-
|
|
1510
|
-
const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source);
|
|
1672
|
+
const { prepareLibavInput } = await import('./libav-http-reader-WXG3Z7AI.js');
|
|
1673
|
+
const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source, opts.transport);
|
|
1511
1674
|
const readPkt = await libav.av_packet_alloc();
|
|
1512
1675
|
const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
|
|
1513
1676
|
const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
|
|
@@ -1578,6 +1741,56 @@ async function startHybridDecoder(opts) {
|
|
|
1578
1741
|
});
|
|
1579
1742
|
throw new Error("hybrid decoder: could not initialize any decoders");
|
|
1580
1743
|
}
|
|
1744
|
+
let bsfCtx = null;
|
|
1745
|
+
let bsfPkt = null;
|
|
1746
|
+
if (videoStream && opts.context.videoTracks[0]?.codec === "mpeg4") {
|
|
1747
|
+
try {
|
|
1748
|
+
bsfCtx = await libav.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
|
|
1749
|
+
if (bsfCtx != null && bsfCtx >= 0) {
|
|
1750
|
+
const parIn = await libav.AVBSFContext_par_in(bsfCtx);
|
|
1751
|
+
await libav.avcodec_parameters_copy(parIn, videoStream.codecpar);
|
|
1752
|
+
await libav.av_bsf_init(bsfCtx);
|
|
1753
|
+
bsfPkt = await libav.av_packet_alloc();
|
|
1754
|
+
dbg.info("bsf", "mpeg4_unpack_bframes BSF active (hybrid)");
|
|
1755
|
+
} else {
|
|
1756
|
+
console.warn("[avbridge] mpeg4_unpack_bframes BSF not available in hybrid decoder");
|
|
1757
|
+
bsfCtx = null;
|
|
1758
|
+
}
|
|
1759
|
+
} catch (err) {
|
|
1760
|
+
console.warn("[avbridge] hybrid: failed to init BSF:", err.message);
|
|
1761
|
+
bsfCtx = null;
|
|
1762
|
+
bsfPkt = null;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
async function applyBSF(packets) {
|
|
1766
|
+
if (!bsfCtx || !bsfPkt) return packets;
|
|
1767
|
+
const out = [];
|
|
1768
|
+
for (const pkt of packets) {
|
|
1769
|
+
await libav.ff_copyin_packet(bsfPkt, pkt);
|
|
1770
|
+
const sendErr = await libav.av_bsf_send_packet(bsfCtx, bsfPkt);
|
|
1771
|
+
if (sendErr < 0) {
|
|
1772
|
+
out.push(pkt);
|
|
1773
|
+
continue;
|
|
1774
|
+
}
|
|
1775
|
+
while (true) {
|
|
1776
|
+
const recvErr = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
|
|
1777
|
+
if (recvErr < 0) break;
|
|
1778
|
+
out.push(await libav.ff_copyout_packet(bsfPkt));
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
return out;
|
|
1782
|
+
}
|
|
1783
|
+
async function flushBSF() {
|
|
1784
|
+
if (!bsfCtx || !bsfPkt) return;
|
|
1785
|
+
try {
|
|
1786
|
+
await libav.av_bsf_send_packet(bsfCtx, 0);
|
|
1787
|
+
while (true) {
|
|
1788
|
+
const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
|
|
1789
|
+
if (err < 0) break;
|
|
1790
|
+
}
|
|
1791
|
+
} catch {
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1581
1794
|
let destroyed = false;
|
|
1582
1795
|
let pumpToken = 0;
|
|
1583
1796
|
let pumpRunning = null;
|
|
@@ -1605,8 +1818,15 @@ async function startHybridDecoder(opts) {
|
|
|
1605
1818
|
if (myToken !== pumpToken || destroyed) return;
|
|
1606
1819
|
const videoPackets = videoStream ? packets[videoStream.index] : void 0;
|
|
1607
1820
|
const audioPackets = audioStream ? packets[audioStream.index] : void 0;
|
|
1821
|
+
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
1822
|
+
await decodeAudioBatch(audioPackets, myToken);
|
|
1823
|
+
}
|
|
1824
|
+
if (myToken !== pumpToken || destroyed) return;
|
|
1825
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
1826
|
+
if (myToken !== pumpToken || destroyed) return;
|
|
1608
1827
|
if (videoDecoder && videoPackets && videoPackets.length > 0) {
|
|
1609
|
-
|
|
1828
|
+
const processed = await applyBSF(videoPackets);
|
|
1829
|
+
for (const pkt of processed) {
|
|
1610
1830
|
if (myToken !== pumpToken || destroyed) return;
|
|
1611
1831
|
sanitizePacketTimestamp(pkt, () => {
|
|
1612
1832
|
const ts = syntheticVideoUs;
|
|
@@ -1626,9 +1846,6 @@ async function startHybridDecoder(opts) {
|
|
|
1626
1846
|
}
|
|
1627
1847
|
}
|
|
1628
1848
|
}
|
|
1629
|
-
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
1630
|
-
await decodeAudioBatch(audioPackets, myToken);
|
|
1631
|
-
}
|
|
1632
1849
|
packetsRead += (videoPackets?.length ?? 0) + (audioPackets?.length ?? 0);
|
|
1633
1850
|
while (!destroyed && myToken === pumpToken && (videoDecoder && videoDecoder.decodeQueueSize > 10 || opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
|
|
1634
1851
|
await new Promise((r) => setTimeout(r, 50));
|
|
@@ -1651,20 +1868,43 @@ async function startHybridDecoder(opts) {
|
|
|
1651
1868
|
}
|
|
1652
1869
|
async function decodeAudioBatch(pkts, myToken, flush = false) {
|
|
1653
1870
|
if (!audioDec || destroyed || myToken !== pumpToken) return;
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1871
|
+
const AUDIO_SUB_BATCH = 4;
|
|
1872
|
+
let allFrames = [];
|
|
1873
|
+
for (let i = 0; i < pkts.length; i += AUDIO_SUB_BATCH) {
|
|
1874
|
+
if (myToken !== pumpToken || destroyed) return;
|
|
1875
|
+
const slice = pkts.slice(i, i + AUDIO_SUB_BATCH);
|
|
1876
|
+
const isLast = i + AUDIO_SUB_BATCH >= pkts.length;
|
|
1877
|
+
try {
|
|
1878
|
+
const frames2 = await libav.ff_decode_multi(
|
|
1879
|
+
audioDec.c,
|
|
1880
|
+
audioDec.pkt,
|
|
1881
|
+
audioDec.frame,
|
|
1882
|
+
slice,
|
|
1883
|
+
isLast && flush ? { fin: true, ignoreErrors: true } : { ignoreErrors: true }
|
|
1884
|
+
);
|
|
1885
|
+
allFrames = allFrames.concat(frames2);
|
|
1886
|
+
} catch (err) {
|
|
1887
|
+
console.error("[avbridge] hybrid audio decode failed:", err);
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
if (!isLast) await new Promise((r) => setTimeout(r, 0));
|
|
1891
|
+
}
|
|
1892
|
+
if (pkts.length === 0 && flush) {
|
|
1893
|
+
try {
|
|
1894
|
+
allFrames = await libav.ff_decode_multi(
|
|
1895
|
+
audioDec.c,
|
|
1896
|
+
audioDec.pkt,
|
|
1897
|
+
audioDec.frame,
|
|
1898
|
+
[],
|
|
1899
|
+
{ fin: true, ignoreErrors: true }
|
|
1900
|
+
);
|
|
1901
|
+
} catch (err) {
|
|
1902
|
+
console.error("[avbridge] hybrid audio flush failed:", err);
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1666
1905
|
}
|
|
1667
1906
|
if (myToken !== pumpToken || destroyed) return;
|
|
1907
|
+
const frames = allFrames;
|
|
1668
1908
|
for (const f of frames) {
|
|
1669
1909
|
if (myToken !== pumpToken || destroyed) return;
|
|
1670
1910
|
sanitizeFrameTimestamp(
|
|
@@ -1701,6 +1941,14 @@ async function startHybridDecoder(opts) {
|
|
|
1701
1941
|
await pumpRunning;
|
|
1702
1942
|
} catch {
|
|
1703
1943
|
}
|
|
1944
|
+
try {
|
|
1945
|
+
if (bsfCtx) await libav.av_bsf_free(bsfCtx);
|
|
1946
|
+
} catch {
|
|
1947
|
+
}
|
|
1948
|
+
try {
|
|
1949
|
+
if (bsfPkt) await libav.av_packet_free?.(bsfPkt);
|
|
1950
|
+
} catch {
|
|
1951
|
+
}
|
|
1704
1952
|
try {
|
|
1705
1953
|
if (videoDecoder && videoDecoder.state !== "closed") videoDecoder.close();
|
|
1706
1954
|
} catch {
|
|
@@ -1754,6 +2002,7 @@ async function startHybridDecoder(opts) {
|
|
|
1754
2002
|
if (audioDec) await libav.avcodec_flush_buffers?.(audioDec.c);
|
|
1755
2003
|
} catch {
|
|
1756
2004
|
}
|
|
2005
|
+
await flushBSF();
|
|
1757
2006
|
syntheticVideoUs = Math.round(timeSec * 1e6);
|
|
1758
2007
|
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
1759
2008
|
pumpRunning = pumpLoop(newToken).catch(
|
|
@@ -1767,6 +2016,7 @@ async function startHybridDecoder(opts) {
|
|
|
1767
2016
|
videoFramesDecoded,
|
|
1768
2017
|
videoChunksFed,
|
|
1769
2018
|
audioFramesDecoded,
|
|
2019
|
+
bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
|
|
1770
2020
|
videoDecodeQueueSize: videoDecoder?.decodeQueueSize ?? 0,
|
|
1771
2021
|
// Confirmed transport info — see fallback decoder for the pattern.
|
|
1772
2022
|
_transport: inputHandle.transport === "http-range" ? "http-range" : "memory",
|
|
@@ -1943,8 +2193,8 @@ async function loadBridge() {
|
|
|
1943
2193
|
// src/strategies/hybrid/index.ts
|
|
1944
2194
|
var READY_AUDIO_BUFFER_SECONDS = 0.3;
|
|
1945
2195
|
var READY_TIMEOUT_SECONDS = 10;
|
|
1946
|
-
async function createHybridSession(ctx, target) {
|
|
1947
|
-
const { normalizeSource: normalizeSource2 } = await import('./source-
|
|
2196
|
+
async function createHybridSession(ctx, target, transport) {
|
|
2197
|
+
const { normalizeSource: normalizeSource2 } = await import('./source-QJR3OHTW.js');
|
|
1948
2198
|
const source = await normalizeSource2(ctx.source);
|
|
1949
2199
|
const fps = ctx.videoTracks[0]?.fps ?? 30;
|
|
1950
2200
|
const audio = new AudioOutput();
|
|
@@ -1956,7 +2206,8 @@ async function createHybridSession(ctx, target) {
|
|
|
1956
2206
|
filename: ctx.name ?? "input.bin",
|
|
1957
2207
|
context: ctx,
|
|
1958
2208
|
renderer,
|
|
1959
|
-
audio
|
|
2209
|
+
audio,
|
|
2210
|
+
transport
|
|
1960
2211
|
});
|
|
1961
2212
|
} catch (err) {
|
|
1962
2213
|
audio.destroy();
|
|
@@ -1970,6 +2221,26 @@ async function createHybridSession(ctx, target) {
|
|
|
1970
2221
|
void doSeek(v);
|
|
1971
2222
|
}
|
|
1972
2223
|
});
|
|
2224
|
+
Object.defineProperty(target, "paused", {
|
|
2225
|
+
configurable: true,
|
|
2226
|
+
get: () => !audio.isPlaying()
|
|
2227
|
+
});
|
|
2228
|
+
Object.defineProperty(target, "volume", {
|
|
2229
|
+
configurable: true,
|
|
2230
|
+
get: () => audio.getVolume(),
|
|
2231
|
+
set: (v) => {
|
|
2232
|
+
audio.setVolume(v);
|
|
2233
|
+
target.dispatchEvent(new Event("volumechange"));
|
|
2234
|
+
}
|
|
2235
|
+
});
|
|
2236
|
+
Object.defineProperty(target, "muted", {
|
|
2237
|
+
configurable: true,
|
|
2238
|
+
get: () => audio.getMuted(),
|
|
2239
|
+
set: (m) => {
|
|
2240
|
+
audio.setMuted(m);
|
|
2241
|
+
target.dispatchEvent(new Event("volumechange"));
|
|
2242
|
+
}
|
|
2243
|
+
});
|
|
1973
2244
|
if (ctx.duration && Number.isFinite(ctx.duration)) {
|
|
1974
2245
|
Object.defineProperty(target, "duration", {
|
|
1975
2246
|
configurable: true,
|
|
@@ -2009,10 +2280,13 @@ async function createHybridSession(ctx, target) {
|
|
|
2009
2280
|
if (!audio.isPlaying()) {
|
|
2010
2281
|
await waitForBuffer();
|
|
2011
2282
|
await audio.start();
|
|
2283
|
+
target.dispatchEvent(new Event("play"));
|
|
2284
|
+
target.dispatchEvent(new Event("playing"));
|
|
2012
2285
|
}
|
|
2013
2286
|
},
|
|
2014
2287
|
pause() {
|
|
2015
2288
|
void audio.pause();
|
|
2289
|
+
target.dispatchEvent(new Event("pause"));
|
|
2016
2290
|
},
|
|
2017
2291
|
async seek(time) {
|
|
2018
2292
|
await doSeek(time);
|
|
@@ -2034,6 +2308,9 @@ async function createHybridSession(ctx, target) {
|
|
|
2034
2308
|
try {
|
|
2035
2309
|
delete target.currentTime;
|
|
2036
2310
|
delete target.duration;
|
|
2311
|
+
delete target.paused;
|
|
2312
|
+
delete target.volume;
|
|
2313
|
+
delete target.muted;
|
|
2037
2314
|
} catch {
|
|
2038
2315
|
}
|
|
2039
2316
|
},
|
|
@@ -2048,8 +2325,8 @@ async function startDecoder(opts) {
|
|
|
2048
2325
|
const variant = pickLibavVariant(opts.context);
|
|
2049
2326
|
const libav = await loadLibav(variant);
|
|
2050
2327
|
const bridge = await loadBridge2();
|
|
2051
|
-
const { prepareLibavInput } = await import('./libav-http-reader-
|
|
2052
|
-
const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source);
|
|
2328
|
+
const { prepareLibavInput } = await import('./libav-http-reader-WXG3Z7AI.js');
|
|
2329
|
+
const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source, opts.transport);
|
|
2053
2330
|
const readPkt = await libav.av_packet_alloc();
|
|
2054
2331
|
const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
|
|
2055
2332
|
const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
|
|
@@ -2105,6 +2382,56 @@ async function startDecoder(opts) {
|
|
|
2105
2382
|
`fallback decoder: could not initialize any libav decoders (${codecs}).${hint}`
|
|
2106
2383
|
);
|
|
2107
2384
|
}
|
|
2385
|
+
let bsfCtx = null;
|
|
2386
|
+
let bsfPkt = null;
|
|
2387
|
+
if (videoStream && opts.context.videoTracks[0]?.codec === "mpeg4") {
|
|
2388
|
+
try {
|
|
2389
|
+
bsfCtx = await libav.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
|
|
2390
|
+
if (bsfCtx != null && bsfCtx >= 0) {
|
|
2391
|
+
const parIn = await libav.AVBSFContext_par_in(bsfCtx);
|
|
2392
|
+
await libav.avcodec_parameters_copy(parIn, videoStream.codecpar);
|
|
2393
|
+
await libav.av_bsf_init(bsfCtx);
|
|
2394
|
+
bsfPkt = await libav.av_packet_alloc();
|
|
2395
|
+
dbg.info("bsf", "mpeg4_unpack_bframes BSF active");
|
|
2396
|
+
} else {
|
|
2397
|
+
console.warn("[avbridge] mpeg4_unpack_bframes BSF not available \u2014 decoding without it");
|
|
2398
|
+
bsfCtx = null;
|
|
2399
|
+
}
|
|
2400
|
+
} catch (err) {
|
|
2401
|
+
console.warn("[avbridge] failed to init mpeg4_unpack_bframes BSF:", err.message);
|
|
2402
|
+
bsfCtx = null;
|
|
2403
|
+
bsfPkt = null;
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
async function applyBSF(packets) {
|
|
2407
|
+
if (!bsfCtx || !bsfPkt) return packets;
|
|
2408
|
+
const out = [];
|
|
2409
|
+
for (const pkt of packets) {
|
|
2410
|
+
await libav.ff_copyin_packet(bsfPkt, pkt);
|
|
2411
|
+
const sendErr = await libav.av_bsf_send_packet(bsfCtx, bsfPkt);
|
|
2412
|
+
if (sendErr < 0) {
|
|
2413
|
+
out.push(pkt);
|
|
2414
|
+
continue;
|
|
2415
|
+
}
|
|
2416
|
+
while (true) {
|
|
2417
|
+
const recvErr = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
|
|
2418
|
+
if (recvErr < 0) break;
|
|
2419
|
+
out.push(await libav.ff_copyout_packet(bsfPkt));
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
return out;
|
|
2423
|
+
}
|
|
2424
|
+
async function flushBSF() {
|
|
2425
|
+
if (!bsfCtx || !bsfPkt) return;
|
|
2426
|
+
try {
|
|
2427
|
+
await libav.av_bsf_send_packet(bsfCtx, 0);
|
|
2428
|
+
while (true) {
|
|
2429
|
+
const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
|
|
2430
|
+
if (err < 0) break;
|
|
2431
|
+
}
|
|
2432
|
+
} catch {
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2108
2435
|
let destroyed = false;
|
|
2109
2436
|
let pumpToken = 0;
|
|
2110
2437
|
let pumpRunning = null;
|
|
@@ -2113,7 +2440,8 @@ async function startDecoder(opts) {
|
|
|
2113
2440
|
let audioFramesDecoded = 0;
|
|
2114
2441
|
let watchdogFirstFrameMs = 0;
|
|
2115
2442
|
let watchdogSlowSinceMs = 0;
|
|
2116
|
-
let
|
|
2443
|
+
let watchdogSlowWarned = false;
|
|
2444
|
+
let watchdogOverflowWarned = false;
|
|
2117
2445
|
let syntheticVideoUs = 0;
|
|
2118
2446
|
let syntheticAudioUs = 0;
|
|
2119
2447
|
const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
|
|
@@ -2125,7 +2453,7 @@ async function startDecoder(opts) {
|
|
|
2125
2453
|
let packets;
|
|
2126
2454
|
try {
|
|
2127
2455
|
[readErr, packets] = await libav.ff_read_frame_multi(fmt_ctx, readPkt, {
|
|
2128
|
-
limit:
|
|
2456
|
+
limit: 16 * 1024
|
|
2129
2457
|
});
|
|
2130
2458
|
} catch (err) {
|
|
2131
2459
|
console.error("[avbridge] ff_read_frame_multi failed:", err);
|
|
@@ -2134,26 +2462,27 @@ async function startDecoder(opts) {
|
|
|
2134
2462
|
if (myToken !== pumpToken || destroyed) return;
|
|
2135
2463
|
const videoPackets = videoStream ? packets[videoStream.index] : void 0;
|
|
2136
2464
|
const audioPackets = audioStream ? packets[audioStream.index] : void 0;
|
|
2137
|
-
if (videoDec && videoPackets && videoPackets.length > 0) {
|
|
2138
|
-
await decodeVideoBatch(videoPackets, myToken);
|
|
2139
|
-
}
|
|
2140
|
-
if (myToken !== pumpToken || destroyed) return;
|
|
2141
2465
|
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
2142
2466
|
await decodeAudioBatch(audioPackets, myToken);
|
|
2143
2467
|
}
|
|
2468
|
+
if (myToken !== pumpToken || destroyed) return;
|
|
2469
|
+
if (videoDec && videoPackets && videoPackets.length > 0) {
|
|
2470
|
+
const processed = await applyBSF(videoPackets);
|
|
2471
|
+
await decodeVideoBatch(processed, myToken);
|
|
2472
|
+
}
|
|
2144
2473
|
packetsRead += (videoPackets?.length ?? 0) + (audioPackets?.length ?? 0);
|
|
2145
2474
|
if (videoFramesDecoded > 0) {
|
|
2146
2475
|
if (watchdogFirstFrameMs === 0) {
|
|
2147
2476
|
watchdogFirstFrameMs = performance.now();
|
|
2148
2477
|
}
|
|
2149
2478
|
const elapsedSinceFirst = (performance.now() - watchdogFirstFrameMs) / 1e3;
|
|
2150
|
-
if (elapsedSinceFirst > 1 && !
|
|
2479
|
+
if (elapsedSinceFirst > 1 && !watchdogSlowWarned) {
|
|
2151
2480
|
const expectedFrames = elapsedSinceFirst * videoFps;
|
|
2152
2481
|
const ratio = videoFramesDecoded / expectedFrames;
|
|
2153
2482
|
if (ratio < 0.6) {
|
|
2154
2483
|
if (watchdogSlowSinceMs === 0) watchdogSlowSinceMs = performance.now();
|
|
2155
2484
|
if ((performance.now() - watchdogSlowSinceMs) / 1e3 > 5) {
|
|
2156
|
-
|
|
2485
|
+
watchdogSlowWarned = true;
|
|
2157
2486
|
console.warn(
|
|
2158
2487
|
"[avbridge:decode-rate]",
|
|
2159
2488
|
`decoder is running slower than realtime: ${videoFramesDecoded} frames in ${elapsedSinceFirst.toFixed(1)}s (${(videoFramesDecoded / elapsedSinceFirst).toFixed(1)} fps vs ${videoFps} fps source \u2014 ${(ratio * 100).toFixed(0)}% of realtime). Playback will stutter. Typical causes: software decode of a codec with no WebCodecs support (rv40, mpeg4 @ 720p+, wmv3), or a resolution too large for single-threaded WASM to keep up with.`
|
|
@@ -2163,6 +2492,17 @@ async function startDecoder(opts) {
|
|
|
2163
2492
|
watchdogSlowSinceMs = 0;
|
|
2164
2493
|
}
|
|
2165
2494
|
}
|
|
2495
|
+
if (!watchdogOverflowWarned && videoFramesDecoded > 100) {
|
|
2496
|
+
const rendererStats = opts.renderer.stats();
|
|
2497
|
+
const overflow = rendererStats.framesDroppedOverflow ?? 0;
|
|
2498
|
+
if (overflow / videoFramesDecoded > 0.1) {
|
|
2499
|
+
watchdogOverflowWarned = true;
|
|
2500
|
+
console.warn(
|
|
2501
|
+
"[avbridge:overflow-drop]",
|
|
2502
|
+
`renderer is dropping ${overflow}/${videoFramesDecoded} frames (${(overflow / videoFramesDecoded * 100).toFixed(0)}%) because the decoder is producing bursts faster than the canvas can drain. Symptom: choppy playback despite decoder keeping up on average. Fix would be smaller read batches in the pump loop or a lower queueHighWater cap \u2014 see src/strategies/fallback/decoder.ts.`
|
|
2503
|
+
);
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2166
2506
|
}
|
|
2167
2507
|
while (!destroyed && myToken === pumpToken && (opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
|
|
2168
2508
|
await new Promise((r) => setTimeout(r, 50));
|
|
@@ -2274,6 +2614,14 @@ async function startDecoder(opts) {
|
|
|
2274
2614
|
await pumpRunning;
|
|
2275
2615
|
} catch {
|
|
2276
2616
|
}
|
|
2617
|
+
try {
|
|
2618
|
+
if (bsfCtx) await libav.av_bsf_free(bsfCtx);
|
|
2619
|
+
} catch {
|
|
2620
|
+
}
|
|
2621
|
+
try {
|
|
2622
|
+
if (bsfPkt) await libav.av_packet_free?.(bsfPkt);
|
|
2623
|
+
} catch {
|
|
2624
|
+
}
|
|
2277
2625
|
try {
|
|
2278
2626
|
if (videoDec) await libav.ff_free_decoder?.(videoDec.c, videoDec.pkt, videoDec.frame);
|
|
2279
2627
|
} catch {
|
|
@@ -2325,6 +2673,7 @@ async function startDecoder(opts) {
|
|
|
2325
2673
|
if (audioDec) await libav.avcodec_flush_buffers?.(audioDec.c);
|
|
2326
2674
|
} catch {
|
|
2327
2675
|
}
|
|
2676
|
+
await flushBSF();
|
|
2328
2677
|
syntheticVideoUs = Math.round(timeSec * 1e6);
|
|
2329
2678
|
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
2330
2679
|
pumpRunning = pumpLoop(newToken).catch(
|
|
@@ -2337,6 +2686,7 @@ async function startDecoder(opts) {
|
|
|
2337
2686
|
packetsRead,
|
|
2338
2687
|
videoFramesDecoded,
|
|
2339
2688
|
audioFramesDecoded,
|
|
2689
|
+
bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
|
|
2340
2690
|
// Confirmed transport info: once prepareLibavInput returns
|
|
2341
2691
|
// successfully, we *know* whether the source is http-range (probe
|
|
2342
2692
|
// succeeded and returned 206) or in-memory blob. Diagnostics hoists
|
|
@@ -2492,8 +2842,8 @@ async function loadBridge2() {
|
|
|
2492
2842
|
// src/strategies/fallback/index.ts
|
|
2493
2843
|
var READY_AUDIO_BUFFER_SECONDS2 = 0.04;
|
|
2494
2844
|
var READY_TIMEOUT_SECONDS2 = 3;
|
|
2495
|
-
async function createFallbackSession(ctx, target) {
|
|
2496
|
-
const { normalizeSource: normalizeSource2 } = await import('./source-
|
|
2845
|
+
async function createFallbackSession(ctx, target, transport) {
|
|
2846
|
+
const { normalizeSource: normalizeSource2 } = await import('./source-QJR3OHTW.js');
|
|
2497
2847
|
const source = await normalizeSource2(ctx.source);
|
|
2498
2848
|
const fps = ctx.videoTracks[0]?.fps ?? 30;
|
|
2499
2849
|
const audio = new AudioOutput();
|
|
@@ -2505,7 +2855,8 @@ async function createFallbackSession(ctx, target) {
|
|
|
2505
2855
|
filename: ctx.name ?? "input.bin",
|
|
2506
2856
|
context: ctx,
|
|
2507
2857
|
renderer,
|
|
2508
|
-
audio
|
|
2858
|
+
audio,
|
|
2859
|
+
transport
|
|
2509
2860
|
});
|
|
2510
2861
|
} catch (err) {
|
|
2511
2862
|
audio.destroy();
|
|
@@ -2519,6 +2870,26 @@ async function createFallbackSession(ctx, target) {
|
|
|
2519
2870
|
void doSeek(v);
|
|
2520
2871
|
}
|
|
2521
2872
|
});
|
|
2873
|
+
Object.defineProperty(target, "paused", {
|
|
2874
|
+
configurable: true,
|
|
2875
|
+
get: () => !audio.isPlaying()
|
|
2876
|
+
});
|
|
2877
|
+
Object.defineProperty(target, "volume", {
|
|
2878
|
+
configurable: true,
|
|
2879
|
+
get: () => audio.getVolume(),
|
|
2880
|
+
set: (v) => {
|
|
2881
|
+
audio.setVolume(v);
|
|
2882
|
+
target.dispatchEvent(new Event("volumechange"));
|
|
2883
|
+
}
|
|
2884
|
+
});
|
|
2885
|
+
Object.defineProperty(target, "muted", {
|
|
2886
|
+
configurable: true,
|
|
2887
|
+
get: () => audio.getMuted(),
|
|
2888
|
+
set: (m) => {
|
|
2889
|
+
audio.setMuted(m);
|
|
2890
|
+
target.dispatchEvent(new Event("volumechange"));
|
|
2891
|
+
}
|
|
2892
|
+
});
|
|
2522
2893
|
if (ctx.duration && Number.isFinite(ctx.duration)) {
|
|
2523
2894
|
Object.defineProperty(target, "duration", {
|
|
2524
2895
|
configurable: true,
|
|
@@ -2527,25 +2898,35 @@ async function createFallbackSession(ctx, target) {
|
|
|
2527
2898
|
}
|
|
2528
2899
|
async function waitForBuffer() {
|
|
2529
2900
|
const start = performance.now();
|
|
2901
|
+
let firstFrameAtMs = 0;
|
|
2530
2902
|
dbg.info(
|
|
2531
2903
|
"cold-start",
|
|
2532
|
-
`gate entry:
|
|
2904
|
+
`gate entry: want audio \u2265 ${READY_AUDIO_BUFFER_SECONDS2 * 1e3}ms + 1 frame`
|
|
2533
2905
|
);
|
|
2534
2906
|
while (true) {
|
|
2535
2907
|
const audioAhead = audio.isNoAudio() ? Infinity : audio.bufferAhead();
|
|
2536
2908
|
const audioReady = audio.isNoAudio() || audioAhead >= READY_AUDIO_BUFFER_SECONDS2;
|
|
2537
2909
|
const hasFrames = renderer.hasFrames();
|
|
2910
|
+
const nowMs = performance.now();
|
|
2911
|
+
if (hasFrames && firstFrameAtMs === 0) firstFrameAtMs = nowMs;
|
|
2538
2912
|
if (audioReady && hasFrames) {
|
|
2539
2913
|
dbg.info(
|
|
2540
2914
|
"cold-start",
|
|
2541
|
-
`gate satisfied in ${(
|
|
2915
|
+
`gate satisfied in ${(nowMs - start).toFixed(0)}ms (audio=${(audioAhead * 1e3).toFixed(0)}ms, frames=${renderer.queueDepth()})`
|
|
2916
|
+
);
|
|
2917
|
+
return;
|
|
2918
|
+
}
|
|
2919
|
+
if (hasFrames && firstFrameAtMs > 0 && nowMs - firstFrameAtMs >= 500) {
|
|
2920
|
+
dbg.info(
|
|
2921
|
+
"cold-start",
|
|
2922
|
+
`gate released on video-only grace at ${(nowMs - start).toFixed(0)}ms (frames=${renderer.queueDepth()}, audio=${(audioAhead * 1e3).toFixed(0)}ms \u2014 demuxer hasn't delivered audio packets yet, starting anyway and letting the audio scheduler catch up at its media-time anchor)`
|
|
2542
2923
|
);
|
|
2543
2924
|
return;
|
|
2544
2925
|
}
|
|
2545
|
-
if ((
|
|
2926
|
+
if ((nowMs - start) / 1e3 > READY_TIMEOUT_SECONDS2) {
|
|
2546
2927
|
dbg.diag(
|
|
2547
2928
|
"cold-start",
|
|
2548
|
-
`gate TIMEOUT after ${READY_TIMEOUT_SECONDS2}s \u2014 audio=${(audioAhead * 1e3).toFixed(0)}ms (needed ${READY_AUDIO_BUFFER_SECONDS2 * 1e3}ms), frames=${renderer.queueDepth()} (needed \u22651).
|
|
2929
|
+
`gate TIMEOUT after ${READY_TIMEOUT_SECONDS2}s \u2014 audio=${(audioAhead * 1e3).toFixed(0)}ms (needed ${READY_AUDIO_BUFFER_SECONDS2 * 1e3}ms), frames=${renderer.queueDepth()} (needed \u22651). Decoder produced nothing in ${READY_TIMEOUT_SECONDS2}s \u2014 either a corrupt source, a missing codec, or WASM is catastrophically slow on this file. Check getDiagnostics().runtime for decode counters.`
|
|
2549
2930
|
);
|
|
2550
2931
|
return;
|
|
2551
2932
|
}
|
|
@@ -2572,10 +2953,13 @@ async function createFallbackSession(ctx, target) {
|
|
|
2572
2953
|
if (!audio.isPlaying()) {
|
|
2573
2954
|
await waitForBuffer();
|
|
2574
2955
|
await audio.start();
|
|
2956
|
+
target.dispatchEvent(new Event("play"));
|
|
2957
|
+
target.dispatchEvent(new Event("playing"));
|
|
2575
2958
|
}
|
|
2576
2959
|
},
|
|
2577
2960
|
pause() {
|
|
2578
2961
|
void audio.pause();
|
|
2962
|
+
target.dispatchEvent(new Event("pause"));
|
|
2579
2963
|
},
|
|
2580
2964
|
async seek(time) {
|
|
2581
2965
|
await doSeek(time);
|
|
@@ -2594,6 +2978,9 @@ async function createFallbackSession(ctx, target) {
|
|
|
2594
2978
|
try {
|
|
2595
2979
|
delete target.currentTime;
|
|
2596
2980
|
delete target.duration;
|
|
2981
|
+
delete target.paused;
|
|
2982
|
+
delete target.volume;
|
|
2983
|
+
delete target.muted;
|
|
2597
2984
|
} catch {
|
|
2598
2985
|
}
|
|
2599
2986
|
},
|
|
@@ -2617,12 +3004,12 @@ var remuxPlugin = {
|
|
|
2617
3004
|
var hybridPlugin = {
|
|
2618
3005
|
name: "hybrid",
|
|
2619
3006
|
canHandle: () => typeof VideoDecoder !== "undefined",
|
|
2620
|
-
execute: (ctx, video) => createHybridSession(ctx, video)
|
|
3007
|
+
execute: (ctx, video, transport) => createHybridSession(ctx, video, transport)
|
|
2621
3008
|
};
|
|
2622
3009
|
var fallbackPlugin = {
|
|
2623
3010
|
name: "fallback",
|
|
2624
3011
|
canHandle: () => true,
|
|
2625
|
-
execute: (ctx, video) => createFallbackSession(ctx, video)
|
|
3012
|
+
execute: (ctx, video, transport) => createFallbackSession(ctx, video, transport)
|
|
2626
3013
|
};
|
|
2627
3014
|
function registerBuiltins(registry) {
|
|
2628
3015
|
registry.register(nativePlugin);
|
|
@@ -2678,7 +3065,8 @@ var SubtitleResourceBag = class {
|
|
|
2678
3065
|
this.urls.clear();
|
|
2679
3066
|
}
|
|
2680
3067
|
};
|
|
2681
|
-
async function attachSubtitleTracks(video, tracks, bag, onError) {
|
|
3068
|
+
async function attachSubtitleTracks(video, tracks, bag, onError, transport) {
|
|
3069
|
+
const doFetch = fetchWith(transport);
|
|
2682
3070
|
for (const t of Array.from(video.querySelectorAll("track[data-avbridge]"))) {
|
|
2683
3071
|
t.remove();
|
|
2684
3072
|
}
|
|
@@ -2687,13 +3075,13 @@ async function attachSubtitleTracks(video, tracks, bag, onError) {
|
|
|
2687
3075
|
try {
|
|
2688
3076
|
let url = t.sidecarUrl;
|
|
2689
3077
|
if (t.format === "srt") {
|
|
2690
|
-
const res = await
|
|
3078
|
+
const res = await doFetch(t.sidecarUrl, transport?.requestInit);
|
|
2691
3079
|
const text = await res.text();
|
|
2692
3080
|
const vtt = srtToVtt(text);
|
|
2693
3081
|
const blob = new Blob([vtt], { type: "text/vtt" });
|
|
2694
3082
|
url = bag ? bag.createObjectURL(blob) : URL.createObjectURL(blob);
|
|
2695
3083
|
} else if (t.format === "vtt") {
|
|
2696
|
-
const res = await
|
|
3084
|
+
const res = await doFetch(t.sidecarUrl, transport?.requestInit);
|
|
2697
3085
|
const text = await res.text();
|
|
2698
3086
|
if (!isVtt(text)) {
|
|
2699
3087
|
console.warn("[avbridge] subtitle missing WEBVTT header:", t.sidecarUrl);
|
|
@@ -2721,6 +3109,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2721
3109
|
constructor(options, registry) {
|
|
2722
3110
|
this.options = options;
|
|
2723
3111
|
this.registry = registry;
|
|
3112
|
+
const { requestInit, fetchFn } = options;
|
|
3113
|
+
if (requestInit || fetchFn) {
|
|
3114
|
+
this.transport = { requestInit, fetchFn };
|
|
3115
|
+
}
|
|
2724
3116
|
}
|
|
2725
3117
|
options;
|
|
2726
3118
|
registry;
|
|
@@ -2736,11 +3128,27 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2736
3128
|
lastProgressTime = 0;
|
|
2737
3129
|
lastProgressPosition = -1;
|
|
2738
3130
|
errorListener = null;
|
|
3131
|
+
// Bound so we can removeEventListener in destroy(); without this the
|
|
3132
|
+
// listener outlives the player and accumulates on elements that swap
|
|
3133
|
+
// source (e.g. <avbridge-video>).
|
|
3134
|
+
endedListener = null;
|
|
3135
|
+
// Background tab handling. userIntent is what the user last asked for
|
|
3136
|
+
// (play vs pause) — used to decide whether to auto-resume on visibility
|
|
3137
|
+
// return. autoPausedForVisibility tracks whether we paused because the
|
|
3138
|
+
// tab was hidden, so we don't resume playback the user deliberately
|
|
3139
|
+
// paused (e.g. via media keys while hidden).
|
|
3140
|
+
userIntent = "pause";
|
|
3141
|
+
autoPausedForVisibility = false;
|
|
3142
|
+
visibilityListener = null;
|
|
2739
3143
|
// Serializes escalation / setStrategy calls
|
|
2740
3144
|
switchingPromise = Promise.resolve();
|
|
2741
3145
|
// Owns blob URLs created during sidecar discovery + SRT->VTT conversion.
|
|
2742
3146
|
// Revoked at destroy() so repeated source swaps don't leak.
|
|
2743
3147
|
subtitleResources = new SubtitleResourceBag();
|
|
3148
|
+
// Transport config extracted from CreatePlayerOptions. Threaded to probe,
|
|
3149
|
+
// subtitle fetches, and strategy session creators. Not stored on MediaContext
|
|
3150
|
+
// because it's runtime config, not media analysis.
|
|
3151
|
+
transport;
|
|
2744
3152
|
static async create(options) {
|
|
2745
3153
|
const registry = new PluginRegistry();
|
|
2746
3154
|
registerBuiltins(registry);
|
|
@@ -2764,7 +3172,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2764
3172
|
const bootstrapStart = performance.now();
|
|
2765
3173
|
try {
|
|
2766
3174
|
dbg.info("bootstrap", "start");
|
|
2767
|
-
const ctx = await dbg.timed("probe", "probe", 3e3, () => probe(this.options.source));
|
|
3175
|
+
const ctx = await dbg.timed("probe", "probe", 3e3, () => probe(this.options.source, this.transport));
|
|
2768
3176
|
dbg.info(
|
|
2769
3177
|
"probe",
|
|
2770
3178
|
`container=${ctx.container} video=${ctx.videoTracks[0]?.codec ?? "-"} audio=${ctx.audioTracks[0]?.codec ?? "-"} probedBy=${ctx.probedBy}`
|
|
@@ -2812,7 +3220,8 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2812
3220
|
this.subtitleResources,
|
|
2813
3221
|
(err, track) => {
|
|
2814
3222
|
console.warn(`[avbridge] subtitle ${track.id} failed: ${err.message}`);
|
|
2815
|
-
}
|
|
3223
|
+
},
|
|
3224
|
+
this.transport
|
|
2816
3225
|
);
|
|
2817
3226
|
}
|
|
2818
3227
|
this.emitter.emitSticky("tracks", {
|
|
@@ -2821,7 +3230,12 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2821
3230
|
subtitle: ctx.subtitleTracks
|
|
2822
3231
|
});
|
|
2823
3232
|
this.startTimeupdateLoop();
|
|
2824
|
-
this.
|
|
3233
|
+
this.endedListener = () => this.emitter.emit("ended", void 0);
|
|
3234
|
+
this.options.target.addEventListener("ended", this.endedListener);
|
|
3235
|
+
if (this.options.backgroundBehavior !== "continue" && typeof document !== "undefined") {
|
|
3236
|
+
this.visibilityListener = () => this.onVisibilityChange();
|
|
3237
|
+
document.addEventListener("visibilitychange", this.visibilityListener);
|
|
3238
|
+
}
|
|
2825
3239
|
this.emitter.emitSticky("ready", void 0);
|
|
2826
3240
|
const bootstrapElapsed = performance.now() - bootstrapStart;
|
|
2827
3241
|
dbg.info("bootstrap", `ready in ${bootstrapElapsed.toFixed(0)}ms`);
|
|
@@ -2848,7 +3262,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2848
3262
|
throw new Error(`no plugin available for strategy "${strategy}"`);
|
|
2849
3263
|
}
|
|
2850
3264
|
try {
|
|
2851
|
-
this.session = await plugin.execute(this.mediaContext, this.options.target);
|
|
3265
|
+
this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
|
|
2852
3266
|
} catch (err) {
|
|
2853
3267
|
const chain = this.classification?.fallbackChain;
|
|
2854
3268
|
if (chain && chain.length > 0) {
|
|
@@ -2921,7 +3335,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2921
3335
|
continue;
|
|
2922
3336
|
}
|
|
2923
3337
|
try {
|
|
2924
|
-
this.session = await plugin.execute(this.mediaContext, this.options.target);
|
|
3338
|
+
this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
|
|
2925
3339
|
} catch (err) {
|
|
2926
3340
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2927
3341
|
errors.push(`${nextStrategy}: ${msg}`);
|
|
@@ -2944,8 +3358,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2944
3358
|
}
|
|
2945
3359
|
return;
|
|
2946
3360
|
}
|
|
2947
|
-
this.emitter.emit("error", new
|
|
2948
|
-
|
|
3361
|
+
this.emitter.emit("error", new AvbridgeError(
|
|
3362
|
+
ERR_ALL_STRATEGIES_EXHAUSTED,
|
|
3363
|
+
`All playback strategies failed: ${errors.join("; ")}`,
|
|
3364
|
+
"This file may require a codec or container that isn't available in this browser. Try the fallback strategy or check browser codec support."
|
|
2949
3365
|
));
|
|
2950
3366
|
}
|
|
2951
3367
|
// ── Stall supervision ─────────────────────────────────────────────────
|
|
@@ -2997,7 +3413,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2997
3413
|
// ── Public: manual strategy switch ────────────────────────────────────
|
|
2998
3414
|
/** Manually switch to a different playback strategy. Preserves current position and play/pause state. Concurrent calls are serialized. */
|
|
2999
3415
|
async setStrategy(strategy, reason) {
|
|
3000
|
-
if (!this.mediaContext) throw new
|
|
3416
|
+
if (!this.mediaContext) throw new AvbridgeError(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.");
|
|
3001
3417
|
if (this.session?.strategy === strategy) return;
|
|
3002
3418
|
this.switchingPromise = this.switchingPromise.then(
|
|
3003
3419
|
() => this.doSetStrategy(strategy, reason)
|
|
@@ -3026,7 +3442,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3026
3442
|
}
|
|
3027
3443
|
const plugin = this.registry.findFor(this.mediaContext, strategy);
|
|
3028
3444
|
if (!plugin) throw new Error(`no plugin available for strategy "${strategy}"`);
|
|
3029
|
-
this.session = await plugin.execute(this.mediaContext, this.options.target);
|
|
3445
|
+
this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
|
|
3030
3446
|
this.emitter.emitSticky("strategy", {
|
|
3031
3447
|
strategy,
|
|
3032
3448
|
reason: switchReason
|
|
@@ -3060,26 +3476,56 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3060
3476
|
}
|
|
3061
3477
|
/** Begin or resume playback. Throws if the player is not ready. */
|
|
3062
3478
|
async play() {
|
|
3063
|
-
if (!this.session) throw new
|
|
3479
|
+
if (!this.session) throw new AvbridgeError(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.");
|
|
3480
|
+
this.userIntent = "play";
|
|
3481
|
+
this.autoPausedForVisibility = false;
|
|
3064
3482
|
await this.session.play();
|
|
3065
3483
|
}
|
|
3066
3484
|
/** Pause playback. No-op if the player is not ready or already paused. */
|
|
3067
3485
|
pause() {
|
|
3486
|
+
this.userIntent = "pause";
|
|
3487
|
+
this.autoPausedForVisibility = false;
|
|
3068
3488
|
this.session?.pause();
|
|
3069
3489
|
}
|
|
3490
|
+
/**
|
|
3491
|
+
* Handle browser tab visibility changes. On hide: pause if the user
|
|
3492
|
+
* had been playing. On show: resume if we were the one who paused.
|
|
3493
|
+
* Skips when `backgroundBehavior: "continue"` is set (listener isn't
|
|
3494
|
+
* installed in that case).
|
|
3495
|
+
*/
|
|
3496
|
+
onVisibilityChange() {
|
|
3497
|
+
if (!this.session) return;
|
|
3498
|
+
const action = decideVisibilityAction({
|
|
3499
|
+
hidden: document.hidden,
|
|
3500
|
+
userIntent: this.userIntent,
|
|
3501
|
+
sessionIsPlaying: !this.options.target.paused,
|
|
3502
|
+
autoPausedForVisibility: this.autoPausedForVisibility
|
|
3503
|
+
});
|
|
3504
|
+
if (action === "pause") {
|
|
3505
|
+
this.autoPausedForVisibility = true;
|
|
3506
|
+
dbg.info("visibility", "tab hidden \u2014 auto-paused");
|
|
3507
|
+
this.session.pause();
|
|
3508
|
+
} else if (action === "resume") {
|
|
3509
|
+
this.autoPausedForVisibility = false;
|
|
3510
|
+
dbg.info("visibility", "tab visible \u2014 auto-resuming");
|
|
3511
|
+
void this.session.play().catch((err) => {
|
|
3512
|
+
console.warn("[avbridge] auto-resume after tab return failed:", err);
|
|
3513
|
+
});
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3070
3516
|
/** Seek to the given time in seconds. Throws if the player is not ready. */
|
|
3071
3517
|
async seek(time) {
|
|
3072
|
-
if (!this.session) throw new
|
|
3518
|
+
if (!this.session) throw new AvbridgeError(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.");
|
|
3073
3519
|
await this.session.seek(time);
|
|
3074
3520
|
}
|
|
3075
3521
|
/** Switch the active audio track by track ID. Throws if the player is not ready. */
|
|
3076
3522
|
async setAudioTrack(id) {
|
|
3077
|
-
if (!this.session) throw new
|
|
3523
|
+
if (!this.session) throw new AvbridgeError(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.");
|
|
3078
3524
|
await this.session.setAudioTrack(id);
|
|
3079
3525
|
}
|
|
3080
3526
|
/** Switch the active subtitle track by track ID, or pass `null` to disable subtitles. */
|
|
3081
3527
|
async setSubtitleTrack(id) {
|
|
3082
|
-
if (!this.session) throw new
|
|
3528
|
+
if (!this.session) throw new AvbridgeError(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.");
|
|
3083
3529
|
await this.session.setSubtitleTrack(id);
|
|
3084
3530
|
}
|
|
3085
3531
|
/** Return a snapshot of current diagnostics: container, codecs, strategy, runtime stats, and strategy history. */
|
|
@@ -3107,6 +3553,14 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3107
3553
|
this.timeupdateInterval = null;
|
|
3108
3554
|
}
|
|
3109
3555
|
this.clearSupervisor();
|
|
3556
|
+
if (this.endedListener) {
|
|
3557
|
+
this.options.target.removeEventListener("ended", this.endedListener);
|
|
3558
|
+
this.endedListener = null;
|
|
3559
|
+
}
|
|
3560
|
+
if (this.visibilityListener) {
|
|
3561
|
+
document.removeEventListener("visibilitychange", this.visibilityListener);
|
|
3562
|
+
this.visibilityListener = null;
|
|
3563
|
+
}
|
|
3110
3564
|
if (this.session) {
|
|
3111
3565
|
await this.session.destroy();
|
|
3112
3566
|
this.session = null;
|
|
@@ -3118,14 +3572,24 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3118
3572
|
async function createPlayer(options) {
|
|
3119
3573
|
return UnifiedPlayer.create(options);
|
|
3120
3574
|
}
|
|
3575
|
+
function decideVisibilityAction(state) {
|
|
3576
|
+
if (state.hidden) {
|
|
3577
|
+
if (state.userIntent === "play" && state.sessionIsPlaying) return "pause";
|
|
3578
|
+
return "noop";
|
|
3579
|
+
}
|
|
3580
|
+
if (state.autoPausedForVisibility) return "resume";
|
|
3581
|
+
return "noop";
|
|
3582
|
+
}
|
|
3121
3583
|
function buildInitialDecision(initial, ctx) {
|
|
3122
3584
|
const natural = classifyContext(ctx);
|
|
3123
3585
|
const cls = strategyToClass(initial, natural);
|
|
3586
|
+
const inherited = natural.fallbackChain ?? defaultFallbackChain(initial);
|
|
3587
|
+
const fallbackChain = inherited.filter((s) => s !== initial);
|
|
3124
3588
|
return {
|
|
3125
3589
|
class: cls,
|
|
3126
3590
|
strategy: initial,
|
|
3127
3591
|
reason: `initial strategy "${initial}" requested via options.initialStrategy`,
|
|
3128
|
-
fallbackChain
|
|
3592
|
+
fallbackChain
|
|
3129
3593
|
};
|
|
3130
3594
|
}
|
|
3131
3595
|
function strategyToClass(strategy, natural) {
|
|
@@ -3154,6 +3618,6 @@ function defaultFallbackChain(strategy) {
|
|
|
3154
3618
|
}
|
|
3155
3619
|
}
|
|
3156
3620
|
|
|
3157
|
-
export { UnifiedPlayer, avbridgeAudioToMediabunny, avbridgeVideoToMediabunny, buildMediabunnySourceFromInput, classifyContext, createPlayer, probe, srtToVtt };
|
|
3158
|
-
//# sourceMappingURL=chunk-
|
|
3159
|
-
//# sourceMappingURL=chunk-
|
|
3621
|
+
export { FALLBACK_AUDIO_CODECS, FALLBACK_VIDEO_CODECS, NATIVE_AUDIO_CODECS, NATIVE_VIDEO_CODECS, UnifiedPlayer, avbridgeAudioToMediabunny, avbridgeVideoToMediabunny, buildMediabunnySourceFromInput, classifyContext, createPlayer, probe, srtToVtt };
|
|
3622
|
+
//# sourceMappingURL=chunk-NV7ILLWH.js.map
|
|
3623
|
+
//# sourceMappingURL=chunk-NV7ILLWH.js.map
|