avbridge 2.2.1 → 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 +80 -1
- package/NOTICE.md +2 -2
- package/README.md +2 -3
- 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-UF2N5L63.cjs → chunk-7RGG6ME7.cjs} +489 -76
- 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-DMWARSEF.js → chunk-NV7ILLWH.js} +483 -74
- 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 +558 -85
- 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-U2NPmFvA.d.cts → player-B6WB74RD.d.cts} +62 -3
- package/dist/{player-U2NPmFvA.d.ts → player-B6WB74RD.d.ts} +62 -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 +104 -12
- 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 +29 -4
- package/src/strategies/fallback/variant-routing.ts +7 -13
- package/src/strategies/fallback/video-renderer.ts +124 -32
- package/src/strategies/hybrid/decoder.ts +131 -20
- package/src/strategies/hybrid/index.ts +31 -5
- package/src/strategies/remux/mse.ts +12 -2
- 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-DMWARSEF.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-UF2N5L63.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);
|
|
@@ -1157,6 +1197,10 @@ async function createRemuxSession(context, video) {
|
|
|
1157
1197
|
}
|
|
1158
1198
|
|
|
1159
1199
|
// src/strategies/fallback/video-renderer.ts
|
|
1200
|
+
function isDebug() {
|
|
1201
|
+
return typeof globalThis !== "undefined" && !!globalThis.AVBRIDGE_DEBUG;
|
|
1202
|
+
}
|
|
1203
|
+
var lastDebugLog = 0;
|
|
1160
1204
|
var VideoRenderer = class {
|
|
1161
1205
|
constructor(target, clock, fps = 30) {
|
|
1162
1206
|
this.target = target;
|
|
@@ -1203,6 +1247,20 @@ var VideoRenderer = class {
|
|
|
1203
1247
|
lastPaintWall = 0;
|
|
1204
1248
|
/** Minimum ms between paints — paces video at roughly source fps. */
|
|
1205
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;
|
|
1206
1264
|
/** Resolves once the first decoded frame has been enqueued. */
|
|
1207
1265
|
firstFrameReady;
|
|
1208
1266
|
resolveFirstFrame;
|
|
@@ -1251,21 +1309,81 @@ var VideoRenderer = class {
|
|
|
1251
1309
|
}
|
|
1252
1310
|
return;
|
|
1253
1311
|
}
|
|
1254
|
-
const
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
if (
|
|
1258
|
-
const
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
this.
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
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
|
+
}
|
|
1266
1349
|
return;
|
|
1267
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;
|
|
1268
1384
|
}
|
|
1385
|
+
const wallNow = performance.now();
|
|
1386
|
+
if (wallNow - this.lastPaintWall < this.paintIntervalMs - 2) return;
|
|
1269
1387
|
const frame = this.queue.shift();
|
|
1270
1388
|
this.paint(frame);
|
|
1271
1389
|
frame.close();
|
|
@@ -1287,8 +1405,13 @@ var VideoRenderer = class {
|
|
|
1287
1405
|
}
|
|
1288
1406
|
/** Discard all queued frames. Used by seek to drop stale buffers. */
|
|
1289
1407
|
flush() {
|
|
1408
|
+
const count = this.queue.length;
|
|
1290
1409
|
while (this.queue.length > 0) this.queue.shift()?.close();
|
|
1291
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
|
+
}
|
|
1292
1415
|
}
|
|
1293
1416
|
stats() {
|
|
1294
1417
|
return {
|
|
@@ -1333,11 +1456,38 @@ var AudioOutput = class {
|
|
|
1333
1456
|
pendingQueue = [];
|
|
1334
1457
|
framesScheduled = 0;
|
|
1335
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;
|
|
1336
1463
|
constructor() {
|
|
1337
1464
|
this.ctx = new AudioContext();
|
|
1338
1465
|
this.gain = this.ctx.createGain();
|
|
1339
1466
|
this.gain.connect(this.ctx.destination);
|
|
1340
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
|
+
}
|
|
1341
1491
|
/**
|
|
1342
1492
|
* Switch into wall-clock fallback mode. Called by the decoder when no
|
|
1343
1493
|
* audio decoder could be initialized for the source. Once set, this
|
|
@@ -1487,6 +1637,7 @@ var AudioOutput = class {
|
|
|
1487
1637
|
}
|
|
1488
1638
|
this.gain = this.ctx.createGain();
|
|
1489
1639
|
this.gain.connect(this.ctx.destination);
|
|
1640
|
+
this.applyGain();
|
|
1490
1641
|
this.pendingQueue = [];
|
|
1491
1642
|
this.mediaTimeOfAnchor = newMediaTime;
|
|
1492
1643
|
this.mediaTimeOfNext = newMediaTime;
|
|
@@ -1518,8 +1669,8 @@ async function startHybridDecoder(opts) {
|
|
|
1518
1669
|
const variant = pickLibavVariant(opts.context);
|
|
1519
1670
|
const libav = await loadLibav(variant);
|
|
1520
1671
|
const bridge = await loadBridge();
|
|
1521
|
-
const { prepareLibavInput } = await import('./libav-http-reader-
|
|
1522
|
-
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);
|
|
1523
1674
|
const readPkt = await libav.av_packet_alloc();
|
|
1524
1675
|
const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
|
|
1525
1676
|
const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
|
|
@@ -1590,6 +1741,56 @@ async function startHybridDecoder(opts) {
|
|
|
1590
1741
|
});
|
|
1591
1742
|
throw new Error("hybrid decoder: could not initialize any decoders");
|
|
1592
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
|
+
}
|
|
1593
1794
|
let destroyed = false;
|
|
1594
1795
|
let pumpToken = 0;
|
|
1595
1796
|
let pumpRunning = null;
|
|
@@ -1617,8 +1818,15 @@ async function startHybridDecoder(opts) {
|
|
|
1617
1818
|
if (myToken !== pumpToken || destroyed) return;
|
|
1618
1819
|
const videoPackets = videoStream ? packets[videoStream.index] : void 0;
|
|
1619
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;
|
|
1620
1827
|
if (videoDecoder && videoPackets && videoPackets.length > 0) {
|
|
1621
|
-
|
|
1828
|
+
const processed = await applyBSF(videoPackets);
|
|
1829
|
+
for (const pkt of processed) {
|
|
1622
1830
|
if (myToken !== pumpToken || destroyed) return;
|
|
1623
1831
|
sanitizePacketTimestamp(pkt, () => {
|
|
1624
1832
|
const ts = syntheticVideoUs;
|
|
@@ -1638,9 +1846,6 @@ async function startHybridDecoder(opts) {
|
|
|
1638
1846
|
}
|
|
1639
1847
|
}
|
|
1640
1848
|
}
|
|
1641
|
-
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
1642
|
-
await decodeAudioBatch(audioPackets, myToken);
|
|
1643
|
-
}
|
|
1644
1849
|
packetsRead += (videoPackets?.length ?? 0) + (audioPackets?.length ?? 0);
|
|
1645
1850
|
while (!destroyed && myToken === pumpToken && (videoDecoder && videoDecoder.decodeQueueSize > 10 || opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
|
|
1646
1851
|
await new Promise((r) => setTimeout(r, 50));
|
|
@@ -1663,20 +1868,43 @@ async function startHybridDecoder(opts) {
|
|
|
1663
1868
|
}
|
|
1664
1869
|
async function decodeAudioBatch(pkts, myToken, flush = false) {
|
|
1665
1870
|
if (!audioDec || destroyed || myToken !== pumpToken) return;
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
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
|
+
}
|
|
1678
1905
|
}
|
|
1679
1906
|
if (myToken !== pumpToken || destroyed) return;
|
|
1907
|
+
const frames = allFrames;
|
|
1680
1908
|
for (const f of frames) {
|
|
1681
1909
|
if (myToken !== pumpToken || destroyed) return;
|
|
1682
1910
|
sanitizeFrameTimestamp(
|
|
@@ -1713,6 +1941,14 @@ async function startHybridDecoder(opts) {
|
|
|
1713
1941
|
await pumpRunning;
|
|
1714
1942
|
} catch {
|
|
1715
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
|
+
}
|
|
1716
1952
|
try {
|
|
1717
1953
|
if (videoDecoder && videoDecoder.state !== "closed") videoDecoder.close();
|
|
1718
1954
|
} catch {
|
|
@@ -1766,6 +2002,7 @@ async function startHybridDecoder(opts) {
|
|
|
1766
2002
|
if (audioDec) await libav.avcodec_flush_buffers?.(audioDec.c);
|
|
1767
2003
|
} catch {
|
|
1768
2004
|
}
|
|
2005
|
+
await flushBSF();
|
|
1769
2006
|
syntheticVideoUs = Math.round(timeSec * 1e6);
|
|
1770
2007
|
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
1771
2008
|
pumpRunning = pumpLoop(newToken).catch(
|
|
@@ -1779,6 +2016,7 @@ async function startHybridDecoder(opts) {
|
|
|
1779
2016
|
videoFramesDecoded,
|
|
1780
2017
|
videoChunksFed,
|
|
1781
2018
|
audioFramesDecoded,
|
|
2019
|
+
bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
|
|
1782
2020
|
videoDecodeQueueSize: videoDecoder?.decodeQueueSize ?? 0,
|
|
1783
2021
|
// Confirmed transport info — see fallback decoder for the pattern.
|
|
1784
2022
|
_transport: inputHandle.transport === "http-range" ? "http-range" : "memory",
|
|
@@ -1955,8 +2193,8 @@ async function loadBridge() {
|
|
|
1955
2193
|
// src/strategies/hybrid/index.ts
|
|
1956
2194
|
var READY_AUDIO_BUFFER_SECONDS = 0.3;
|
|
1957
2195
|
var READY_TIMEOUT_SECONDS = 10;
|
|
1958
|
-
async function createHybridSession(ctx, target) {
|
|
1959
|
-
const { normalizeSource: normalizeSource2 } = await import('./source-
|
|
2196
|
+
async function createHybridSession(ctx, target, transport) {
|
|
2197
|
+
const { normalizeSource: normalizeSource2 } = await import('./source-QJR3OHTW.js');
|
|
1960
2198
|
const source = await normalizeSource2(ctx.source);
|
|
1961
2199
|
const fps = ctx.videoTracks[0]?.fps ?? 30;
|
|
1962
2200
|
const audio = new AudioOutput();
|
|
@@ -1968,7 +2206,8 @@ async function createHybridSession(ctx, target) {
|
|
|
1968
2206
|
filename: ctx.name ?? "input.bin",
|
|
1969
2207
|
context: ctx,
|
|
1970
2208
|
renderer,
|
|
1971
|
-
audio
|
|
2209
|
+
audio,
|
|
2210
|
+
transport
|
|
1972
2211
|
});
|
|
1973
2212
|
} catch (err) {
|
|
1974
2213
|
audio.destroy();
|
|
@@ -1986,6 +2225,22 @@ async function createHybridSession(ctx, target) {
|
|
|
1986
2225
|
configurable: true,
|
|
1987
2226
|
get: () => !audio.isPlaying()
|
|
1988
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
|
+
});
|
|
1989
2244
|
if (ctx.duration && Number.isFinite(ctx.duration)) {
|
|
1990
2245
|
Object.defineProperty(target, "duration", {
|
|
1991
2246
|
configurable: true,
|
|
@@ -2025,10 +2280,13 @@ async function createHybridSession(ctx, target) {
|
|
|
2025
2280
|
if (!audio.isPlaying()) {
|
|
2026
2281
|
await waitForBuffer();
|
|
2027
2282
|
await audio.start();
|
|
2283
|
+
target.dispatchEvent(new Event("play"));
|
|
2284
|
+
target.dispatchEvent(new Event("playing"));
|
|
2028
2285
|
}
|
|
2029
2286
|
},
|
|
2030
2287
|
pause() {
|
|
2031
2288
|
void audio.pause();
|
|
2289
|
+
target.dispatchEvent(new Event("pause"));
|
|
2032
2290
|
},
|
|
2033
2291
|
async seek(time) {
|
|
2034
2292
|
await doSeek(time);
|
|
@@ -2051,6 +2309,8 @@ async function createHybridSession(ctx, target) {
|
|
|
2051
2309
|
delete target.currentTime;
|
|
2052
2310
|
delete target.duration;
|
|
2053
2311
|
delete target.paused;
|
|
2312
|
+
delete target.volume;
|
|
2313
|
+
delete target.muted;
|
|
2054
2314
|
} catch {
|
|
2055
2315
|
}
|
|
2056
2316
|
},
|
|
@@ -2065,8 +2325,8 @@ async function startDecoder(opts) {
|
|
|
2065
2325
|
const variant = pickLibavVariant(opts.context);
|
|
2066
2326
|
const libav = await loadLibav(variant);
|
|
2067
2327
|
const bridge = await loadBridge2();
|
|
2068
|
-
const { prepareLibavInput } = await import('./libav-http-reader-
|
|
2069
|
-
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);
|
|
2070
2330
|
const readPkt = await libav.av_packet_alloc();
|
|
2071
2331
|
const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
|
|
2072
2332
|
const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
|
|
@@ -2122,6 +2382,56 @@ async function startDecoder(opts) {
|
|
|
2122
2382
|
`fallback decoder: could not initialize any libav decoders (${codecs}).${hint}`
|
|
2123
2383
|
);
|
|
2124
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
|
+
}
|
|
2125
2435
|
let destroyed = false;
|
|
2126
2436
|
let pumpToken = 0;
|
|
2127
2437
|
let pumpRunning = null;
|
|
@@ -2157,7 +2467,8 @@ async function startDecoder(opts) {
|
|
|
2157
2467
|
}
|
|
2158
2468
|
if (myToken !== pumpToken || destroyed) return;
|
|
2159
2469
|
if (videoDec && videoPackets && videoPackets.length > 0) {
|
|
2160
|
-
await
|
|
2470
|
+
const processed = await applyBSF(videoPackets);
|
|
2471
|
+
await decodeVideoBatch(processed, myToken);
|
|
2161
2472
|
}
|
|
2162
2473
|
packetsRead += (videoPackets?.length ?? 0) + (audioPackets?.length ?? 0);
|
|
2163
2474
|
if (videoFramesDecoded > 0) {
|
|
@@ -2303,6 +2614,14 @@ async function startDecoder(opts) {
|
|
|
2303
2614
|
await pumpRunning;
|
|
2304
2615
|
} catch {
|
|
2305
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
|
+
}
|
|
2306
2625
|
try {
|
|
2307
2626
|
if (videoDec) await libav.ff_free_decoder?.(videoDec.c, videoDec.pkt, videoDec.frame);
|
|
2308
2627
|
} catch {
|
|
@@ -2354,6 +2673,7 @@ async function startDecoder(opts) {
|
|
|
2354
2673
|
if (audioDec) await libav.avcodec_flush_buffers?.(audioDec.c);
|
|
2355
2674
|
} catch {
|
|
2356
2675
|
}
|
|
2676
|
+
await flushBSF();
|
|
2357
2677
|
syntheticVideoUs = Math.round(timeSec * 1e6);
|
|
2358
2678
|
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
2359
2679
|
pumpRunning = pumpLoop(newToken).catch(
|
|
@@ -2366,6 +2686,7 @@ async function startDecoder(opts) {
|
|
|
2366
2686
|
packetsRead,
|
|
2367
2687
|
videoFramesDecoded,
|
|
2368
2688
|
audioFramesDecoded,
|
|
2689
|
+
bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
|
|
2369
2690
|
// Confirmed transport info: once prepareLibavInput returns
|
|
2370
2691
|
// successfully, we *know* whether the source is http-range (probe
|
|
2371
2692
|
// succeeded and returned 206) or in-memory blob. Diagnostics hoists
|
|
@@ -2521,8 +2842,8 @@ async function loadBridge2() {
|
|
|
2521
2842
|
// src/strategies/fallback/index.ts
|
|
2522
2843
|
var READY_AUDIO_BUFFER_SECONDS2 = 0.04;
|
|
2523
2844
|
var READY_TIMEOUT_SECONDS2 = 3;
|
|
2524
|
-
async function createFallbackSession(ctx, target) {
|
|
2525
|
-
const { normalizeSource: normalizeSource2 } = await import('./source-
|
|
2845
|
+
async function createFallbackSession(ctx, target, transport) {
|
|
2846
|
+
const { normalizeSource: normalizeSource2 } = await import('./source-QJR3OHTW.js');
|
|
2526
2847
|
const source = await normalizeSource2(ctx.source);
|
|
2527
2848
|
const fps = ctx.videoTracks[0]?.fps ?? 30;
|
|
2528
2849
|
const audio = new AudioOutput();
|
|
@@ -2534,7 +2855,8 @@ async function createFallbackSession(ctx, target) {
|
|
|
2534
2855
|
filename: ctx.name ?? "input.bin",
|
|
2535
2856
|
context: ctx,
|
|
2536
2857
|
renderer,
|
|
2537
|
-
audio
|
|
2858
|
+
audio,
|
|
2859
|
+
transport
|
|
2538
2860
|
});
|
|
2539
2861
|
} catch (err) {
|
|
2540
2862
|
audio.destroy();
|
|
@@ -2552,6 +2874,22 @@ async function createFallbackSession(ctx, target) {
|
|
|
2552
2874
|
configurable: true,
|
|
2553
2875
|
get: () => !audio.isPlaying()
|
|
2554
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
|
+
});
|
|
2555
2893
|
if (ctx.duration && Number.isFinite(ctx.duration)) {
|
|
2556
2894
|
Object.defineProperty(target, "duration", {
|
|
2557
2895
|
configurable: true,
|
|
@@ -2615,10 +2953,13 @@ async function createFallbackSession(ctx, target) {
|
|
|
2615
2953
|
if (!audio.isPlaying()) {
|
|
2616
2954
|
await waitForBuffer();
|
|
2617
2955
|
await audio.start();
|
|
2956
|
+
target.dispatchEvent(new Event("play"));
|
|
2957
|
+
target.dispatchEvent(new Event("playing"));
|
|
2618
2958
|
}
|
|
2619
2959
|
},
|
|
2620
2960
|
pause() {
|
|
2621
2961
|
void audio.pause();
|
|
2962
|
+
target.dispatchEvent(new Event("pause"));
|
|
2622
2963
|
},
|
|
2623
2964
|
async seek(time) {
|
|
2624
2965
|
await doSeek(time);
|
|
@@ -2638,6 +2979,8 @@ async function createFallbackSession(ctx, target) {
|
|
|
2638
2979
|
delete target.currentTime;
|
|
2639
2980
|
delete target.duration;
|
|
2640
2981
|
delete target.paused;
|
|
2982
|
+
delete target.volume;
|
|
2983
|
+
delete target.muted;
|
|
2641
2984
|
} catch {
|
|
2642
2985
|
}
|
|
2643
2986
|
},
|
|
@@ -2661,12 +3004,12 @@ var remuxPlugin = {
|
|
|
2661
3004
|
var hybridPlugin = {
|
|
2662
3005
|
name: "hybrid",
|
|
2663
3006
|
canHandle: () => typeof VideoDecoder !== "undefined",
|
|
2664
|
-
execute: (ctx, video) => createHybridSession(ctx, video)
|
|
3007
|
+
execute: (ctx, video, transport) => createHybridSession(ctx, video, transport)
|
|
2665
3008
|
};
|
|
2666
3009
|
var fallbackPlugin = {
|
|
2667
3010
|
name: "fallback",
|
|
2668
3011
|
canHandle: () => true,
|
|
2669
|
-
execute: (ctx, video) => createFallbackSession(ctx, video)
|
|
3012
|
+
execute: (ctx, video, transport) => createFallbackSession(ctx, video, transport)
|
|
2670
3013
|
};
|
|
2671
3014
|
function registerBuiltins(registry) {
|
|
2672
3015
|
registry.register(nativePlugin);
|
|
@@ -2722,7 +3065,8 @@ var SubtitleResourceBag = class {
|
|
|
2722
3065
|
this.urls.clear();
|
|
2723
3066
|
}
|
|
2724
3067
|
};
|
|
2725
|
-
async function attachSubtitleTracks(video, tracks, bag, onError) {
|
|
3068
|
+
async function attachSubtitleTracks(video, tracks, bag, onError, transport) {
|
|
3069
|
+
const doFetch = fetchWith(transport);
|
|
2726
3070
|
for (const t of Array.from(video.querySelectorAll("track[data-avbridge]"))) {
|
|
2727
3071
|
t.remove();
|
|
2728
3072
|
}
|
|
@@ -2731,13 +3075,13 @@ async function attachSubtitleTracks(video, tracks, bag, onError) {
|
|
|
2731
3075
|
try {
|
|
2732
3076
|
let url = t.sidecarUrl;
|
|
2733
3077
|
if (t.format === "srt") {
|
|
2734
|
-
const res = await
|
|
3078
|
+
const res = await doFetch(t.sidecarUrl, transport?.requestInit);
|
|
2735
3079
|
const text = await res.text();
|
|
2736
3080
|
const vtt = srtToVtt(text);
|
|
2737
3081
|
const blob = new Blob([vtt], { type: "text/vtt" });
|
|
2738
3082
|
url = bag ? bag.createObjectURL(blob) : URL.createObjectURL(blob);
|
|
2739
3083
|
} else if (t.format === "vtt") {
|
|
2740
|
-
const res = await
|
|
3084
|
+
const res = await doFetch(t.sidecarUrl, transport?.requestInit);
|
|
2741
3085
|
const text = await res.text();
|
|
2742
3086
|
if (!isVtt(text)) {
|
|
2743
3087
|
console.warn("[avbridge] subtitle missing WEBVTT header:", t.sidecarUrl);
|
|
@@ -2765,6 +3109,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2765
3109
|
constructor(options, registry) {
|
|
2766
3110
|
this.options = options;
|
|
2767
3111
|
this.registry = registry;
|
|
3112
|
+
const { requestInit, fetchFn } = options;
|
|
3113
|
+
if (requestInit || fetchFn) {
|
|
3114
|
+
this.transport = { requestInit, fetchFn };
|
|
3115
|
+
}
|
|
2768
3116
|
}
|
|
2769
3117
|
options;
|
|
2770
3118
|
registry;
|
|
@@ -2784,11 +3132,23 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2784
3132
|
// listener outlives the player and accumulates on elements that swap
|
|
2785
3133
|
// source (e.g. <avbridge-video>).
|
|
2786
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;
|
|
2787
3143
|
// Serializes escalation / setStrategy calls
|
|
2788
3144
|
switchingPromise = Promise.resolve();
|
|
2789
3145
|
// Owns blob URLs created during sidecar discovery + SRT->VTT conversion.
|
|
2790
3146
|
// Revoked at destroy() so repeated source swaps don't leak.
|
|
2791
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;
|
|
2792
3152
|
static async create(options) {
|
|
2793
3153
|
const registry = new PluginRegistry();
|
|
2794
3154
|
registerBuiltins(registry);
|
|
@@ -2812,7 +3172,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2812
3172
|
const bootstrapStart = performance.now();
|
|
2813
3173
|
try {
|
|
2814
3174
|
dbg.info("bootstrap", "start");
|
|
2815
|
-
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));
|
|
2816
3176
|
dbg.info(
|
|
2817
3177
|
"probe",
|
|
2818
3178
|
`container=${ctx.container} video=${ctx.videoTracks[0]?.codec ?? "-"} audio=${ctx.audioTracks[0]?.codec ?? "-"} probedBy=${ctx.probedBy}`
|
|
@@ -2860,7 +3220,8 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2860
3220
|
this.subtitleResources,
|
|
2861
3221
|
(err, track) => {
|
|
2862
3222
|
console.warn(`[avbridge] subtitle ${track.id} failed: ${err.message}`);
|
|
2863
|
-
}
|
|
3223
|
+
},
|
|
3224
|
+
this.transport
|
|
2864
3225
|
);
|
|
2865
3226
|
}
|
|
2866
3227
|
this.emitter.emitSticky("tracks", {
|
|
@@ -2871,6 +3232,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2871
3232
|
this.startTimeupdateLoop();
|
|
2872
3233
|
this.endedListener = () => this.emitter.emit("ended", void 0);
|
|
2873
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
|
+
}
|
|
2874
3239
|
this.emitter.emitSticky("ready", void 0);
|
|
2875
3240
|
const bootstrapElapsed = performance.now() - bootstrapStart;
|
|
2876
3241
|
dbg.info("bootstrap", `ready in ${bootstrapElapsed.toFixed(0)}ms`);
|
|
@@ -2897,7 +3262,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2897
3262
|
throw new Error(`no plugin available for strategy "${strategy}"`);
|
|
2898
3263
|
}
|
|
2899
3264
|
try {
|
|
2900
|
-
this.session = await plugin.execute(this.mediaContext, this.options.target);
|
|
3265
|
+
this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
|
|
2901
3266
|
} catch (err) {
|
|
2902
3267
|
const chain = this.classification?.fallbackChain;
|
|
2903
3268
|
if (chain && chain.length > 0) {
|
|
@@ -2970,7 +3335,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2970
3335
|
continue;
|
|
2971
3336
|
}
|
|
2972
3337
|
try {
|
|
2973
|
-
this.session = await plugin.execute(this.mediaContext, this.options.target);
|
|
3338
|
+
this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
|
|
2974
3339
|
} catch (err) {
|
|
2975
3340
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2976
3341
|
errors.push(`${nextStrategy}: ${msg}`);
|
|
@@ -2993,8 +3358,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2993
3358
|
}
|
|
2994
3359
|
return;
|
|
2995
3360
|
}
|
|
2996
|
-
this.emitter.emit("error", new
|
|
2997
|
-
|
|
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."
|
|
2998
3365
|
));
|
|
2999
3366
|
}
|
|
3000
3367
|
// ── Stall supervision ─────────────────────────────────────────────────
|
|
@@ -3046,7 +3413,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3046
3413
|
// ── Public: manual strategy switch ────────────────────────────────────
|
|
3047
3414
|
/** Manually switch to a different playback strategy. Preserves current position and play/pause state. Concurrent calls are serialized. */
|
|
3048
3415
|
async setStrategy(strategy, reason) {
|
|
3049
|
-
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.");
|
|
3050
3417
|
if (this.session?.strategy === strategy) return;
|
|
3051
3418
|
this.switchingPromise = this.switchingPromise.then(
|
|
3052
3419
|
() => this.doSetStrategy(strategy, reason)
|
|
@@ -3075,7 +3442,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3075
3442
|
}
|
|
3076
3443
|
const plugin = this.registry.findFor(this.mediaContext, strategy);
|
|
3077
3444
|
if (!plugin) throw new Error(`no plugin available for strategy "${strategy}"`);
|
|
3078
|
-
this.session = await plugin.execute(this.mediaContext, this.options.target);
|
|
3445
|
+
this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
|
|
3079
3446
|
this.emitter.emitSticky("strategy", {
|
|
3080
3447
|
strategy,
|
|
3081
3448
|
reason: switchReason
|
|
@@ -3109,26 +3476,56 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3109
3476
|
}
|
|
3110
3477
|
/** Begin or resume playback. Throws if the player is not ready. */
|
|
3111
3478
|
async play() {
|
|
3112
|
-
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;
|
|
3113
3482
|
await this.session.play();
|
|
3114
3483
|
}
|
|
3115
3484
|
/** Pause playback. No-op if the player is not ready or already paused. */
|
|
3116
3485
|
pause() {
|
|
3486
|
+
this.userIntent = "pause";
|
|
3487
|
+
this.autoPausedForVisibility = false;
|
|
3117
3488
|
this.session?.pause();
|
|
3118
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
|
+
}
|
|
3119
3516
|
/** Seek to the given time in seconds. Throws if the player is not ready. */
|
|
3120
3517
|
async seek(time) {
|
|
3121
|
-
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.");
|
|
3122
3519
|
await this.session.seek(time);
|
|
3123
3520
|
}
|
|
3124
3521
|
/** Switch the active audio track by track ID. Throws if the player is not ready. */
|
|
3125
3522
|
async setAudioTrack(id) {
|
|
3126
|
-
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.");
|
|
3127
3524
|
await this.session.setAudioTrack(id);
|
|
3128
3525
|
}
|
|
3129
3526
|
/** Switch the active subtitle track by track ID, or pass `null` to disable subtitles. */
|
|
3130
3527
|
async setSubtitleTrack(id) {
|
|
3131
|
-
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.");
|
|
3132
3529
|
await this.session.setSubtitleTrack(id);
|
|
3133
3530
|
}
|
|
3134
3531
|
/** Return a snapshot of current diagnostics: container, codecs, strategy, runtime stats, and strategy history. */
|
|
@@ -3160,6 +3557,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3160
3557
|
this.options.target.removeEventListener("ended", this.endedListener);
|
|
3161
3558
|
this.endedListener = null;
|
|
3162
3559
|
}
|
|
3560
|
+
if (this.visibilityListener) {
|
|
3561
|
+
document.removeEventListener("visibilitychange", this.visibilityListener);
|
|
3562
|
+
this.visibilityListener = null;
|
|
3563
|
+
}
|
|
3163
3564
|
if (this.session) {
|
|
3164
3565
|
await this.session.destroy();
|
|
3165
3566
|
this.session = null;
|
|
@@ -3171,6 +3572,14 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3171
3572
|
async function createPlayer(options) {
|
|
3172
3573
|
return UnifiedPlayer.create(options);
|
|
3173
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
|
+
}
|
|
3174
3583
|
function buildInitialDecision(initial, ctx) {
|
|
3175
3584
|
const natural = classifyContext(ctx);
|
|
3176
3585
|
const cls = strategyToClass(initial, natural);
|
|
@@ -3209,6 +3618,6 @@ function defaultFallbackChain(strategy) {
|
|
|
3209
3618
|
}
|
|
3210
3619
|
}
|
|
3211
3620
|
|
|
3212
|
-
export { UnifiedPlayer, avbridgeAudioToMediabunny, avbridgeVideoToMediabunny, buildMediabunnySourceFromInput, classifyContext, createPlayer, probe, srtToVtt };
|
|
3213
|
-
//# sourceMappingURL=chunk-
|
|
3214
|
-
//# 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
|