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,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunk6UUT4BEA_cjs = require('./chunk-6UUT4BEA.cjs');
|
|
4
4
|
var chunkG4APZMCP_cjs = require('./chunk-G4APZMCP.cjs');
|
|
5
|
-
var
|
|
5
|
+
var chunkF3LQJKXK_cjs = require('./chunk-F3LQJKXK.cjs');
|
|
6
6
|
|
|
7
7
|
// src/probe/mediabunny.ts
|
|
8
8
|
async function probeWithMediabunny(source, sniffedContainer) {
|
|
@@ -180,37 +180,58 @@ var MEDIABUNNY_CONTAINERS = /* @__PURE__ */ new Set([
|
|
|
180
180
|
"adts",
|
|
181
181
|
"mpegts"
|
|
182
182
|
]);
|
|
183
|
-
async function probe(source) {
|
|
184
|
-
const normalized = await
|
|
185
|
-
const sniffed = await
|
|
183
|
+
async function probe(source, transport) {
|
|
184
|
+
const normalized = await chunk6UUT4BEA_cjs.normalizeSource(source, transport);
|
|
185
|
+
const sniffed = await chunk6UUT4BEA_cjs.sniffNormalizedSource(normalized);
|
|
186
186
|
if (MEDIABUNNY_CONTAINERS.has(sniffed)) {
|
|
187
187
|
try {
|
|
188
|
-
|
|
188
|
+
const result = await probeWithMediabunny(normalized, sniffed);
|
|
189
|
+
const hasUnknownCodec = result.videoTracks.some((t) => t.codec === "unknown") || result.audioTracks.some((t) => t.codec === "unknown");
|
|
190
|
+
if (hasUnknownCodec) {
|
|
191
|
+
try {
|
|
192
|
+
const { probeWithLibav } = await import('./avi-W6L3BTWU.cjs');
|
|
193
|
+
return await probeWithLibav(normalized, sniffed);
|
|
194
|
+
} catch {
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return result;
|
|
189
199
|
} catch (mediabunnyErr) {
|
|
190
200
|
console.warn(
|
|
191
201
|
`[avbridge] mediabunny rejected ${sniffed} file, falling back to libav:`,
|
|
192
202
|
mediabunnyErr.message
|
|
193
203
|
);
|
|
194
204
|
try {
|
|
195
|
-
const { probeWithLibav } = await import('./avi-
|
|
205
|
+
const { probeWithLibav } = await import('./avi-W6L3BTWU.cjs');
|
|
196
206
|
return await probeWithLibav(normalized, sniffed);
|
|
197
207
|
} catch (libavErr) {
|
|
198
208
|
const mbMsg = mediabunnyErr.message || String(mediabunnyErr);
|
|
199
209
|
const lvMsg = libavErr instanceof Error ? libavErr.message : String(libavErr);
|
|
200
|
-
throw new
|
|
201
|
-
|
|
210
|
+
throw new chunk6UUT4BEA_cjs.AvbridgeError(
|
|
211
|
+
chunk6UUT4BEA_cjs.ERR_PROBE_FAILED,
|
|
212
|
+
`Failed to probe ${sniffed.toUpperCase()} file. mediabunny: ${mbMsg}. libav: ${lvMsg}.`,
|
|
213
|
+
"The file may be corrupt, truncated, or in an unsupported format. Enable AVBRIDGE_DEBUG for detailed logs."
|
|
202
214
|
);
|
|
203
215
|
}
|
|
204
216
|
}
|
|
205
217
|
}
|
|
206
218
|
try {
|
|
207
|
-
const { probeWithLibav } = await import('./avi-
|
|
219
|
+
const { probeWithLibav } = await import('./avi-W6L3BTWU.cjs');
|
|
208
220
|
return await probeWithLibav(normalized, sniffed);
|
|
209
221
|
} catch (err) {
|
|
210
222
|
const inner = err instanceof Error ? err.message : String(err);
|
|
211
223
|
console.error("[avbridge] libav probe failed for", sniffed, "file:", err);
|
|
212
|
-
|
|
213
|
-
|
|
224
|
+
if (sniffed === "unknown") {
|
|
225
|
+
throw new chunk6UUT4BEA_cjs.AvbridgeError(
|
|
226
|
+
chunk6UUT4BEA_cjs.ERR_PROBE_UNKNOWN_CONTAINER,
|
|
227
|
+
`Unable to probe source: container format could not be identified. libav fallback: ${inner || "(no details)"}`,
|
|
228
|
+
"The file may be corrupt or in a format avbridge doesn't recognize. Check the file plays in VLC or ffprobe."
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
throw new chunk6UUT4BEA_cjs.AvbridgeError(
|
|
232
|
+
chunk6UUT4BEA_cjs.ERR_LIBAV_NOT_REACHABLE,
|
|
233
|
+
`${sniffed.toUpperCase()} files require libav.js, which failed to load: ${inner || "(no details)"}`,
|
|
234
|
+
"Install @libav.js/variant-webcodecs, or check that AVBRIDGE_LIBAV_BASE points to the correct path."
|
|
214
235
|
);
|
|
215
236
|
}
|
|
216
237
|
}
|
|
@@ -311,7 +332,9 @@ var FALLBACK_AUDIO_CODECS = /* @__PURE__ */ new Set([
|
|
|
311
332
|
"ra_144",
|
|
312
333
|
"ra_288",
|
|
313
334
|
"sipr",
|
|
314
|
-
"atrac3"
|
|
335
|
+
"atrac3",
|
|
336
|
+
"dts",
|
|
337
|
+
"truehd"
|
|
315
338
|
]);
|
|
316
339
|
var NATIVE_CONTAINERS = /* @__PURE__ */ new Set([
|
|
317
340
|
"mp4",
|
|
@@ -373,7 +396,16 @@ function classifyContext(ctx) {
|
|
|
373
396
|
reason: `video codec "${video.codec}" has no browser decoder; WASM fallback required`
|
|
374
397
|
};
|
|
375
398
|
}
|
|
376
|
-
|
|
399
|
+
const audioNeedsFallback = audio && (FALLBACK_AUDIO_CODECS.has(audio.codec) || !NATIVE_AUDIO_CODECS.has(audio.codec));
|
|
400
|
+
if (audioNeedsFallback) {
|
|
401
|
+
if (NATIVE_VIDEO_CODECS.has(video.codec) && webCodecsAvailable()) {
|
|
402
|
+
return {
|
|
403
|
+
class: "HYBRID_CANDIDATE",
|
|
404
|
+
strategy: "hybrid",
|
|
405
|
+
reason: `video "${video.codec}" is hardware-decodable via WebCodecs; audio "${audio.codec}" decoded in software by libav`,
|
|
406
|
+
fallbackChain: ["fallback"]
|
|
407
|
+
};
|
|
408
|
+
}
|
|
377
409
|
return {
|
|
378
410
|
class: "FALLBACK_REQUIRED",
|
|
379
411
|
strategy: "fallback",
|
|
@@ -740,10 +772,18 @@ var MseSink = class {
|
|
|
740
772
|
constructor(options) {
|
|
741
773
|
this.options = options;
|
|
742
774
|
if (typeof MediaSource === "undefined") {
|
|
743
|
-
throw new
|
|
775
|
+
throw new chunk6UUT4BEA_cjs.AvbridgeError(
|
|
776
|
+
chunk6UUT4BEA_cjs.ERR_MSE_NOT_SUPPORTED,
|
|
777
|
+
"MediaSource Extensions (MSE) are not supported in this environment.",
|
|
778
|
+
"MSE is required for the remux strategy. Use a browser that supports MSE, or try the fallback strategy."
|
|
779
|
+
);
|
|
744
780
|
}
|
|
745
781
|
if (!MediaSource.isTypeSupported(options.mime)) {
|
|
746
|
-
throw new
|
|
782
|
+
throw new chunk6UUT4BEA_cjs.AvbridgeError(
|
|
783
|
+
chunk6UUT4BEA_cjs.ERR_MSE_CODEC_NOT_SUPPORTED,
|
|
784
|
+
`This browser's MSE does not support "${options.mime}".`,
|
|
785
|
+
"The codec combination can't be played via remux in this browser. The player will try the next strategy automatically."
|
|
786
|
+
);
|
|
747
787
|
}
|
|
748
788
|
this.mediaSource = new MediaSource();
|
|
749
789
|
this.objectUrl = URL.createObjectURL(this.mediaSource);
|
|
@@ -1072,6 +1112,10 @@ async function createRemuxPipeline(ctx, video) {
|
|
|
1072
1112
|
console.error("[avbridge] remux pipeline reseek failed:", err);
|
|
1073
1113
|
});
|
|
1074
1114
|
},
|
|
1115
|
+
setAutoPlay(autoPlay) {
|
|
1116
|
+
pendingAutoPlay = autoPlay;
|
|
1117
|
+
if (sink) sink.setPlayOnSeek(autoPlay);
|
|
1118
|
+
},
|
|
1075
1119
|
async destroy() {
|
|
1076
1120
|
destroyed = true;
|
|
1077
1121
|
pumpToken++;
|
|
@@ -1112,7 +1156,11 @@ async function createRemuxSession(context, video) {
|
|
|
1112
1156
|
await pipeline.start(video.currentTime || 0, true);
|
|
1113
1157
|
return;
|
|
1114
1158
|
}
|
|
1115
|
-
|
|
1159
|
+
pipeline.setAutoPlay(true);
|
|
1160
|
+
try {
|
|
1161
|
+
await video.play();
|
|
1162
|
+
} catch {
|
|
1163
|
+
}
|
|
1116
1164
|
},
|
|
1117
1165
|
pause() {
|
|
1118
1166
|
wantPlay = false;
|
|
@@ -1151,6 +1199,10 @@ async function createRemuxSession(context, video) {
|
|
|
1151
1199
|
}
|
|
1152
1200
|
|
|
1153
1201
|
// src/strategies/fallback/video-renderer.ts
|
|
1202
|
+
function isDebug() {
|
|
1203
|
+
return typeof globalThis !== "undefined" && !!globalThis.AVBRIDGE_DEBUG;
|
|
1204
|
+
}
|
|
1205
|
+
var lastDebugLog = 0;
|
|
1154
1206
|
var VideoRenderer = class {
|
|
1155
1207
|
constructor(target, clock, fps = 30) {
|
|
1156
1208
|
this.target = target;
|
|
@@ -1160,7 +1212,7 @@ var VideoRenderer = class {
|
|
|
1160
1212
|
this.resolveFirstFrame = resolve;
|
|
1161
1213
|
});
|
|
1162
1214
|
this.canvas = document.createElement("canvas");
|
|
1163
|
-
this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;";
|
|
1215
|
+
this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;object-fit:contain;";
|
|
1164
1216
|
const parent = target.parentElement ?? target.parentNode;
|
|
1165
1217
|
if (parent && parent instanceof HTMLElement) {
|
|
1166
1218
|
if (getComputedStyle(parent).position === "static") {
|
|
@@ -1197,6 +1249,20 @@ var VideoRenderer = class {
|
|
|
1197
1249
|
lastPaintWall = 0;
|
|
1198
1250
|
/** Minimum ms between paints — paces video at roughly source fps. */
|
|
1199
1251
|
paintIntervalMs;
|
|
1252
|
+
/** Cumulative count of frames skipped because all PTS are in the future. */
|
|
1253
|
+
ticksWaiting = 0;
|
|
1254
|
+
/** Cumulative count of ticks where PTS mode painted a frame. */
|
|
1255
|
+
ticksPainted = 0;
|
|
1256
|
+
/**
|
|
1257
|
+
* Calibration offset (microseconds) between video PTS and audio clock.
|
|
1258
|
+
* Video PTS and AudioContext.currentTime can drift ~0.1% relative to
|
|
1259
|
+
* each other (different clock domains). Over 45 minutes that's 2.6s.
|
|
1260
|
+
* We measure the offset on the first painted frame and update it
|
|
1261
|
+
* periodically so the PTS comparison stays calibrated.
|
|
1262
|
+
*/
|
|
1263
|
+
ptsCalibrationUs = 0;
|
|
1264
|
+
ptsCalibrated = false;
|
|
1265
|
+
lastCalibrationWall = 0;
|
|
1200
1266
|
/** Resolves once the first decoded frame has been enqueued. */
|
|
1201
1267
|
firstFrameReady;
|
|
1202
1268
|
resolveFirstFrame;
|
|
@@ -1245,21 +1311,81 @@ var VideoRenderer = class {
|
|
|
1245
1311
|
}
|
|
1246
1312
|
return;
|
|
1247
1313
|
}
|
|
1248
|
-
const
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
if (
|
|
1252
|
-
const
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
this.
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1314
|
+
const rawAudioNowUs = this.clock.now() * 1e6;
|
|
1315
|
+
const headTs = this.queue[0].timestamp ?? 0;
|
|
1316
|
+
const hasPts = headTs > 0 || this.queue.length > 1;
|
|
1317
|
+
if (hasPts) {
|
|
1318
|
+
const wallNow2 = performance.now();
|
|
1319
|
+
if (!this.ptsCalibrated || wallNow2 - this.lastCalibrationWall > 1e4) {
|
|
1320
|
+
this.ptsCalibrationUs = headTs - rawAudioNowUs;
|
|
1321
|
+
this.ptsCalibrated = true;
|
|
1322
|
+
this.lastCalibrationWall = wallNow2;
|
|
1323
|
+
}
|
|
1324
|
+
const audioNowUs = rawAudioNowUs + this.ptsCalibrationUs;
|
|
1325
|
+
const frameDurationUs = this.paintIntervalMs * 1e3;
|
|
1326
|
+
const deadlineUs = audioNowUs + frameDurationUs;
|
|
1327
|
+
let bestIdx = -1;
|
|
1328
|
+
for (let i = 0; i < this.queue.length; i++) {
|
|
1329
|
+
const ts = this.queue[i].timestamp ?? 0;
|
|
1330
|
+
if (ts <= deadlineUs) {
|
|
1331
|
+
bestIdx = i;
|
|
1332
|
+
} else {
|
|
1333
|
+
break;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
if (bestIdx < 0) {
|
|
1337
|
+
this.ticksWaiting++;
|
|
1338
|
+
if (isDebug()) {
|
|
1339
|
+
const now = performance.now();
|
|
1340
|
+
if (now - lastDebugLog > 1e3) {
|
|
1341
|
+
const headPtsMs = (headTs / 1e3).toFixed(1);
|
|
1342
|
+
const audioMs = (audioNowUs / 1e3).toFixed(1);
|
|
1343
|
+
const rawDriftMs = ((headTs - rawAudioNowUs) / 1e3).toFixed(1);
|
|
1344
|
+
const calibMs = (this.ptsCalibrationUs / 1e3).toFixed(1);
|
|
1345
|
+
console.log(
|
|
1346
|
+
`[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}`
|
|
1347
|
+
);
|
|
1348
|
+
lastDebugLog = now;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1260
1351
|
return;
|
|
1261
1352
|
}
|
|
1353
|
+
const dropThresholdUs = audioNowUs - frameDurationUs * 2;
|
|
1354
|
+
let dropped = 0;
|
|
1355
|
+
while (bestIdx > 0) {
|
|
1356
|
+
const ts = this.queue[0].timestamp ?? 0;
|
|
1357
|
+
if (ts < dropThresholdUs) {
|
|
1358
|
+
this.queue.shift()?.close();
|
|
1359
|
+
this.framesDroppedLate++;
|
|
1360
|
+
bestIdx--;
|
|
1361
|
+
dropped++;
|
|
1362
|
+
} else {
|
|
1363
|
+
break;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
this.ticksPainted++;
|
|
1367
|
+
if (isDebug()) {
|
|
1368
|
+
const now = performance.now();
|
|
1369
|
+
if (now - lastDebugLog > 1e3) {
|
|
1370
|
+
const paintedTs = this.queue[0]?.timestamp ?? 0;
|
|
1371
|
+
const audioMs = (audioNowUs / 1e3).toFixed(1);
|
|
1372
|
+
const ptsMs = (paintedTs / 1e3).toFixed(1);
|
|
1373
|
+
const rawDriftMs = ((paintedTs - rawAudioNowUs) / 1e3).toFixed(1);
|
|
1374
|
+
const calibMs = (this.ptsCalibrationUs / 1e3).toFixed(1);
|
|
1375
|
+
console.log(
|
|
1376
|
+
`[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}`
|
|
1377
|
+
);
|
|
1378
|
+
lastDebugLog = now;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
const frame2 = this.queue.shift();
|
|
1382
|
+
this.paint(frame2);
|
|
1383
|
+
frame2.close();
|
|
1384
|
+
this.lastPaintWall = performance.now();
|
|
1385
|
+
return;
|
|
1262
1386
|
}
|
|
1387
|
+
const wallNow = performance.now();
|
|
1388
|
+
if (wallNow - this.lastPaintWall < this.paintIntervalMs - 2) return;
|
|
1263
1389
|
const frame = this.queue.shift();
|
|
1264
1390
|
this.paint(frame);
|
|
1265
1391
|
frame.close();
|
|
@@ -1281,8 +1407,13 @@ var VideoRenderer = class {
|
|
|
1281
1407
|
}
|
|
1282
1408
|
/** Discard all queued frames. Used by seek to drop stale buffers. */
|
|
1283
1409
|
flush() {
|
|
1410
|
+
const count = this.queue.length;
|
|
1284
1411
|
while (this.queue.length > 0) this.queue.shift()?.close();
|
|
1285
1412
|
this.prerolled = false;
|
|
1413
|
+
this.ptsCalibrated = false;
|
|
1414
|
+
if (isDebug() && count > 0) {
|
|
1415
|
+
console.log(`[avbridge:renderer] FLUSH discarded=${count} painted=${this.framesPainted} drops=${this.framesDroppedLate}`);
|
|
1416
|
+
}
|
|
1286
1417
|
}
|
|
1287
1418
|
stats() {
|
|
1288
1419
|
return {
|
|
@@ -1327,11 +1458,38 @@ var AudioOutput = class {
|
|
|
1327
1458
|
pendingQueue = [];
|
|
1328
1459
|
framesScheduled = 0;
|
|
1329
1460
|
destroyed = false;
|
|
1461
|
+
/** User-set volume (0..1). Applied to the gain node. */
|
|
1462
|
+
_volume = 1;
|
|
1463
|
+
/** User-set muted flag. When true, gain is forced to 0. */
|
|
1464
|
+
_muted = false;
|
|
1330
1465
|
constructor() {
|
|
1331
1466
|
this.ctx = new AudioContext();
|
|
1332
1467
|
this.gain = this.ctx.createGain();
|
|
1333
1468
|
this.gain.connect(this.ctx.destination);
|
|
1334
1469
|
}
|
|
1470
|
+
/** Set volume (0..1). Applied immediately to the gain node. */
|
|
1471
|
+
setVolume(v) {
|
|
1472
|
+
this._volume = Math.max(0, Math.min(1, v));
|
|
1473
|
+
this.applyGain();
|
|
1474
|
+
}
|
|
1475
|
+
getVolume() {
|
|
1476
|
+
return this._volume;
|
|
1477
|
+
}
|
|
1478
|
+
/** Set muted. When true, output is silenced regardless of volume. */
|
|
1479
|
+
setMuted(m) {
|
|
1480
|
+
this._muted = m;
|
|
1481
|
+
this.applyGain();
|
|
1482
|
+
}
|
|
1483
|
+
getMuted() {
|
|
1484
|
+
return this._muted;
|
|
1485
|
+
}
|
|
1486
|
+
applyGain() {
|
|
1487
|
+
const target = this._muted ? 0 : this._volume;
|
|
1488
|
+
try {
|
|
1489
|
+
this.gain.gain.value = target;
|
|
1490
|
+
} catch {
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1335
1493
|
/**
|
|
1336
1494
|
* Switch into wall-clock fallback mode. Called by the decoder when no
|
|
1337
1495
|
* audio decoder could be initialized for the source. Once set, this
|
|
@@ -1402,9 +1560,13 @@ var AudioOutput = class {
|
|
|
1402
1560
|
const node = this.ctx.createBufferSource();
|
|
1403
1561
|
node.buffer = buffer;
|
|
1404
1562
|
node.connect(this.gain);
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1563
|
+
let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor);
|
|
1564
|
+
if (ctxStart < this.ctx.currentTime) {
|
|
1565
|
+
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
1566
|
+
this.mediaTimeOfAnchor = this.mediaTimeOfNext;
|
|
1567
|
+
ctxStart = this.ctx.currentTime;
|
|
1568
|
+
}
|
|
1569
|
+
node.start(ctxStart);
|
|
1408
1570
|
this.mediaTimeOfNext += frameCount / sampleRate;
|
|
1409
1571
|
this.framesScheduled++;
|
|
1410
1572
|
}
|
|
@@ -1477,6 +1639,7 @@ var AudioOutput = class {
|
|
|
1477
1639
|
}
|
|
1478
1640
|
this.gain = this.ctx.createGain();
|
|
1479
1641
|
this.gain.connect(this.ctx.destination);
|
|
1642
|
+
this.applyGain();
|
|
1480
1643
|
this.pendingQueue = [];
|
|
1481
1644
|
this.mediaTimeOfAnchor = newMediaTime;
|
|
1482
1645
|
this.mediaTimeOfNext = newMediaTime;
|
|
@@ -1505,11 +1668,11 @@ var AudioOutput = class {
|
|
|
1505
1668
|
|
|
1506
1669
|
// src/strategies/hybrid/decoder.ts
|
|
1507
1670
|
async function startHybridDecoder(opts) {
|
|
1508
|
-
const variant =
|
|
1671
|
+
const variant = chunkF3LQJKXK_cjs.pickLibavVariant(opts.context);
|
|
1509
1672
|
const libav = await chunkG4APZMCP_cjs.loadLibav(variant);
|
|
1510
1673
|
const bridge = await loadBridge();
|
|
1511
|
-
const { prepareLibavInput } = await import('./libav-http-reader-
|
|
1512
|
-
const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source);
|
|
1674
|
+
const { prepareLibavInput } = await import('./libav-http-reader-AZLE7YFS.cjs');
|
|
1675
|
+
const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source, opts.transport);
|
|
1513
1676
|
const readPkt = await libav.av_packet_alloc();
|
|
1514
1677
|
const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
|
|
1515
1678
|
const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
|
|
@@ -1580,6 +1743,56 @@ async function startHybridDecoder(opts) {
|
|
|
1580
1743
|
});
|
|
1581
1744
|
throw new Error("hybrid decoder: could not initialize any decoders");
|
|
1582
1745
|
}
|
|
1746
|
+
let bsfCtx = null;
|
|
1747
|
+
let bsfPkt = null;
|
|
1748
|
+
if (videoStream && opts.context.videoTracks[0]?.codec === "mpeg4") {
|
|
1749
|
+
try {
|
|
1750
|
+
bsfCtx = await libav.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
|
|
1751
|
+
if (bsfCtx != null && bsfCtx >= 0) {
|
|
1752
|
+
const parIn = await libav.AVBSFContext_par_in(bsfCtx);
|
|
1753
|
+
await libav.avcodec_parameters_copy(parIn, videoStream.codecpar);
|
|
1754
|
+
await libav.av_bsf_init(bsfCtx);
|
|
1755
|
+
bsfPkt = await libav.av_packet_alloc();
|
|
1756
|
+
chunkG4APZMCP_cjs.dbg.info("bsf", "mpeg4_unpack_bframes BSF active (hybrid)");
|
|
1757
|
+
} else {
|
|
1758
|
+
console.warn("[avbridge] mpeg4_unpack_bframes BSF not available in hybrid decoder");
|
|
1759
|
+
bsfCtx = null;
|
|
1760
|
+
}
|
|
1761
|
+
} catch (err) {
|
|
1762
|
+
console.warn("[avbridge] hybrid: failed to init BSF:", err.message);
|
|
1763
|
+
bsfCtx = null;
|
|
1764
|
+
bsfPkt = null;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
async function applyBSF(packets) {
|
|
1768
|
+
if (!bsfCtx || !bsfPkt) return packets;
|
|
1769
|
+
const out = [];
|
|
1770
|
+
for (const pkt of packets) {
|
|
1771
|
+
await libav.ff_copyin_packet(bsfPkt, pkt);
|
|
1772
|
+
const sendErr = await libav.av_bsf_send_packet(bsfCtx, bsfPkt);
|
|
1773
|
+
if (sendErr < 0) {
|
|
1774
|
+
out.push(pkt);
|
|
1775
|
+
continue;
|
|
1776
|
+
}
|
|
1777
|
+
while (true) {
|
|
1778
|
+
const recvErr = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
|
|
1779
|
+
if (recvErr < 0) break;
|
|
1780
|
+
out.push(await libav.ff_copyout_packet(bsfPkt));
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
return out;
|
|
1784
|
+
}
|
|
1785
|
+
async function flushBSF() {
|
|
1786
|
+
if (!bsfCtx || !bsfPkt) return;
|
|
1787
|
+
try {
|
|
1788
|
+
await libav.av_bsf_send_packet(bsfCtx, 0);
|
|
1789
|
+
while (true) {
|
|
1790
|
+
const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
|
|
1791
|
+
if (err < 0) break;
|
|
1792
|
+
}
|
|
1793
|
+
} catch {
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1583
1796
|
let destroyed = false;
|
|
1584
1797
|
let pumpToken = 0;
|
|
1585
1798
|
let pumpRunning = null;
|
|
@@ -1607,8 +1820,15 @@ async function startHybridDecoder(opts) {
|
|
|
1607
1820
|
if (myToken !== pumpToken || destroyed) return;
|
|
1608
1821
|
const videoPackets = videoStream ? packets[videoStream.index] : void 0;
|
|
1609
1822
|
const audioPackets = audioStream ? packets[audioStream.index] : void 0;
|
|
1823
|
+
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
1824
|
+
await decodeAudioBatch(audioPackets, myToken);
|
|
1825
|
+
}
|
|
1826
|
+
if (myToken !== pumpToken || destroyed) return;
|
|
1827
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
1828
|
+
if (myToken !== pumpToken || destroyed) return;
|
|
1610
1829
|
if (videoDecoder && videoPackets && videoPackets.length > 0) {
|
|
1611
|
-
|
|
1830
|
+
const processed = await applyBSF(videoPackets);
|
|
1831
|
+
for (const pkt of processed) {
|
|
1612
1832
|
if (myToken !== pumpToken || destroyed) return;
|
|
1613
1833
|
sanitizePacketTimestamp(pkt, () => {
|
|
1614
1834
|
const ts = syntheticVideoUs;
|
|
@@ -1628,9 +1848,6 @@ async function startHybridDecoder(opts) {
|
|
|
1628
1848
|
}
|
|
1629
1849
|
}
|
|
1630
1850
|
}
|
|
1631
|
-
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
1632
|
-
await decodeAudioBatch(audioPackets, myToken);
|
|
1633
|
-
}
|
|
1634
1851
|
packetsRead += (videoPackets?.length ?? 0) + (audioPackets?.length ?? 0);
|
|
1635
1852
|
while (!destroyed && myToken === pumpToken && (videoDecoder && videoDecoder.decodeQueueSize > 10 || opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
|
|
1636
1853
|
await new Promise((r) => setTimeout(r, 50));
|
|
@@ -1653,20 +1870,43 @@ async function startHybridDecoder(opts) {
|
|
|
1653
1870
|
}
|
|
1654
1871
|
async function decodeAudioBatch(pkts, myToken, flush = false) {
|
|
1655
1872
|
if (!audioDec || destroyed || myToken !== pumpToken) return;
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1873
|
+
const AUDIO_SUB_BATCH = 4;
|
|
1874
|
+
let allFrames = [];
|
|
1875
|
+
for (let i = 0; i < pkts.length; i += AUDIO_SUB_BATCH) {
|
|
1876
|
+
if (myToken !== pumpToken || destroyed) return;
|
|
1877
|
+
const slice = pkts.slice(i, i + AUDIO_SUB_BATCH);
|
|
1878
|
+
const isLast = i + AUDIO_SUB_BATCH >= pkts.length;
|
|
1879
|
+
try {
|
|
1880
|
+
const frames2 = await libav.ff_decode_multi(
|
|
1881
|
+
audioDec.c,
|
|
1882
|
+
audioDec.pkt,
|
|
1883
|
+
audioDec.frame,
|
|
1884
|
+
slice,
|
|
1885
|
+
isLast && flush ? { fin: true, ignoreErrors: true } : { ignoreErrors: true }
|
|
1886
|
+
);
|
|
1887
|
+
allFrames = allFrames.concat(frames2);
|
|
1888
|
+
} catch (err) {
|
|
1889
|
+
console.error("[avbridge] hybrid audio decode failed:", err);
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1892
|
+
if (!isLast) await new Promise((r) => setTimeout(r, 0));
|
|
1893
|
+
}
|
|
1894
|
+
if (pkts.length === 0 && flush) {
|
|
1895
|
+
try {
|
|
1896
|
+
allFrames = await libav.ff_decode_multi(
|
|
1897
|
+
audioDec.c,
|
|
1898
|
+
audioDec.pkt,
|
|
1899
|
+
audioDec.frame,
|
|
1900
|
+
[],
|
|
1901
|
+
{ fin: true, ignoreErrors: true }
|
|
1902
|
+
);
|
|
1903
|
+
} catch (err) {
|
|
1904
|
+
console.error("[avbridge] hybrid audio flush failed:", err);
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1668
1907
|
}
|
|
1669
1908
|
if (myToken !== pumpToken || destroyed) return;
|
|
1909
|
+
const frames = allFrames;
|
|
1670
1910
|
for (const f of frames) {
|
|
1671
1911
|
if (myToken !== pumpToken || destroyed) return;
|
|
1672
1912
|
sanitizeFrameTimestamp(
|
|
@@ -1703,6 +1943,14 @@ async function startHybridDecoder(opts) {
|
|
|
1703
1943
|
await pumpRunning;
|
|
1704
1944
|
} catch {
|
|
1705
1945
|
}
|
|
1946
|
+
try {
|
|
1947
|
+
if (bsfCtx) await libav.av_bsf_free(bsfCtx);
|
|
1948
|
+
} catch {
|
|
1949
|
+
}
|
|
1950
|
+
try {
|
|
1951
|
+
if (bsfPkt) await libav.av_packet_free?.(bsfPkt);
|
|
1952
|
+
} catch {
|
|
1953
|
+
}
|
|
1706
1954
|
try {
|
|
1707
1955
|
if (videoDecoder && videoDecoder.state !== "closed") videoDecoder.close();
|
|
1708
1956
|
} catch {
|
|
@@ -1756,6 +2004,7 @@ async function startHybridDecoder(opts) {
|
|
|
1756
2004
|
if (audioDec) await libav.avcodec_flush_buffers?.(audioDec.c);
|
|
1757
2005
|
} catch {
|
|
1758
2006
|
}
|
|
2007
|
+
await flushBSF();
|
|
1759
2008
|
syntheticVideoUs = Math.round(timeSec * 1e6);
|
|
1760
2009
|
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
1761
2010
|
pumpRunning = pumpLoop(newToken).catch(
|
|
@@ -1769,6 +2018,7 @@ async function startHybridDecoder(opts) {
|
|
|
1769
2018
|
videoFramesDecoded,
|
|
1770
2019
|
videoChunksFed,
|
|
1771
2020
|
audioFramesDecoded,
|
|
2021
|
+
bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
|
|
1772
2022
|
videoDecodeQueueSize: videoDecoder?.decodeQueueSize ?? 0,
|
|
1773
2023
|
// Confirmed transport info — see fallback decoder for the pattern.
|
|
1774
2024
|
_transport: inputHandle.transport === "http-range" ? "http-range" : "memory",
|
|
@@ -1945,8 +2195,8 @@ async function loadBridge() {
|
|
|
1945
2195
|
// src/strategies/hybrid/index.ts
|
|
1946
2196
|
var READY_AUDIO_BUFFER_SECONDS = 0.3;
|
|
1947
2197
|
var READY_TIMEOUT_SECONDS = 10;
|
|
1948
|
-
async function createHybridSession(ctx, target) {
|
|
1949
|
-
const { normalizeSource: normalizeSource2 } = await import('./source-
|
|
2198
|
+
async function createHybridSession(ctx, target, transport) {
|
|
2199
|
+
const { normalizeSource: normalizeSource2 } = await import('./source-73CAH6HW.cjs');
|
|
1950
2200
|
const source = await normalizeSource2(ctx.source);
|
|
1951
2201
|
const fps = ctx.videoTracks[0]?.fps ?? 30;
|
|
1952
2202
|
const audio = new AudioOutput();
|
|
@@ -1958,7 +2208,8 @@ async function createHybridSession(ctx, target) {
|
|
|
1958
2208
|
filename: ctx.name ?? "input.bin",
|
|
1959
2209
|
context: ctx,
|
|
1960
2210
|
renderer,
|
|
1961
|
-
audio
|
|
2211
|
+
audio,
|
|
2212
|
+
transport
|
|
1962
2213
|
});
|
|
1963
2214
|
} catch (err) {
|
|
1964
2215
|
audio.destroy();
|
|
@@ -1972,6 +2223,26 @@ async function createHybridSession(ctx, target) {
|
|
|
1972
2223
|
void doSeek(v);
|
|
1973
2224
|
}
|
|
1974
2225
|
});
|
|
2226
|
+
Object.defineProperty(target, "paused", {
|
|
2227
|
+
configurable: true,
|
|
2228
|
+
get: () => !audio.isPlaying()
|
|
2229
|
+
});
|
|
2230
|
+
Object.defineProperty(target, "volume", {
|
|
2231
|
+
configurable: true,
|
|
2232
|
+
get: () => audio.getVolume(),
|
|
2233
|
+
set: (v) => {
|
|
2234
|
+
audio.setVolume(v);
|
|
2235
|
+
target.dispatchEvent(new Event("volumechange"));
|
|
2236
|
+
}
|
|
2237
|
+
});
|
|
2238
|
+
Object.defineProperty(target, "muted", {
|
|
2239
|
+
configurable: true,
|
|
2240
|
+
get: () => audio.getMuted(),
|
|
2241
|
+
set: (m) => {
|
|
2242
|
+
audio.setMuted(m);
|
|
2243
|
+
target.dispatchEvent(new Event("volumechange"));
|
|
2244
|
+
}
|
|
2245
|
+
});
|
|
1975
2246
|
if (ctx.duration && Number.isFinite(ctx.duration)) {
|
|
1976
2247
|
Object.defineProperty(target, "duration", {
|
|
1977
2248
|
configurable: true,
|
|
@@ -2011,10 +2282,13 @@ async function createHybridSession(ctx, target) {
|
|
|
2011
2282
|
if (!audio.isPlaying()) {
|
|
2012
2283
|
await waitForBuffer();
|
|
2013
2284
|
await audio.start();
|
|
2285
|
+
target.dispatchEvent(new Event("play"));
|
|
2286
|
+
target.dispatchEvent(new Event("playing"));
|
|
2014
2287
|
}
|
|
2015
2288
|
},
|
|
2016
2289
|
pause() {
|
|
2017
2290
|
void audio.pause();
|
|
2291
|
+
target.dispatchEvent(new Event("pause"));
|
|
2018
2292
|
},
|
|
2019
2293
|
async seek(time) {
|
|
2020
2294
|
await doSeek(time);
|
|
@@ -2036,6 +2310,9 @@ async function createHybridSession(ctx, target) {
|
|
|
2036
2310
|
try {
|
|
2037
2311
|
delete target.currentTime;
|
|
2038
2312
|
delete target.duration;
|
|
2313
|
+
delete target.paused;
|
|
2314
|
+
delete target.volume;
|
|
2315
|
+
delete target.muted;
|
|
2039
2316
|
} catch {
|
|
2040
2317
|
}
|
|
2041
2318
|
},
|
|
@@ -2047,11 +2324,11 @@ async function createHybridSession(ctx, target) {
|
|
|
2047
2324
|
|
|
2048
2325
|
// src/strategies/fallback/decoder.ts
|
|
2049
2326
|
async function startDecoder(opts) {
|
|
2050
|
-
const variant =
|
|
2327
|
+
const variant = chunkF3LQJKXK_cjs.pickLibavVariant(opts.context);
|
|
2051
2328
|
const libav = await chunkG4APZMCP_cjs.loadLibav(variant);
|
|
2052
2329
|
const bridge = await loadBridge2();
|
|
2053
|
-
const { prepareLibavInput } = await import('./libav-http-reader-
|
|
2054
|
-
const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source);
|
|
2330
|
+
const { prepareLibavInput } = await import('./libav-http-reader-AZLE7YFS.cjs');
|
|
2331
|
+
const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source, opts.transport);
|
|
2055
2332
|
const readPkt = await libav.av_packet_alloc();
|
|
2056
2333
|
const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
|
|
2057
2334
|
const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
|
|
@@ -2107,6 +2384,56 @@ async function startDecoder(opts) {
|
|
|
2107
2384
|
`fallback decoder: could not initialize any libav decoders (${codecs}).${hint}`
|
|
2108
2385
|
);
|
|
2109
2386
|
}
|
|
2387
|
+
let bsfCtx = null;
|
|
2388
|
+
let bsfPkt = null;
|
|
2389
|
+
if (videoStream && opts.context.videoTracks[0]?.codec === "mpeg4") {
|
|
2390
|
+
try {
|
|
2391
|
+
bsfCtx = await libav.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
|
|
2392
|
+
if (bsfCtx != null && bsfCtx >= 0) {
|
|
2393
|
+
const parIn = await libav.AVBSFContext_par_in(bsfCtx);
|
|
2394
|
+
await libav.avcodec_parameters_copy(parIn, videoStream.codecpar);
|
|
2395
|
+
await libav.av_bsf_init(bsfCtx);
|
|
2396
|
+
bsfPkt = await libav.av_packet_alloc();
|
|
2397
|
+
chunkG4APZMCP_cjs.dbg.info("bsf", "mpeg4_unpack_bframes BSF active");
|
|
2398
|
+
} else {
|
|
2399
|
+
console.warn("[avbridge] mpeg4_unpack_bframes BSF not available \u2014 decoding without it");
|
|
2400
|
+
bsfCtx = null;
|
|
2401
|
+
}
|
|
2402
|
+
} catch (err) {
|
|
2403
|
+
console.warn("[avbridge] failed to init mpeg4_unpack_bframes BSF:", err.message);
|
|
2404
|
+
bsfCtx = null;
|
|
2405
|
+
bsfPkt = null;
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
async function applyBSF(packets) {
|
|
2409
|
+
if (!bsfCtx || !bsfPkt) return packets;
|
|
2410
|
+
const out = [];
|
|
2411
|
+
for (const pkt of packets) {
|
|
2412
|
+
await libav.ff_copyin_packet(bsfPkt, pkt);
|
|
2413
|
+
const sendErr = await libav.av_bsf_send_packet(bsfCtx, bsfPkt);
|
|
2414
|
+
if (sendErr < 0) {
|
|
2415
|
+
out.push(pkt);
|
|
2416
|
+
continue;
|
|
2417
|
+
}
|
|
2418
|
+
while (true) {
|
|
2419
|
+
const recvErr = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
|
|
2420
|
+
if (recvErr < 0) break;
|
|
2421
|
+
out.push(await libav.ff_copyout_packet(bsfPkt));
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
return out;
|
|
2425
|
+
}
|
|
2426
|
+
async function flushBSF() {
|
|
2427
|
+
if (!bsfCtx || !bsfPkt) return;
|
|
2428
|
+
try {
|
|
2429
|
+
await libav.av_bsf_send_packet(bsfCtx, 0);
|
|
2430
|
+
while (true) {
|
|
2431
|
+
const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
|
|
2432
|
+
if (err < 0) break;
|
|
2433
|
+
}
|
|
2434
|
+
} catch {
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2110
2437
|
let destroyed = false;
|
|
2111
2438
|
let pumpToken = 0;
|
|
2112
2439
|
let pumpRunning = null;
|
|
@@ -2115,7 +2442,8 @@ async function startDecoder(opts) {
|
|
|
2115
2442
|
let audioFramesDecoded = 0;
|
|
2116
2443
|
let watchdogFirstFrameMs = 0;
|
|
2117
2444
|
let watchdogSlowSinceMs = 0;
|
|
2118
|
-
let
|
|
2445
|
+
let watchdogSlowWarned = false;
|
|
2446
|
+
let watchdogOverflowWarned = false;
|
|
2119
2447
|
let syntheticVideoUs = 0;
|
|
2120
2448
|
let syntheticAudioUs = 0;
|
|
2121
2449
|
const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
|
|
@@ -2127,7 +2455,7 @@ async function startDecoder(opts) {
|
|
|
2127
2455
|
let packets;
|
|
2128
2456
|
try {
|
|
2129
2457
|
[readErr, packets] = await libav.ff_read_frame_multi(fmt_ctx, readPkt, {
|
|
2130
|
-
limit:
|
|
2458
|
+
limit: 16 * 1024
|
|
2131
2459
|
});
|
|
2132
2460
|
} catch (err) {
|
|
2133
2461
|
console.error("[avbridge] ff_read_frame_multi failed:", err);
|
|
@@ -2136,26 +2464,27 @@ async function startDecoder(opts) {
|
|
|
2136
2464
|
if (myToken !== pumpToken || destroyed) return;
|
|
2137
2465
|
const videoPackets = videoStream ? packets[videoStream.index] : void 0;
|
|
2138
2466
|
const audioPackets = audioStream ? packets[audioStream.index] : void 0;
|
|
2139
|
-
if (videoDec && videoPackets && videoPackets.length > 0) {
|
|
2140
|
-
await decodeVideoBatch(videoPackets, myToken);
|
|
2141
|
-
}
|
|
2142
|
-
if (myToken !== pumpToken || destroyed) return;
|
|
2143
2467
|
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
2144
2468
|
await decodeAudioBatch(audioPackets, myToken);
|
|
2145
2469
|
}
|
|
2470
|
+
if (myToken !== pumpToken || destroyed) return;
|
|
2471
|
+
if (videoDec && videoPackets && videoPackets.length > 0) {
|
|
2472
|
+
const processed = await applyBSF(videoPackets);
|
|
2473
|
+
await decodeVideoBatch(processed, myToken);
|
|
2474
|
+
}
|
|
2146
2475
|
packetsRead += (videoPackets?.length ?? 0) + (audioPackets?.length ?? 0);
|
|
2147
2476
|
if (videoFramesDecoded > 0) {
|
|
2148
2477
|
if (watchdogFirstFrameMs === 0) {
|
|
2149
2478
|
watchdogFirstFrameMs = performance.now();
|
|
2150
2479
|
}
|
|
2151
2480
|
const elapsedSinceFirst = (performance.now() - watchdogFirstFrameMs) / 1e3;
|
|
2152
|
-
if (elapsedSinceFirst > 1 && !
|
|
2481
|
+
if (elapsedSinceFirst > 1 && !watchdogSlowWarned) {
|
|
2153
2482
|
const expectedFrames = elapsedSinceFirst * videoFps;
|
|
2154
2483
|
const ratio = videoFramesDecoded / expectedFrames;
|
|
2155
2484
|
if (ratio < 0.6) {
|
|
2156
2485
|
if (watchdogSlowSinceMs === 0) watchdogSlowSinceMs = performance.now();
|
|
2157
2486
|
if ((performance.now() - watchdogSlowSinceMs) / 1e3 > 5) {
|
|
2158
|
-
|
|
2487
|
+
watchdogSlowWarned = true;
|
|
2159
2488
|
console.warn(
|
|
2160
2489
|
"[avbridge:decode-rate]",
|
|
2161
2490
|
`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.`
|
|
@@ -2165,6 +2494,17 @@ async function startDecoder(opts) {
|
|
|
2165
2494
|
watchdogSlowSinceMs = 0;
|
|
2166
2495
|
}
|
|
2167
2496
|
}
|
|
2497
|
+
if (!watchdogOverflowWarned && videoFramesDecoded > 100) {
|
|
2498
|
+
const rendererStats = opts.renderer.stats();
|
|
2499
|
+
const overflow = rendererStats.framesDroppedOverflow ?? 0;
|
|
2500
|
+
if (overflow / videoFramesDecoded > 0.1) {
|
|
2501
|
+
watchdogOverflowWarned = true;
|
|
2502
|
+
console.warn(
|
|
2503
|
+
"[avbridge:overflow-drop]",
|
|
2504
|
+
`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.`
|
|
2505
|
+
);
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2168
2508
|
}
|
|
2169
2509
|
while (!destroyed && myToken === pumpToken && (opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
|
|
2170
2510
|
await new Promise((r) => setTimeout(r, 50));
|
|
@@ -2276,6 +2616,14 @@ async function startDecoder(opts) {
|
|
|
2276
2616
|
await pumpRunning;
|
|
2277
2617
|
} catch {
|
|
2278
2618
|
}
|
|
2619
|
+
try {
|
|
2620
|
+
if (bsfCtx) await libav.av_bsf_free(bsfCtx);
|
|
2621
|
+
} catch {
|
|
2622
|
+
}
|
|
2623
|
+
try {
|
|
2624
|
+
if (bsfPkt) await libav.av_packet_free?.(bsfPkt);
|
|
2625
|
+
} catch {
|
|
2626
|
+
}
|
|
2279
2627
|
try {
|
|
2280
2628
|
if (videoDec) await libav.ff_free_decoder?.(videoDec.c, videoDec.pkt, videoDec.frame);
|
|
2281
2629
|
} catch {
|
|
@@ -2327,6 +2675,7 @@ async function startDecoder(opts) {
|
|
|
2327
2675
|
if (audioDec) await libav.avcodec_flush_buffers?.(audioDec.c);
|
|
2328
2676
|
} catch {
|
|
2329
2677
|
}
|
|
2678
|
+
await flushBSF();
|
|
2330
2679
|
syntheticVideoUs = Math.round(timeSec * 1e6);
|
|
2331
2680
|
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
2332
2681
|
pumpRunning = pumpLoop(newToken).catch(
|
|
@@ -2339,6 +2688,7 @@ async function startDecoder(opts) {
|
|
|
2339
2688
|
packetsRead,
|
|
2340
2689
|
videoFramesDecoded,
|
|
2341
2690
|
audioFramesDecoded,
|
|
2691
|
+
bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
|
|
2342
2692
|
// Confirmed transport info: once prepareLibavInput returns
|
|
2343
2693
|
// successfully, we *know* whether the source is http-range (probe
|
|
2344
2694
|
// succeeded and returned 206) or in-memory blob. Diagnostics hoists
|
|
@@ -2494,8 +2844,8 @@ async function loadBridge2() {
|
|
|
2494
2844
|
// src/strategies/fallback/index.ts
|
|
2495
2845
|
var READY_AUDIO_BUFFER_SECONDS2 = 0.04;
|
|
2496
2846
|
var READY_TIMEOUT_SECONDS2 = 3;
|
|
2497
|
-
async function createFallbackSession(ctx, target) {
|
|
2498
|
-
const { normalizeSource: normalizeSource2 } = await import('./source-
|
|
2847
|
+
async function createFallbackSession(ctx, target, transport) {
|
|
2848
|
+
const { normalizeSource: normalizeSource2 } = await import('./source-73CAH6HW.cjs');
|
|
2499
2849
|
const source = await normalizeSource2(ctx.source);
|
|
2500
2850
|
const fps = ctx.videoTracks[0]?.fps ?? 30;
|
|
2501
2851
|
const audio = new AudioOutput();
|
|
@@ -2507,7 +2857,8 @@ async function createFallbackSession(ctx, target) {
|
|
|
2507
2857
|
filename: ctx.name ?? "input.bin",
|
|
2508
2858
|
context: ctx,
|
|
2509
2859
|
renderer,
|
|
2510
|
-
audio
|
|
2860
|
+
audio,
|
|
2861
|
+
transport
|
|
2511
2862
|
});
|
|
2512
2863
|
} catch (err) {
|
|
2513
2864
|
audio.destroy();
|
|
@@ -2521,6 +2872,26 @@ async function createFallbackSession(ctx, target) {
|
|
|
2521
2872
|
void doSeek(v);
|
|
2522
2873
|
}
|
|
2523
2874
|
});
|
|
2875
|
+
Object.defineProperty(target, "paused", {
|
|
2876
|
+
configurable: true,
|
|
2877
|
+
get: () => !audio.isPlaying()
|
|
2878
|
+
});
|
|
2879
|
+
Object.defineProperty(target, "volume", {
|
|
2880
|
+
configurable: true,
|
|
2881
|
+
get: () => audio.getVolume(),
|
|
2882
|
+
set: (v) => {
|
|
2883
|
+
audio.setVolume(v);
|
|
2884
|
+
target.dispatchEvent(new Event("volumechange"));
|
|
2885
|
+
}
|
|
2886
|
+
});
|
|
2887
|
+
Object.defineProperty(target, "muted", {
|
|
2888
|
+
configurable: true,
|
|
2889
|
+
get: () => audio.getMuted(),
|
|
2890
|
+
set: (m) => {
|
|
2891
|
+
audio.setMuted(m);
|
|
2892
|
+
target.dispatchEvent(new Event("volumechange"));
|
|
2893
|
+
}
|
|
2894
|
+
});
|
|
2524
2895
|
if (ctx.duration && Number.isFinite(ctx.duration)) {
|
|
2525
2896
|
Object.defineProperty(target, "duration", {
|
|
2526
2897
|
configurable: true,
|
|
@@ -2529,25 +2900,35 @@ async function createFallbackSession(ctx, target) {
|
|
|
2529
2900
|
}
|
|
2530
2901
|
async function waitForBuffer() {
|
|
2531
2902
|
const start = performance.now();
|
|
2903
|
+
let firstFrameAtMs = 0;
|
|
2532
2904
|
chunkG4APZMCP_cjs.dbg.info(
|
|
2533
2905
|
"cold-start",
|
|
2534
|
-
`gate entry:
|
|
2906
|
+
`gate entry: want audio \u2265 ${READY_AUDIO_BUFFER_SECONDS2 * 1e3}ms + 1 frame`
|
|
2535
2907
|
);
|
|
2536
2908
|
while (true) {
|
|
2537
2909
|
const audioAhead = audio.isNoAudio() ? Infinity : audio.bufferAhead();
|
|
2538
2910
|
const audioReady = audio.isNoAudio() || audioAhead >= READY_AUDIO_BUFFER_SECONDS2;
|
|
2539
2911
|
const hasFrames = renderer.hasFrames();
|
|
2912
|
+
const nowMs = performance.now();
|
|
2913
|
+
if (hasFrames && firstFrameAtMs === 0) firstFrameAtMs = nowMs;
|
|
2540
2914
|
if (audioReady && hasFrames) {
|
|
2541
2915
|
chunkG4APZMCP_cjs.dbg.info(
|
|
2542
2916
|
"cold-start",
|
|
2543
|
-
`gate satisfied in ${(
|
|
2917
|
+
`gate satisfied in ${(nowMs - start).toFixed(0)}ms (audio=${(audioAhead * 1e3).toFixed(0)}ms, frames=${renderer.queueDepth()})`
|
|
2918
|
+
);
|
|
2919
|
+
return;
|
|
2920
|
+
}
|
|
2921
|
+
if (hasFrames && firstFrameAtMs > 0 && nowMs - firstFrameAtMs >= 500) {
|
|
2922
|
+
chunkG4APZMCP_cjs.dbg.info(
|
|
2923
|
+
"cold-start",
|
|
2924
|
+
`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)`
|
|
2544
2925
|
);
|
|
2545
2926
|
return;
|
|
2546
2927
|
}
|
|
2547
|
-
if ((
|
|
2928
|
+
if ((nowMs - start) / 1e3 > READY_TIMEOUT_SECONDS2) {
|
|
2548
2929
|
chunkG4APZMCP_cjs.dbg.diag(
|
|
2549
2930
|
"cold-start",
|
|
2550
|
-
`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).
|
|
2931
|
+
`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.`
|
|
2551
2932
|
);
|
|
2552
2933
|
return;
|
|
2553
2934
|
}
|
|
@@ -2574,10 +2955,13 @@ async function createFallbackSession(ctx, target) {
|
|
|
2574
2955
|
if (!audio.isPlaying()) {
|
|
2575
2956
|
await waitForBuffer();
|
|
2576
2957
|
await audio.start();
|
|
2958
|
+
target.dispatchEvent(new Event("play"));
|
|
2959
|
+
target.dispatchEvent(new Event("playing"));
|
|
2577
2960
|
}
|
|
2578
2961
|
},
|
|
2579
2962
|
pause() {
|
|
2580
2963
|
void audio.pause();
|
|
2964
|
+
target.dispatchEvent(new Event("pause"));
|
|
2581
2965
|
},
|
|
2582
2966
|
async seek(time) {
|
|
2583
2967
|
await doSeek(time);
|
|
@@ -2596,6 +2980,9 @@ async function createFallbackSession(ctx, target) {
|
|
|
2596
2980
|
try {
|
|
2597
2981
|
delete target.currentTime;
|
|
2598
2982
|
delete target.duration;
|
|
2983
|
+
delete target.paused;
|
|
2984
|
+
delete target.volume;
|
|
2985
|
+
delete target.muted;
|
|
2599
2986
|
} catch {
|
|
2600
2987
|
}
|
|
2601
2988
|
},
|
|
@@ -2619,12 +3006,12 @@ var remuxPlugin = {
|
|
|
2619
3006
|
var hybridPlugin = {
|
|
2620
3007
|
name: "hybrid",
|
|
2621
3008
|
canHandle: () => typeof VideoDecoder !== "undefined",
|
|
2622
|
-
execute: (ctx, video) => createHybridSession(ctx, video)
|
|
3009
|
+
execute: (ctx, video, transport) => createHybridSession(ctx, video, transport)
|
|
2623
3010
|
};
|
|
2624
3011
|
var fallbackPlugin = {
|
|
2625
3012
|
name: "fallback",
|
|
2626
3013
|
canHandle: () => true,
|
|
2627
|
-
execute: (ctx, video) => createFallbackSession(ctx, video)
|
|
3014
|
+
execute: (ctx, video, transport) => createFallbackSession(ctx, video, transport)
|
|
2628
3015
|
};
|
|
2629
3016
|
function registerBuiltins(registry) {
|
|
2630
3017
|
registry.register(nativePlugin);
|
|
@@ -2680,7 +3067,8 @@ var SubtitleResourceBag = class {
|
|
|
2680
3067
|
this.urls.clear();
|
|
2681
3068
|
}
|
|
2682
3069
|
};
|
|
2683
|
-
async function attachSubtitleTracks(video, tracks, bag, onError) {
|
|
3070
|
+
async function attachSubtitleTracks(video, tracks, bag, onError, transport) {
|
|
3071
|
+
const doFetch = chunk6UUT4BEA_cjs.fetchWith(transport);
|
|
2684
3072
|
for (const t of Array.from(video.querySelectorAll("track[data-avbridge]"))) {
|
|
2685
3073
|
t.remove();
|
|
2686
3074
|
}
|
|
@@ -2689,13 +3077,13 @@ async function attachSubtitleTracks(video, tracks, bag, onError) {
|
|
|
2689
3077
|
try {
|
|
2690
3078
|
let url = t.sidecarUrl;
|
|
2691
3079
|
if (t.format === "srt") {
|
|
2692
|
-
const res = await
|
|
3080
|
+
const res = await doFetch(t.sidecarUrl, transport?.requestInit);
|
|
2693
3081
|
const text = await res.text();
|
|
2694
3082
|
const vtt = srtToVtt(text);
|
|
2695
3083
|
const blob = new Blob([vtt], { type: "text/vtt" });
|
|
2696
3084
|
url = bag ? bag.createObjectURL(blob) : URL.createObjectURL(blob);
|
|
2697
3085
|
} else if (t.format === "vtt") {
|
|
2698
|
-
const res = await
|
|
3086
|
+
const res = await doFetch(t.sidecarUrl, transport?.requestInit);
|
|
2699
3087
|
const text = await res.text();
|
|
2700
3088
|
if (!isVtt(text)) {
|
|
2701
3089
|
console.warn("[avbridge] subtitle missing WEBVTT header:", t.sidecarUrl);
|
|
@@ -2723,6 +3111,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2723
3111
|
constructor(options, registry) {
|
|
2724
3112
|
this.options = options;
|
|
2725
3113
|
this.registry = registry;
|
|
3114
|
+
const { requestInit, fetchFn } = options;
|
|
3115
|
+
if (requestInit || fetchFn) {
|
|
3116
|
+
this.transport = { requestInit, fetchFn };
|
|
3117
|
+
}
|
|
2726
3118
|
}
|
|
2727
3119
|
options;
|
|
2728
3120
|
registry;
|
|
@@ -2738,11 +3130,27 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2738
3130
|
lastProgressTime = 0;
|
|
2739
3131
|
lastProgressPosition = -1;
|
|
2740
3132
|
errorListener = null;
|
|
3133
|
+
// Bound so we can removeEventListener in destroy(); without this the
|
|
3134
|
+
// listener outlives the player and accumulates on elements that swap
|
|
3135
|
+
// source (e.g. <avbridge-video>).
|
|
3136
|
+
endedListener = null;
|
|
3137
|
+
// Background tab handling. userIntent is what the user last asked for
|
|
3138
|
+
// (play vs pause) — used to decide whether to auto-resume on visibility
|
|
3139
|
+
// return. autoPausedForVisibility tracks whether we paused because the
|
|
3140
|
+
// tab was hidden, so we don't resume playback the user deliberately
|
|
3141
|
+
// paused (e.g. via media keys while hidden).
|
|
3142
|
+
userIntent = "pause";
|
|
3143
|
+
autoPausedForVisibility = false;
|
|
3144
|
+
visibilityListener = null;
|
|
2741
3145
|
// Serializes escalation / setStrategy calls
|
|
2742
3146
|
switchingPromise = Promise.resolve();
|
|
2743
3147
|
// Owns blob URLs created during sidecar discovery + SRT->VTT conversion.
|
|
2744
3148
|
// Revoked at destroy() so repeated source swaps don't leak.
|
|
2745
3149
|
subtitleResources = new SubtitleResourceBag();
|
|
3150
|
+
// Transport config extracted from CreatePlayerOptions. Threaded to probe,
|
|
3151
|
+
// subtitle fetches, and strategy session creators. Not stored on MediaContext
|
|
3152
|
+
// because it's runtime config, not media analysis.
|
|
3153
|
+
transport;
|
|
2746
3154
|
static async create(options) {
|
|
2747
3155
|
const registry = new PluginRegistry();
|
|
2748
3156
|
registerBuiltins(registry);
|
|
@@ -2766,7 +3174,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2766
3174
|
const bootstrapStart = performance.now();
|
|
2767
3175
|
try {
|
|
2768
3176
|
chunkG4APZMCP_cjs.dbg.info("bootstrap", "start");
|
|
2769
|
-
const ctx = await chunkG4APZMCP_cjs.dbg.timed("probe", "probe", 3e3, () => probe(this.options.source));
|
|
3177
|
+
const ctx = await chunkG4APZMCP_cjs.dbg.timed("probe", "probe", 3e3, () => probe(this.options.source, this.transport));
|
|
2770
3178
|
chunkG4APZMCP_cjs.dbg.info(
|
|
2771
3179
|
"probe",
|
|
2772
3180
|
`container=${ctx.container} video=${ctx.videoTracks[0]?.codec ?? "-"} audio=${ctx.audioTracks[0]?.codec ?? "-"} probedBy=${ctx.probedBy}`
|
|
@@ -2814,7 +3222,8 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2814
3222
|
this.subtitleResources,
|
|
2815
3223
|
(err, track) => {
|
|
2816
3224
|
console.warn(`[avbridge] subtitle ${track.id} failed: ${err.message}`);
|
|
2817
|
-
}
|
|
3225
|
+
},
|
|
3226
|
+
this.transport
|
|
2818
3227
|
);
|
|
2819
3228
|
}
|
|
2820
3229
|
this.emitter.emitSticky("tracks", {
|
|
@@ -2823,7 +3232,12 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2823
3232
|
subtitle: ctx.subtitleTracks
|
|
2824
3233
|
});
|
|
2825
3234
|
this.startTimeupdateLoop();
|
|
2826
|
-
this.
|
|
3235
|
+
this.endedListener = () => this.emitter.emit("ended", void 0);
|
|
3236
|
+
this.options.target.addEventListener("ended", this.endedListener);
|
|
3237
|
+
if (this.options.backgroundBehavior !== "continue" && typeof document !== "undefined") {
|
|
3238
|
+
this.visibilityListener = () => this.onVisibilityChange();
|
|
3239
|
+
document.addEventListener("visibilitychange", this.visibilityListener);
|
|
3240
|
+
}
|
|
2827
3241
|
this.emitter.emitSticky("ready", void 0);
|
|
2828
3242
|
const bootstrapElapsed = performance.now() - bootstrapStart;
|
|
2829
3243
|
chunkG4APZMCP_cjs.dbg.info("bootstrap", `ready in ${bootstrapElapsed.toFixed(0)}ms`);
|
|
@@ -2850,7 +3264,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2850
3264
|
throw new Error(`no plugin available for strategy "${strategy}"`);
|
|
2851
3265
|
}
|
|
2852
3266
|
try {
|
|
2853
|
-
this.session = await plugin.execute(this.mediaContext, this.options.target);
|
|
3267
|
+
this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
|
|
2854
3268
|
} catch (err) {
|
|
2855
3269
|
const chain = this.classification?.fallbackChain;
|
|
2856
3270
|
if (chain && chain.length > 0) {
|
|
@@ -2923,7 +3337,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2923
3337
|
continue;
|
|
2924
3338
|
}
|
|
2925
3339
|
try {
|
|
2926
|
-
this.session = await plugin.execute(this.mediaContext, this.options.target);
|
|
3340
|
+
this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
|
|
2927
3341
|
} catch (err) {
|
|
2928
3342
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2929
3343
|
errors.push(`${nextStrategy}: ${msg}`);
|
|
@@ -2946,8 +3360,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2946
3360
|
}
|
|
2947
3361
|
return;
|
|
2948
3362
|
}
|
|
2949
|
-
this.emitter.emit("error", new
|
|
2950
|
-
|
|
3363
|
+
this.emitter.emit("error", new chunk6UUT4BEA_cjs.AvbridgeError(
|
|
3364
|
+
chunk6UUT4BEA_cjs.ERR_ALL_STRATEGIES_EXHAUSTED,
|
|
3365
|
+
`All playback strategies failed: ${errors.join("; ")}`,
|
|
3366
|
+
"This file may require a codec or container that isn't available in this browser. Try the fallback strategy or check browser codec support."
|
|
2951
3367
|
));
|
|
2952
3368
|
}
|
|
2953
3369
|
// ── Stall supervision ─────────────────────────────────────────────────
|
|
@@ -2999,7 +3415,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2999
3415
|
// ── Public: manual strategy switch ────────────────────────────────────
|
|
3000
3416
|
/** Manually switch to a different playback strategy. Preserves current position and play/pause state. Concurrent calls are serialized. */
|
|
3001
3417
|
async setStrategy(strategy, reason) {
|
|
3002
|
-
if (!this.mediaContext) throw new
|
|
3418
|
+
if (!this.mediaContext) throw new chunk6UUT4BEA_cjs.AvbridgeError(chunk6UUT4BEA_cjs.ERR_PLAYER_NOT_READY, "Player not ready \u2014 wait for the 'ready' event before calling playback methods.", "Await the 'ready' event or check player.readyState before calling play/pause/seek.");
|
|
3003
3419
|
if (this.session?.strategy === strategy) return;
|
|
3004
3420
|
this.switchingPromise = this.switchingPromise.then(
|
|
3005
3421
|
() => this.doSetStrategy(strategy, reason)
|
|
@@ -3028,7 +3444,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3028
3444
|
}
|
|
3029
3445
|
const plugin = this.registry.findFor(this.mediaContext, strategy);
|
|
3030
3446
|
if (!plugin) throw new Error(`no plugin available for strategy "${strategy}"`);
|
|
3031
|
-
this.session = await plugin.execute(this.mediaContext, this.options.target);
|
|
3447
|
+
this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
|
|
3032
3448
|
this.emitter.emitSticky("strategy", {
|
|
3033
3449
|
strategy,
|
|
3034
3450
|
reason: switchReason
|
|
@@ -3062,26 +3478,56 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3062
3478
|
}
|
|
3063
3479
|
/** Begin or resume playback. Throws if the player is not ready. */
|
|
3064
3480
|
async play() {
|
|
3065
|
-
if (!this.session) throw new
|
|
3481
|
+
if (!this.session) throw new chunk6UUT4BEA_cjs.AvbridgeError(chunk6UUT4BEA_cjs.ERR_PLAYER_NOT_READY, "Player not ready \u2014 wait for the 'ready' event before calling playback methods.", "Await the 'ready' event or check player.readyState before calling play/pause/seek.");
|
|
3482
|
+
this.userIntent = "play";
|
|
3483
|
+
this.autoPausedForVisibility = false;
|
|
3066
3484
|
await this.session.play();
|
|
3067
3485
|
}
|
|
3068
3486
|
/** Pause playback. No-op if the player is not ready or already paused. */
|
|
3069
3487
|
pause() {
|
|
3488
|
+
this.userIntent = "pause";
|
|
3489
|
+
this.autoPausedForVisibility = false;
|
|
3070
3490
|
this.session?.pause();
|
|
3071
3491
|
}
|
|
3492
|
+
/**
|
|
3493
|
+
* Handle browser tab visibility changes. On hide: pause if the user
|
|
3494
|
+
* had been playing. On show: resume if we were the one who paused.
|
|
3495
|
+
* Skips when `backgroundBehavior: "continue"` is set (listener isn't
|
|
3496
|
+
* installed in that case).
|
|
3497
|
+
*/
|
|
3498
|
+
onVisibilityChange() {
|
|
3499
|
+
if (!this.session) return;
|
|
3500
|
+
const action = decideVisibilityAction({
|
|
3501
|
+
hidden: document.hidden,
|
|
3502
|
+
userIntent: this.userIntent,
|
|
3503
|
+
sessionIsPlaying: !this.options.target.paused,
|
|
3504
|
+
autoPausedForVisibility: this.autoPausedForVisibility
|
|
3505
|
+
});
|
|
3506
|
+
if (action === "pause") {
|
|
3507
|
+
this.autoPausedForVisibility = true;
|
|
3508
|
+
chunkG4APZMCP_cjs.dbg.info("visibility", "tab hidden \u2014 auto-paused");
|
|
3509
|
+
this.session.pause();
|
|
3510
|
+
} else if (action === "resume") {
|
|
3511
|
+
this.autoPausedForVisibility = false;
|
|
3512
|
+
chunkG4APZMCP_cjs.dbg.info("visibility", "tab visible \u2014 auto-resuming");
|
|
3513
|
+
void this.session.play().catch((err) => {
|
|
3514
|
+
console.warn("[avbridge] auto-resume after tab return failed:", err);
|
|
3515
|
+
});
|
|
3516
|
+
}
|
|
3517
|
+
}
|
|
3072
3518
|
/** Seek to the given time in seconds. Throws if the player is not ready. */
|
|
3073
3519
|
async seek(time) {
|
|
3074
|
-
if (!this.session) throw new
|
|
3520
|
+
if (!this.session) throw new chunk6UUT4BEA_cjs.AvbridgeError(chunk6UUT4BEA_cjs.ERR_PLAYER_NOT_READY, "Player not ready \u2014 wait for the 'ready' event before calling playback methods.", "Await the 'ready' event or check player.readyState before calling play/pause/seek.");
|
|
3075
3521
|
await this.session.seek(time);
|
|
3076
3522
|
}
|
|
3077
3523
|
/** Switch the active audio track by track ID. Throws if the player is not ready. */
|
|
3078
3524
|
async setAudioTrack(id) {
|
|
3079
|
-
if (!this.session) throw new
|
|
3525
|
+
if (!this.session) throw new chunk6UUT4BEA_cjs.AvbridgeError(chunk6UUT4BEA_cjs.ERR_PLAYER_NOT_READY, "Player not ready \u2014 wait for the 'ready' event before calling playback methods.", "Await the 'ready' event or check player.readyState before calling play/pause/seek.");
|
|
3080
3526
|
await this.session.setAudioTrack(id);
|
|
3081
3527
|
}
|
|
3082
3528
|
/** Switch the active subtitle track by track ID, or pass `null` to disable subtitles. */
|
|
3083
3529
|
async setSubtitleTrack(id) {
|
|
3084
|
-
if (!this.session) throw new
|
|
3530
|
+
if (!this.session) throw new chunk6UUT4BEA_cjs.AvbridgeError(chunk6UUT4BEA_cjs.ERR_PLAYER_NOT_READY, "Player not ready \u2014 wait for the 'ready' event before calling playback methods.", "Await the 'ready' event or check player.readyState before calling play/pause/seek.");
|
|
3085
3531
|
await this.session.setSubtitleTrack(id);
|
|
3086
3532
|
}
|
|
3087
3533
|
/** Return a snapshot of current diagnostics: container, codecs, strategy, runtime stats, and strategy history. */
|
|
@@ -3109,6 +3555,14 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3109
3555
|
this.timeupdateInterval = null;
|
|
3110
3556
|
}
|
|
3111
3557
|
this.clearSupervisor();
|
|
3558
|
+
if (this.endedListener) {
|
|
3559
|
+
this.options.target.removeEventListener("ended", this.endedListener);
|
|
3560
|
+
this.endedListener = null;
|
|
3561
|
+
}
|
|
3562
|
+
if (this.visibilityListener) {
|
|
3563
|
+
document.removeEventListener("visibilitychange", this.visibilityListener);
|
|
3564
|
+
this.visibilityListener = null;
|
|
3565
|
+
}
|
|
3112
3566
|
if (this.session) {
|
|
3113
3567
|
await this.session.destroy();
|
|
3114
3568
|
this.session = null;
|
|
@@ -3120,14 +3574,24 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3120
3574
|
async function createPlayer(options) {
|
|
3121
3575
|
return UnifiedPlayer.create(options);
|
|
3122
3576
|
}
|
|
3577
|
+
function decideVisibilityAction(state) {
|
|
3578
|
+
if (state.hidden) {
|
|
3579
|
+
if (state.userIntent === "play" && state.sessionIsPlaying) return "pause";
|
|
3580
|
+
return "noop";
|
|
3581
|
+
}
|
|
3582
|
+
if (state.autoPausedForVisibility) return "resume";
|
|
3583
|
+
return "noop";
|
|
3584
|
+
}
|
|
3123
3585
|
function buildInitialDecision(initial, ctx) {
|
|
3124
3586
|
const natural = classifyContext(ctx);
|
|
3125
3587
|
const cls = strategyToClass(initial, natural);
|
|
3588
|
+
const inherited = natural.fallbackChain ?? defaultFallbackChain(initial);
|
|
3589
|
+
const fallbackChain = inherited.filter((s) => s !== initial);
|
|
3126
3590
|
return {
|
|
3127
3591
|
class: cls,
|
|
3128
3592
|
strategy: initial,
|
|
3129
3593
|
reason: `initial strategy "${initial}" requested via options.initialStrategy`,
|
|
3130
|
-
fallbackChain
|
|
3594
|
+
fallbackChain
|
|
3131
3595
|
};
|
|
3132
3596
|
}
|
|
3133
3597
|
function strategyToClass(strategy, natural) {
|
|
@@ -3156,6 +3620,10 @@ function defaultFallbackChain(strategy) {
|
|
|
3156
3620
|
}
|
|
3157
3621
|
}
|
|
3158
3622
|
|
|
3623
|
+
exports.FALLBACK_AUDIO_CODECS = FALLBACK_AUDIO_CODECS;
|
|
3624
|
+
exports.FALLBACK_VIDEO_CODECS = FALLBACK_VIDEO_CODECS;
|
|
3625
|
+
exports.NATIVE_AUDIO_CODECS = NATIVE_AUDIO_CODECS;
|
|
3626
|
+
exports.NATIVE_VIDEO_CODECS = NATIVE_VIDEO_CODECS;
|
|
3159
3627
|
exports.UnifiedPlayer = UnifiedPlayer;
|
|
3160
3628
|
exports.avbridgeAudioToMediabunny = avbridgeAudioToMediabunny;
|
|
3161
3629
|
exports.avbridgeVideoToMediabunny = avbridgeVideoToMediabunny;
|
|
@@ -3164,5 +3632,5 @@ exports.classifyContext = classifyContext;
|
|
|
3164
3632
|
exports.createPlayer = createPlayer;
|
|
3165
3633
|
exports.probe = probe;
|
|
3166
3634
|
exports.srtToVtt = srtToVtt;
|
|
3167
|
-
//# sourceMappingURL=chunk-
|
|
3168
|
-
//# sourceMappingURL=chunk-
|
|
3635
|
+
//# sourceMappingURL=chunk-7RGG6ME7.cjs.map
|
|
3636
|
+
//# sourceMappingURL=chunk-7RGG6ME7.cjs.map
|