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
package/dist/element-browser.js
CHANGED
|
@@ -31,6 +31,51 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
31
31
|
mod
|
|
32
32
|
));
|
|
33
33
|
|
|
34
|
+
// src/util/transport.ts
|
|
35
|
+
function mergeFetchInit(base, extra) {
|
|
36
|
+
if (!base && !extra) return void 0;
|
|
37
|
+
return {
|
|
38
|
+
...base,
|
|
39
|
+
...extra,
|
|
40
|
+
headers: {
|
|
41
|
+
...base?.headers ?? {},
|
|
42
|
+
...extra?.headers ?? {}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function fetchWith(transport) {
|
|
47
|
+
return transport?.fetchFn ?? globalThis.fetch;
|
|
48
|
+
}
|
|
49
|
+
var init_transport = __esm({
|
|
50
|
+
"src/util/transport.ts"() {
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// src/errors.ts
|
|
55
|
+
var AvbridgeError, ERR_PROBE_FAILED, ERR_PROBE_UNKNOWN_CONTAINER, ERR_PROBE_FETCH_FAILED, ERR_ALL_STRATEGIES_EXHAUSTED, ERR_PLAYER_NOT_READY, ERR_LIBAV_NOT_REACHABLE, ERR_MSE_NOT_SUPPORTED, ERR_MSE_CODEC_NOT_SUPPORTED;
|
|
56
|
+
var init_errors = __esm({
|
|
57
|
+
"src/errors.ts"() {
|
|
58
|
+
AvbridgeError = class extends Error {
|
|
59
|
+
constructor(code, message, recovery, options) {
|
|
60
|
+
super(message, options);
|
|
61
|
+
this.code = code;
|
|
62
|
+
this.recovery = recovery;
|
|
63
|
+
}
|
|
64
|
+
code;
|
|
65
|
+
recovery;
|
|
66
|
+
name = "AvbridgeError";
|
|
67
|
+
};
|
|
68
|
+
ERR_PROBE_FAILED = "ERR_AVBRIDGE_PROBE_FAILED";
|
|
69
|
+
ERR_PROBE_UNKNOWN_CONTAINER = "ERR_AVBRIDGE_PROBE_UNKNOWN_CONTAINER";
|
|
70
|
+
ERR_PROBE_FETCH_FAILED = "ERR_AVBRIDGE_PROBE_FETCH_FAILED";
|
|
71
|
+
ERR_ALL_STRATEGIES_EXHAUSTED = "ERR_AVBRIDGE_ALL_STRATEGIES_EXHAUSTED";
|
|
72
|
+
ERR_PLAYER_NOT_READY = "ERR_AVBRIDGE_PLAYER_NOT_READY";
|
|
73
|
+
ERR_LIBAV_NOT_REACHABLE = "ERR_AVBRIDGE_LIBAV_NOT_REACHABLE";
|
|
74
|
+
ERR_MSE_NOT_SUPPORTED = "ERR_AVBRIDGE_MSE_NOT_SUPPORTED";
|
|
75
|
+
ERR_MSE_CODEC_NOT_SUPPORTED = "ERR_AVBRIDGE_MSE_CODEC_NOT_SUPPORTED";
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
34
79
|
// src/util/source.ts
|
|
35
80
|
var source_exports = {};
|
|
36
81
|
__export(source_exports, {
|
|
@@ -43,7 +88,7 @@ __export(source_exports, {
|
|
|
43
88
|
function isInMemorySource(source) {
|
|
44
89
|
return source.kind === "blob";
|
|
45
90
|
}
|
|
46
|
-
async function normalizeSource(source) {
|
|
91
|
+
async function normalizeSource(source, transport) {
|
|
47
92
|
if (source instanceof File) {
|
|
48
93
|
return {
|
|
49
94
|
kind: "blob",
|
|
@@ -66,22 +111,31 @@ async function normalizeSource(source) {
|
|
|
66
111
|
}
|
|
67
112
|
if (typeof source === "string" || source instanceof URL) {
|
|
68
113
|
const url2 = source instanceof URL ? source.toString() : source;
|
|
69
|
-
return await fetchUrlForSniff(url2, source);
|
|
114
|
+
return await fetchUrlForSniff(url2, source, transport);
|
|
70
115
|
}
|
|
71
116
|
throw new TypeError("unsupported source type");
|
|
72
117
|
}
|
|
73
|
-
async function fetchUrlForSniff(url2, originalSource) {
|
|
118
|
+
async function fetchUrlForSniff(url2, originalSource, transport) {
|
|
74
119
|
const name = url2.split("/").pop()?.split("?")[0] ?? void 0;
|
|
120
|
+
const doFetch = fetchWith(transport);
|
|
75
121
|
let res;
|
|
76
122
|
try {
|
|
77
|
-
res = await
|
|
123
|
+
res = await doFetch(url2, mergeFetchInit(transport?.requestInit, {
|
|
78
124
|
headers: { Range: `bytes=0-${URL_SNIFF_RANGE_BYTES - 1}` }
|
|
79
|
-
});
|
|
125
|
+
}));
|
|
80
126
|
} catch (err) {
|
|
81
|
-
throw new
|
|
127
|
+
throw new AvbridgeError(
|
|
128
|
+
ERR_PROBE_FETCH_FAILED,
|
|
129
|
+
`Failed to fetch source ${url2}: ${err.message}`,
|
|
130
|
+
"Check that the URL is reachable and CORS is configured. If the source requires authentication, pass requestInit with credentials/headers."
|
|
131
|
+
);
|
|
82
132
|
}
|
|
83
133
|
if (!res.ok && res.status !== 206) {
|
|
84
|
-
throw new
|
|
134
|
+
throw new AvbridgeError(
|
|
135
|
+
ERR_PROBE_FETCH_FAILED,
|
|
136
|
+
`Failed to fetch source ${url2}: ${res.status} ${res.statusText}`,
|
|
137
|
+
res.status === 403 || res.status === 401 ? "The server rejected the request. Pass requestInit with the required Authorization header or credentials." : "Check that the URL is correct and the server is reachable."
|
|
138
|
+
);
|
|
85
139
|
}
|
|
86
140
|
let byteLength;
|
|
87
141
|
const contentRange = res.headers.get("content-range");
|
|
@@ -186,6 +240,8 @@ async function readBlobBytes(blob, limit) {
|
|
|
186
240
|
var SNIFF_BYTES_NEEDED, URL_SNIFF_RANGE_BYTES;
|
|
187
241
|
var init_source = __esm({
|
|
188
242
|
"src/util/source.ts"() {
|
|
243
|
+
init_transport();
|
|
244
|
+
init_errors();
|
|
189
245
|
SNIFF_BYTES_NEEDED = 380;
|
|
190
246
|
URL_SNIFF_RANGE_BYTES = 32 * 1024;
|
|
191
247
|
}
|
|
@@ -29453,9 +29509,12 @@ __export(libav_http_reader_exports, {
|
|
|
29453
29509
|
attachLibavHttpReader: () => attachLibavHttpReader,
|
|
29454
29510
|
prepareLibavInput: () => prepareLibavInput
|
|
29455
29511
|
});
|
|
29456
|
-
async function prepareLibavInput(libav, filename, source) {
|
|
29512
|
+
async function prepareLibavInput(libav, filename, source, transport) {
|
|
29457
29513
|
if (source.kind === "url") {
|
|
29458
|
-
const handle = await attachLibavHttpReader(libav, filename, source.url
|
|
29514
|
+
const handle = await attachLibavHttpReader(libav, filename, source.url, {
|
|
29515
|
+
requestInit: transport?.requestInit,
|
|
29516
|
+
fetchFn: transport?.fetchFn
|
|
29517
|
+
});
|
|
29459
29518
|
return {
|
|
29460
29519
|
filename,
|
|
29461
29520
|
transport: "http-range",
|
|
@@ -30009,6 +30068,12 @@ function ffmpegToAvbridgeAudio(name) {
|
|
|
30009
30068
|
return "sipr";
|
|
30010
30069
|
case "atrac3":
|
|
30011
30070
|
return "atrac3";
|
|
30071
|
+
case "dca":
|
|
30072
|
+
case "dts":
|
|
30073
|
+
return "dts";
|
|
30074
|
+
case "truehd":
|
|
30075
|
+
case "mlp":
|
|
30076
|
+
return "truehd";
|
|
30012
30077
|
default:
|
|
30013
30078
|
return name;
|
|
30014
30079
|
}
|
|
@@ -31152,6 +31217,7 @@ async function safe(fn) {
|
|
|
31152
31217
|
}
|
|
31153
31218
|
|
|
31154
31219
|
// src/probe/index.ts
|
|
31220
|
+
init_errors();
|
|
31155
31221
|
var MEDIABUNNY_CONTAINERS = /* @__PURE__ */ new Set([
|
|
31156
31222
|
"mp4",
|
|
31157
31223
|
"mov",
|
|
@@ -31164,12 +31230,22 @@ var MEDIABUNNY_CONTAINERS = /* @__PURE__ */ new Set([
|
|
|
31164
31230
|
"adts",
|
|
31165
31231
|
"mpegts"
|
|
31166
31232
|
]);
|
|
31167
|
-
async function probe(source) {
|
|
31168
|
-
const normalized = await normalizeSource(source);
|
|
31233
|
+
async function probe(source, transport) {
|
|
31234
|
+
const normalized = await normalizeSource(source, transport);
|
|
31169
31235
|
const sniffed = await sniffNormalizedSource(normalized);
|
|
31170
31236
|
if (MEDIABUNNY_CONTAINERS.has(sniffed)) {
|
|
31171
31237
|
try {
|
|
31172
|
-
|
|
31238
|
+
const result = await probeWithMediabunny(normalized, sniffed);
|
|
31239
|
+
const hasUnknownCodec = result.videoTracks.some((t) => t.codec === "unknown") || result.audioTracks.some((t) => t.codec === "unknown");
|
|
31240
|
+
if (hasUnknownCodec) {
|
|
31241
|
+
try {
|
|
31242
|
+
const { probeWithLibav: probeWithLibav2 } = await Promise.resolve().then(() => (init_avi(), avi_exports));
|
|
31243
|
+
return await probeWithLibav2(normalized, sniffed);
|
|
31244
|
+
} catch {
|
|
31245
|
+
return result;
|
|
31246
|
+
}
|
|
31247
|
+
}
|
|
31248
|
+
return result;
|
|
31173
31249
|
} catch (mediabunnyErr) {
|
|
31174
31250
|
console.warn(
|
|
31175
31251
|
`[avbridge] mediabunny rejected ${sniffed} file, falling back to libav:`,
|
|
@@ -31181,8 +31257,10 @@ async function probe(source) {
|
|
|
31181
31257
|
} catch (libavErr) {
|
|
31182
31258
|
const mbMsg = mediabunnyErr.message || String(mediabunnyErr);
|
|
31183
31259
|
const lvMsg = libavErr instanceof Error ? libavErr.message : String(libavErr);
|
|
31184
|
-
throw new
|
|
31185
|
-
|
|
31260
|
+
throw new AvbridgeError(
|
|
31261
|
+
ERR_PROBE_FAILED,
|
|
31262
|
+
`Failed to probe ${sniffed.toUpperCase()} file. mediabunny: ${mbMsg}. libav: ${lvMsg}.`,
|
|
31263
|
+
"The file may be corrupt, truncated, or in an unsupported format. Enable AVBRIDGE_DEBUG for detailed logs."
|
|
31186
31264
|
);
|
|
31187
31265
|
}
|
|
31188
31266
|
}
|
|
@@ -31193,8 +31271,17 @@ async function probe(source) {
|
|
|
31193
31271
|
} catch (err) {
|
|
31194
31272
|
const inner = err instanceof Error ? err.message : String(err);
|
|
31195
31273
|
console.error("[avbridge] libav probe failed for", sniffed, "file:", err);
|
|
31196
|
-
|
|
31197
|
-
|
|
31274
|
+
if (sniffed === "unknown") {
|
|
31275
|
+
throw new AvbridgeError(
|
|
31276
|
+
ERR_PROBE_UNKNOWN_CONTAINER,
|
|
31277
|
+
`Unable to probe source: container format could not be identified. libav fallback: ${inner || "(no details)"}`,
|
|
31278
|
+
"The file may be corrupt or in a format avbridge doesn't recognize. Check the file plays in VLC or ffprobe."
|
|
31279
|
+
);
|
|
31280
|
+
}
|
|
31281
|
+
throw new AvbridgeError(
|
|
31282
|
+
ERR_LIBAV_NOT_REACHABLE,
|
|
31283
|
+
`${sniffed.toUpperCase()} files require libav.js, which failed to load: ${inner || "(no details)"}`,
|
|
31284
|
+
"Install @libav.js/variant-webcodecs, or check that AVBRIDGE_LIBAV_BASE points to the correct path."
|
|
31198
31285
|
);
|
|
31199
31286
|
}
|
|
31200
31287
|
}
|
|
@@ -31295,7 +31382,9 @@ var FALLBACK_AUDIO_CODECS = /* @__PURE__ */ new Set([
|
|
|
31295
31382
|
"ra_144",
|
|
31296
31383
|
"ra_288",
|
|
31297
31384
|
"sipr",
|
|
31298
|
-
"atrac3"
|
|
31385
|
+
"atrac3",
|
|
31386
|
+
"dts",
|
|
31387
|
+
"truehd"
|
|
31299
31388
|
]);
|
|
31300
31389
|
var NATIVE_CONTAINERS = /* @__PURE__ */ new Set([
|
|
31301
31390
|
"mp4",
|
|
@@ -31357,7 +31446,16 @@ function classifyContext(ctx) {
|
|
|
31357
31446
|
reason: `video codec "${video.codec}" has no browser decoder; WASM fallback required`
|
|
31358
31447
|
};
|
|
31359
31448
|
}
|
|
31360
|
-
|
|
31449
|
+
const audioNeedsFallback = audio && (FALLBACK_AUDIO_CODECS.has(audio.codec) || !NATIVE_AUDIO_CODECS.has(audio.codec));
|
|
31450
|
+
if (audioNeedsFallback) {
|
|
31451
|
+
if (NATIVE_VIDEO_CODECS.has(video.codec) && webCodecsAvailable()) {
|
|
31452
|
+
return {
|
|
31453
|
+
class: "HYBRID_CANDIDATE",
|
|
31454
|
+
strategy: "hybrid",
|
|
31455
|
+
reason: `video "${video.codec}" is hardware-decodable via WebCodecs; audio "${audio.codec}" decoded in software by libav`,
|
|
31456
|
+
fallbackChain: ["fallback"]
|
|
31457
|
+
};
|
|
31458
|
+
}
|
|
31361
31459
|
return {
|
|
31362
31460
|
class: "FALLBACK_REQUIRED",
|
|
31363
31461
|
strategy: "fallback",
|
|
@@ -31642,14 +31740,23 @@ function sourceToVideoUrl(source) {
|
|
|
31642
31740
|
}
|
|
31643
31741
|
|
|
31644
31742
|
// src/strategies/remux/mse.ts
|
|
31743
|
+
init_errors();
|
|
31645
31744
|
var MseSink = class {
|
|
31646
31745
|
constructor(options) {
|
|
31647
31746
|
this.options = options;
|
|
31648
31747
|
if (typeof MediaSource === "undefined") {
|
|
31649
|
-
throw new
|
|
31748
|
+
throw new AvbridgeError(
|
|
31749
|
+
ERR_MSE_NOT_SUPPORTED,
|
|
31750
|
+
"MediaSource Extensions (MSE) are not supported in this environment.",
|
|
31751
|
+
"MSE is required for the remux strategy. Use a browser that supports MSE, or try the fallback strategy."
|
|
31752
|
+
);
|
|
31650
31753
|
}
|
|
31651
31754
|
if (!MediaSource.isTypeSupported(options.mime)) {
|
|
31652
|
-
throw new
|
|
31755
|
+
throw new AvbridgeError(
|
|
31756
|
+
ERR_MSE_CODEC_NOT_SUPPORTED,
|
|
31757
|
+
`This browser's MSE does not support "${options.mime}".`,
|
|
31758
|
+
"The codec combination can't be played via remux in this browser. The player will try the next strategy automatically."
|
|
31759
|
+
);
|
|
31653
31760
|
}
|
|
31654
31761
|
this.mediaSource = new MediaSource();
|
|
31655
31762
|
this.objectUrl = URL.createObjectURL(this.mediaSource);
|
|
@@ -31978,6 +32085,10 @@ async function createRemuxPipeline(ctx, video) {
|
|
|
31978
32085
|
console.error("[avbridge] remux pipeline reseek failed:", err);
|
|
31979
32086
|
});
|
|
31980
32087
|
},
|
|
32088
|
+
setAutoPlay(autoPlay) {
|
|
32089
|
+
pendingAutoPlay = autoPlay;
|
|
32090
|
+
if (sink) sink.setPlayOnSeek(autoPlay);
|
|
32091
|
+
},
|
|
31981
32092
|
async destroy() {
|
|
31982
32093
|
destroyed = true;
|
|
31983
32094
|
pumpToken++;
|
|
@@ -32018,7 +32129,11 @@ async function createRemuxSession(context, video) {
|
|
|
32018
32129
|
await pipeline.start(video.currentTime || 0, true);
|
|
32019
32130
|
return;
|
|
32020
32131
|
}
|
|
32021
|
-
|
|
32132
|
+
pipeline.setAutoPlay(true);
|
|
32133
|
+
try {
|
|
32134
|
+
await video.play();
|
|
32135
|
+
} catch {
|
|
32136
|
+
}
|
|
32022
32137
|
},
|
|
32023
32138
|
pause() {
|
|
32024
32139
|
wantPlay = false;
|
|
@@ -32057,6 +32172,10 @@ async function createRemuxSession(context, video) {
|
|
|
32057
32172
|
}
|
|
32058
32173
|
|
|
32059
32174
|
// src/strategies/fallback/video-renderer.ts
|
|
32175
|
+
function isDebug() {
|
|
32176
|
+
return typeof globalThis !== "undefined" && !!globalThis.AVBRIDGE_DEBUG;
|
|
32177
|
+
}
|
|
32178
|
+
var lastDebugLog = 0;
|
|
32060
32179
|
var VideoRenderer = class {
|
|
32061
32180
|
constructor(target, clock, fps = 30) {
|
|
32062
32181
|
this.target = target;
|
|
@@ -32066,7 +32185,7 @@ var VideoRenderer = class {
|
|
|
32066
32185
|
this.resolveFirstFrame = resolve;
|
|
32067
32186
|
});
|
|
32068
32187
|
this.canvas = document.createElement("canvas");
|
|
32069
|
-
this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;";
|
|
32188
|
+
this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;object-fit:contain;";
|
|
32070
32189
|
const parent = target.parentElement ?? target.parentNode;
|
|
32071
32190
|
if (parent && parent instanceof HTMLElement) {
|
|
32072
32191
|
if (getComputedStyle(parent).position === "static") {
|
|
@@ -32103,6 +32222,20 @@ var VideoRenderer = class {
|
|
|
32103
32222
|
lastPaintWall = 0;
|
|
32104
32223
|
/** Minimum ms between paints — paces video at roughly source fps. */
|
|
32105
32224
|
paintIntervalMs;
|
|
32225
|
+
/** Cumulative count of frames skipped because all PTS are in the future. */
|
|
32226
|
+
ticksWaiting = 0;
|
|
32227
|
+
/** Cumulative count of ticks where PTS mode painted a frame. */
|
|
32228
|
+
ticksPainted = 0;
|
|
32229
|
+
/**
|
|
32230
|
+
* Calibration offset (microseconds) between video PTS and audio clock.
|
|
32231
|
+
* Video PTS and AudioContext.currentTime can drift ~0.1% relative to
|
|
32232
|
+
* each other (different clock domains). Over 45 minutes that's 2.6s.
|
|
32233
|
+
* We measure the offset on the first painted frame and update it
|
|
32234
|
+
* periodically so the PTS comparison stays calibrated.
|
|
32235
|
+
*/
|
|
32236
|
+
ptsCalibrationUs = 0;
|
|
32237
|
+
ptsCalibrated = false;
|
|
32238
|
+
lastCalibrationWall = 0;
|
|
32106
32239
|
/** Resolves once the first decoded frame has been enqueued. */
|
|
32107
32240
|
firstFrameReady;
|
|
32108
32241
|
resolveFirstFrame;
|
|
@@ -32151,21 +32284,81 @@ var VideoRenderer = class {
|
|
|
32151
32284
|
}
|
|
32152
32285
|
return;
|
|
32153
32286
|
}
|
|
32154
|
-
const
|
|
32155
|
-
|
|
32156
|
-
|
|
32157
|
-
if (
|
|
32158
|
-
const
|
|
32159
|
-
|
|
32160
|
-
|
|
32161
|
-
|
|
32162
|
-
this.
|
|
32163
|
-
|
|
32164
|
-
|
|
32165
|
-
|
|
32287
|
+
const rawAudioNowUs = this.clock.now() * 1e6;
|
|
32288
|
+
const headTs = this.queue[0].timestamp ?? 0;
|
|
32289
|
+
const hasPts = headTs > 0 || this.queue.length > 1;
|
|
32290
|
+
if (hasPts) {
|
|
32291
|
+
const wallNow2 = performance.now();
|
|
32292
|
+
if (!this.ptsCalibrated || wallNow2 - this.lastCalibrationWall > 1e4) {
|
|
32293
|
+
this.ptsCalibrationUs = headTs - rawAudioNowUs;
|
|
32294
|
+
this.ptsCalibrated = true;
|
|
32295
|
+
this.lastCalibrationWall = wallNow2;
|
|
32296
|
+
}
|
|
32297
|
+
const audioNowUs = rawAudioNowUs + this.ptsCalibrationUs;
|
|
32298
|
+
const frameDurationUs = this.paintIntervalMs * 1e3;
|
|
32299
|
+
const deadlineUs = audioNowUs + frameDurationUs;
|
|
32300
|
+
let bestIdx = -1;
|
|
32301
|
+
for (let i = 0; i < this.queue.length; i++) {
|
|
32302
|
+
const ts = this.queue[i].timestamp ?? 0;
|
|
32303
|
+
if (ts <= deadlineUs) {
|
|
32304
|
+
bestIdx = i;
|
|
32305
|
+
} else {
|
|
32306
|
+
break;
|
|
32307
|
+
}
|
|
32308
|
+
}
|
|
32309
|
+
if (bestIdx < 0) {
|
|
32310
|
+
this.ticksWaiting++;
|
|
32311
|
+
if (isDebug()) {
|
|
32312
|
+
const now = performance.now();
|
|
32313
|
+
if (now - lastDebugLog > 1e3) {
|
|
32314
|
+
const headPtsMs = (headTs / 1e3).toFixed(1);
|
|
32315
|
+
const audioMs = (audioNowUs / 1e3).toFixed(1);
|
|
32316
|
+
const rawDriftMs = ((headTs - rawAudioNowUs) / 1e3).toFixed(1);
|
|
32317
|
+
const calibMs = (this.ptsCalibrationUs / 1e3).toFixed(1);
|
|
32318
|
+
console.log(
|
|
32319
|
+
`[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}`
|
|
32320
|
+
);
|
|
32321
|
+
lastDebugLog = now;
|
|
32322
|
+
}
|
|
32323
|
+
}
|
|
32166
32324
|
return;
|
|
32167
32325
|
}
|
|
32326
|
+
const dropThresholdUs = audioNowUs - frameDurationUs * 2;
|
|
32327
|
+
let dropped = 0;
|
|
32328
|
+
while (bestIdx > 0) {
|
|
32329
|
+
const ts = this.queue[0].timestamp ?? 0;
|
|
32330
|
+
if (ts < dropThresholdUs) {
|
|
32331
|
+
this.queue.shift()?.close();
|
|
32332
|
+
this.framesDroppedLate++;
|
|
32333
|
+
bestIdx--;
|
|
32334
|
+
dropped++;
|
|
32335
|
+
} else {
|
|
32336
|
+
break;
|
|
32337
|
+
}
|
|
32338
|
+
}
|
|
32339
|
+
this.ticksPainted++;
|
|
32340
|
+
if (isDebug()) {
|
|
32341
|
+
const now = performance.now();
|
|
32342
|
+
if (now - lastDebugLog > 1e3) {
|
|
32343
|
+
const paintedTs = this.queue[0]?.timestamp ?? 0;
|
|
32344
|
+
const audioMs = (audioNowUs / 1e3).toFixed(1);
|
|
32345
|
+
const ptsMs = (paintedTs / 1e3).toFixed(1);
|
|
32346
|
+
const rawDriftMs = ((paintedTs - rawAudioNowUs) / 1e3).toFixed(1);
|
|
32347
|
+
const calibMs = (this.ptsCalibrationUs / 1e3).toFixed(1);
|
|
32348
|
+
console.log(
|
|
32349
|
+
`[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}`
|
|
32350
|
+
);
|
|
32351
|
+
lastDebugLog = now;
|
|
32352
|
+
}
|
|
32353
|
+
}
|
|
32354
|
+
const frame2 = this.queue.shift();
|
|
32355
|
+
this.paint(frame2);
|
|
32356
|
+
frame2.close();
|
|
32357
|
+
this.lastPaintWall = performance.now();
|
|
32358
|
+
return;
|
|
32168
32359
|
}
|
|
32360
|
+
const wallNow = performance.now();
|
|
32361
|
+
if (wallNow - this.lastPaintWall < this.paintIntervalMs - 2) return;
|
|
32169
32362
|
const frame = this.queue.shift();
|
|
32170
32363
|
this.paint(frame);
|
|
32171
32364
|
frame.close();
|
|
@@ -32187,8 +32380,13 @@ var VideoRenderer = class {
|
|
|
32187
32380
|
}
|
|
32188
32381
|
/** Discard all queued frames. Used by seek to drop stale buffers. */
|
|
32189
32382
|
flush() {
|
|
32383
|
+
const count = this.queue.length;
|
|
32190
32384
|
while (this.queue.length > 0) this.queue.shift()?.close();
|
|
32191
32385
|
this.prerolled = false;
|
|
32386
|
+
this.ptsCalibrated = false;
|
|
32387
|
+
if (isDebug() && count > 0) {
|
|
32388
|
+
console.log(`[avbridge:renderer] FLUSH discarded=${count} painted=${this.framesPainted} drops=${this.framesDroppedLate}`);
|
|
32389
|
+
}
|
|
32192
32390
|
}
|
|
32193
32391
|
stats() {
|
|
32194
32392
|
return {
|
|
@@ -32233,11 +32431,38 @@ var AudioOutput = class {
|
|
|
32233
32431
|
pendingQueue = [];
|
|
32234
32432
|
framesScheduled = 0;
|
|
32235
32433
|
destroyed = false;
|
|
32434
|
+
/** User-set volume (0..1). Applied to the gain node. */
|
|
32435
|
+
_volume = 1;
|
|
32436
|
+
/** User-set muted flag. When true, gain is forced to 0. */
|
|
32437
|
+
_muted = false;
|
|
32236
32438
|
constructor() {
|
|
32237
32439
|
this.ctx = new AudioContext();
|
|
32238
32440
|
this.gain = this.ctx.createGain();
|
|
32239
32441
|
this.gain.connect(this.ctx.destination);
|
|
32240
32442
|
}
|
|
32443
|
+
/** Set volume (0..1). Applied immediately to the gain node. */
|
|
32444
|
+
setVolume(v) {
|
|
32445
|
+
this._volume = Math.max(0, Math.min(1, v));
|
|
32446
|
+
this.applyGain();
|
|
32447
|
+
}
|
|
32448
|
+
getVolume() {
|
|
32449
|
+
return this._volume;
|
|
32450
|
+
}
|
|
32451
|
+
/** Set muted. When true, output is silenced regardless of volume. */
|
|
32452
|
+
setMuted(m) {
|
|
32453
|
+
this._muted = m;
|
|
32454
|
+
this.applyGain();
|
|
32455
|
+
}
|
|
32456
|
+
getMuted() {
|
|
32457
|
+
return this._muted;
|
|
32458
|
+
}
|
|
32459
|
+
applyGain() {
|
|
32460
|
+
const target = this._muted ? 0 : this._volume;
|
|
32461
|
+
try {
|
|
32462
|
+
this.gain.gain.value = target;
|
|
32463
|
+
} catch {
|
|
32464
|
+
}
|
|
32465
|
+
}
|
|
32241
32466
|
/**
|
|
32242
32467
|
* Switch into wall-clock fallback mode. Called by the decoder when no
|
|
32243
32468
|
* audio decoder could be initialized for the source. Once set, this
|
|
@@ -32308,9 +32533,13 @@ var AudioOutput = class {
|
|
|
32308
32533
|
const node3 = this.ctx.createBufferSource();
|
|
32309
32534
|
node3.buffer = buffer;
|
|
32310
32535
|
node3.connect(this.gain);
|
|
32311
|
-
|
|
32312
|
-
|
|
32313
|
-
|
|
32536
|
+
let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor);
|
|
32537
|
+
if (ctxStart < this.ctx.currentTime) {
|
|
32538
|
+
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
32539
|
+
this.mediaTimeOfAnchor = this.mediaTimeOfNext;
|
|
32540
|
+
ctxStart = this.ctx.currentTime;
|
|
32541
|
+
}
|
|
32542
|
+
node3.start(ctxStart);
|
|
32314
32543
|
this.mediaTimeOfNext += frameCount / sampleRate;
|
|
32315
32544
|
this.framesScheduled++;
|
|
32316
32545
|
}
|
|
@@ -32383,6 +32612,7 @@ var AudioOutput = class {
|
|
|
32383
32612
|
}
|
|
32384
32613
|
this.gain = this.ctx.createGain();
|
|
32385
32614
|
this.gain.connect(this.ctx.destination);
|
|
32615
|
+
this.applyGain();
|
|
32386
32616
|
this.pendingQueue = [];
|
|
32387
32617
|
this.mediaTimeOfAnchor = newMediaTime;
|
|
32388
32618
|
this.mediaTimeOfNext = newMediaTime;
|
|
@@ -32411,27 +32641,19 @@ var AudioOutput = class {
|
|
|
32411
32641
|
|
|
32412
32642
|
// src/strategies/hybrid/decoder.ts
|
|
32413
32643
|
init_libav_loader();
|
|
32644
|
+
init_debug();
|
|
32414
32645
|
|
|
32415
32646
|
// src/strategies/fallback/variant-routing.ts
|
|
32416
32647
|
var LEGACY_CONTAINERS = /* @__PURE__ */ new Set(["avi", "asf", "flv"]);
|
|
32417
|
-
var
|
|
32418
|
-
|
|
32419
|
-
"vc1",
|
|
32420
|
-
"mpeg4",
|
|
32421
|
-
// MPEG-4 Part 2 / DivX / Xvid
|
|
32422
|
-
"rv40",
|
|
32423
|
-
"mpeg2",
|
|
32424
|
-
"mpeg1",
|
|
32425
|
-
"theora"
|
|
32426
|
-
]);
|
|
32427
|
-
var LEGACY_AUDIO_CODECS = /* @__PURE__ */ new Set(["wmav2", "wmapro", "ac3", "eac3"]);
|
|
32648
|
+
var WEBCODECS_AUDIO = /* @__PURE__ */ new Set(["aac", "mp3", "opus", "vorbis", "flac"]);
|
|
32649
|
+
var WEBCODECS_VIDEO = /* @__PURE__ */ new Set(["h264", "h265", "vp8", "vp9", "av1"]);
|
|
32428
32650
|
function pickLibavVariant(ctx) {
|
|
32429
32651
|
if (LEGACY_CONTAINERS.has(ctx.container)) return "avbridge";
|
|
32430
32652
|
for (const v of ctx.videoTracks) {
|
|
32431
|
-
if (
|
|
32653
|
+
if (!WEBCODECS_VIDEO.has(v.codec)) return "avbridge";
|
|
32432
32654
|
}
|
|
32433
32655
|
for (const a of ctx.audioTracks) {
|
|
32434
|
-
if (
|
|
32656
|
+
if (!WEBCODECS_AUDIO.has(a.codec)) return "avbridge";
|
|
32435
32657
|
}
|
|
32436
32658
|
return "webcodecs";
|
|
32437
32659
|
}
|
|
@@ -32442,7 +32664,7 @@ async function startHybridDecoder(opts) {
|
|
|
32442
32664
|
const libav = await loadLibav(variant);
|
|
32443
32665
|
const bridge = await loadBridge();
|
|
32444
32666
|
const { prepareLibavInput: prepareLibavInput2 } = await Promise.resolve().then(() => (init_libav_http_reader(), libav_http_reader_exports));
|
|
32445
|
-
const inputHandle = await prepareLibavInput2(libav, opts.filename, opts.source);
|
|
32667
|
+
const inputHandle = await prepareLibavInput2(libav, opts.filename, opts.source, opts.transport);
|
|
32446
32668
|
const readPkt = await libav.av_packet_alloc();
|
|
32447
32669
|
const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
|
|
32448
32670
|
const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
|
|
@@ -32513,6 +32735,56 @@ async function startHybridDecoder(opts) {
|
|
|
32513
32735
|
});
|
|
32514
32736
|
throw new Error("hybrid decoder: could not initialize any decoders");
|
|
32515
32737
|
}
|
|
32738
|
+
let bsfCtx = null;
|
|
32739
|
+
let bsfPkt = null;
|
|
32740
|
+
if (videoStream && opts.context.videoTracks[0]?.codec === "mpeg4") {
|
|
32741
|
+
try {
|
|
32742
|
+
bsfCtx = await libav.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
|
|
32743
|
+
if (bsfCtx != null && bsfCtx >= 0) {
|
|
32744
|
+
const parIn = await libav.AVBSFContext_par_in(bsfCtx);
|
|
32745
|
+
await libav.avcodec_parameters_copy(parIn, videoStream.codecpar);
|
|
32746
|
+
await libav.av_bsf_init(bsfCtx);
|
|
32747
|
+
bsfPkt = await libav.av_packet_alloc();
|
|
32748
|
+
dbg.info("bsf", "mpeg4_unpack_bframes BSF active (hybrid)");
|
|
32749
|
+
} else {
|
|
32750
|
+
console.warn("[avbridge] mpeg4_unpack_bframes BSF not available in hybrid decoder");
|
|
32751
|
+
bsfCtx = null;
|
|
32752
|
+
}
|
|
32753
|
+
} catch (err) {
|
|
32754
|
+
console.warn("[avbridge] hybrid: failed to init BSF:", err.message);
|
|
32755
|
+
bsfCtx = null;
|
|
32756
|
+
bsfPkt = null;
|
|
32757
|
+
}
|
|
32758
|
+
}
|
|
32759
|
+
async function applyBSF(packets) {
|
|
32760
|
+
if (!bsfCtx || !bsfPkt) return packets;
|
|
32761
|
+
const out = [];
|
|
32762
|
+
for (const pkt of packets) {
|
|
32763
|
+
await libav.ff_copyin_packet(bsfPkt, pkt);
|
|
32764
|
+
const sendErr = await libav.av_bsf_send_packet(bsfCtx, bsfPkt);
|
|
32765
|
+
if (sendErr < 0) {
|
|
32766
|
+
out.push(pkt);
|
|
32767
|
+
continue;
|
|
32768
|
+
}
|
|
32769
|
+
while (true) {
|
|
32770
|
+
const recvErr = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
|
|
32771
|
+
if (recvErr < 0) break;
|
|
32772
|
+
out.push(await libav.ff_copyout_packet(bsfPkt));
|
|
32773
|
+
}
|
|
32774
|
+
}
|
|
32775
|
+
return out;
|
|
32776
|
+
}
|
|
32777
|
+
async function flushBSF() {
|
|
32778
|
+
if (!bsfCtx || !bsfPkt) return;
|
|
32779
|
+
try {
|
|
32780
|
+
await libav.av_bsf_send_packet(bsfCtx, 0);
|
|
32781
|
+
while (true) {
|
|
32782
|
+
const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
|
|
32783
|
+
if (err < 0) break;
|
|
32784
|
+
}
|
|
32785
|
+
} catch {
|
|
32786
|
+
}
|
|
32787
|
+
}
|
|
32516
32788
|
let destroyed = false;
|
|
32517
32789
|
let pumpToken = 0;
|
|
32518
32790
|
let pumpRunning = null;
|
|
@@ -32540,8 +32812,15 @@ async function startHybridDecoder(opts) {
|
|
|
32540
32812
|
if (myToken !== pumpToken || destroyed) return;
|
|
32541
32813
|
const videoPackets = videoStream ? packets[videoStream.index] : void 0;
|
|
32542
32814
|
const audioPackets = audioStream ? packets[audioStream.index] : void 0;
|
|
32815
|
+
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
32816
|
+
await decodeAudioBatch(audioPackets, myToken);
|
|
32817
|
+
}
|
|
32818
|
+
if (myToken !== pumpToken || destroyed) return;
|
|
32819
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
32820
|
+
if (myToken !== pumpToken || destroyed) return;
|
|
32543
32821
|
if (videoDecoder && videoPackets && videoPackets.length > 0) {
|
|
32544
|
-
|
|
32822
|
+
const processed = await applyBSF(videoPackets);
|
|
32823
|
+
for (const pkt of processed) {
|
|
32545
32824
|
if (myToken !== pumpToken || destroyed) return;
|
|
32546
32825
|
sanitizePacketTimestamp(pkt, () => {
|
|
32547
32826
|
const ts = syntheticVideoUs;
|
|
@@ -32561,9 +32840,6 @@ async function startHybridDecoder(opts) {
|
|
|
32561
32840
|
}
|
|
32562
32841
|
}
|
|
32563
32842
|
}
|
|
32564
|
-
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
32565
|
-
await decodeAudioBatch(audioPackets, myToken);
|
|
32566
|
-
}
|
|
32567
32843
|
packetsRead += (videoPackets?.length ?? 0) + (audioPackets?.length ?? 0);
|
|
32568
32844
|
while (!destroyed && myToken === pumpToken && (videoDecoder && videoDecoder.decodeQueueSize > 10 || opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
|
|
32569
32845
|
await new Promise((r) => setTimeout(r, 50));
|
|
@@ -32586,20 +32862,43 @@ async function startHybridDecoder(opts) {
|
|
|
32586
32862
|
}
|
|
32587
32863
|
async function decodeAudioBatch(pkts, myToken, flush = false) {
|
|
32588
32864
|
if (!audioDec || destroyed || myToken !== pumpToken) return;
|
|
32589
|
-
|
|
32590
|
-
|
|
32591
|
-
|
|
32592
|
-
|
|
32593
|
-
|
|
32594
|
-
|
|
32595
|
-
|
|
32596
|
-
|
|
32597
|
-
|
|
32598
|
-
|
|
32599
|
-
|
|
32600
|
-
|
|
32865
|
+
const AUDIO_SUB_BATCH = 4;
|
|
32866
|
+
let allFrames = [];
|
|
32867
|
+
for (let i = 0; i < pkts.length; i += AUDIO_SUB_BATCH) {
|
|
32868
|
+
if (myToken !== pumpToken || destroyed) return;
|
|
32869
|
+
const slice = pkts.slice(i, i + AUDIO_SUB_BATCH);
|
|
32870
|
+
const isLast = i + AUDIO_SUB_BATCH >= pkts.length;
|
|
32871
|
+
try {
|
|
32872
|
+
const frames2 = await libav.ff_decode_multi(
|
|
32873
|
+
audioDec.c,
|
|
32874
|
+
audioDec.pkt,
|
|
32875
|
+
audioDec.frame,
|
|
32876
|
+
slice,
|
|
32877
|
+
isLast && flush ? { fin: true, ignoreErrors: true } : { ignoreErrors: true }
|
|
32878
|
+
);
|
|
32879
|
+
allFrames = allFrames.concat(frames2);
|
|
32880
|
+
} catch (err) {
|
|
32881
|
+
console.error("[avbridge] hybrid audio decode failed:", err);
|
|
32882
|
+
return;
|
|
32883
|
+
}
|
|
32884
|
+
if (!isLast) await new Promise((r) => setTimeout(r, 0));
|
|
32885
|
+
}
|
|
32886
|
+
if (pkts.length === 0 && flush) {
|
|
32887
|
+
try {
|
|
32888
|
+
allFrames = await libav.ff_decode_multi(
|
|
32889
|
+
audioDec.c,
|
|
32890
|
+
audioDec.pkt,
|
|
32891
|
+
audioDec.frame,
|
|
32892
|
+
[],
|
|
32893
|
+
{ fin: true, ignoreErrors: true }
|
|
32894
|
+
);
|
|
32895
|
+
} catch (err) {
|
|
32896
|
+
console.error("[avbridge] hybrid audio flush failed:", err);
|
|
32897
|
+
return;
|
|
32898
|
+
}
|
|
32601
32899
|
}
|
|
32602
32900
|
if (myToken !== pumpToken || destroyed) return;
|
|
32901
|
+
const frames = allFrames;
|
|
32603
32902
|
for (const f of frames) {
|
|
32604
32903
|
if (myToken !== pumpToken || destroyed) return;
|
|
32605
32904
|
sanitizeFrameTimestamp(
|
|
@@ -32636,6 +32935,14 @@ async function startHybridDecoder(opts) {
|
|
|
32636
32935
|
await pumpRunning;
|
|
32637
32936
|
} catch {
|
|
32638
32937
|
}
|
|
32938
|
+
try {
|
|
32939
|
+
if (bsfCtx) await libav.av_bsf_free(bsfCtx);
|
|
32940
|
+
} catch {
|
|
32941
|
+
}
|
|
32942
|
+
try {
|
|
32943
|
+
if (bsfPkt) await libav.av_packet_free?.(bsfPkt);
|
|
32944
|
+
} catch {
|
|
32945
|
+
}
|
|
32639
32946
|
try {
|
|
32640
32947
|
if (videoDecoder && videoDecoder.state !== "closed") videoDecoder.close();
|
|
32641
32948
|
} catch {
|
|
@@ -32689,6 +32996,7 @@ async function startHybridDecoder(opts) {
|
|
|
32689
32996
|
if (audioDec) await libav.avcodec_flush_buffers?.(audioDec.c);
|
|
32690
32997
|
} catch {
|
|
32691
32998
|
}
|
|
32999
|
+
await flushBSF();
|
|
32692
33000
|
syntheticVideoUs = Math.round(timeSec * 1e6);
|
|
32693
33001
|
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
32694
33002
|
pumpRunning = pumpLoop(newToken).catch(
|
|
@@ -32702,6 +33010,7 @@ async function startHybridDecoder(opts) {
|
|
|
32702
33010
|
videoFramesDecoded,
|
|
32703
33011
|
videoChunksFed,
|
|
32704
33012
|
audioFramesDecoded,
|
|
33013
|
+
bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
|
|
32705
33014
|
videoDecodeQueueSize: videoDecoder?.decodeQueueSize ?? 0,
|
|
32706
33015
|
// Confirmed transport info — see fallback decoder for the pattern.
|
|
32707
33016
|
_transport: inputHandle.transport === "http-range" ? "http-range" : "memory",
|
|
@@ -32878,7 +33187,7 @@ async function loadBridge() {
|
|
|
32878
33187
|
// src/strategies/hybrid/index.ts
|
|
32879
33188
|
var READY_AUDIO_BUFFER_SECONDS = 0.3;
|
|
32880
33189
|
var READY_TIMEOUT_SECONDS = 10;
|
|
32881
|
-
async function createHybridSession(ctx, target) {
|
|
33190
|
+
async function createHybridSession(ctx, target, transport) {
|
|
32882
33191
|
const { normalizeSource: normalizeSource2 } = await Promise.resolve().then(() => (init_source(), source_exports));
|
|
32883
33192
|
const source = await normalizeSource2(ctx.source);
|
|
32884
33193
|
const fps = ctx.videoTracks[0]?.fps ?? 30;
|
|
@@ -32891,7 +33200,8 @@ async function createHybridSession(ctx, target) {
|
|
|
32891
33200
|
filename: ctx.name ?? "input.bin",
|
|
32892
33201
|
context: ctx,
|
|
32893
33202
|
renderer,
|
|
32894
|
-
audio
|
|
33203
|
+
audio,
|
|
33204
|
+
transport
|
|
32895
33205
|
});
|
|
32896
33206
|
} catch (err) {
|
|
32897
33207
|
audio.destroy();
|
|
@@ -32905,6 +33215,26 @@ async function createHybridSession(ctx, target) {
|
|
|
32905
33215
|
void doSeek(v);
|
|
32906
33216
|
}
|
|
32907
33217
|
});
|
|
33218
|
+
Object.defineProperty(target, "paused", {
|
|
33219
|
+
configurable: true,
|
|
33220
|
+
get: () => !audio.isPlaying()
|
|
33221
|
+
});
|
|
33222
|
+
Object.defineProperty(target, "volume", {
|
|
33223
|
+
configurable: true,
|
|
33224
|
+
get: () => audio.getVolume(),
|
|
33225
|
+
set: (v) => {
|
|
33226
|
+
audio.setVolume(v);
|
|
33227
|
+
target.dispatchEvent(new Event("volumechange"));
|
|
33228
|
+
}
|
|
33229
|
+
});
|
|
33230
|
+
Object.defineProperty(target, "muted", {
|
|
33231
|
+
configurable: true,
|
|
33232
|
+
get: () => audio.getMuted(),
|
|
33233
|
+
set: (m) => {
|
|
33234
|
+
audio.setMuted(m);
|
|
33235
|
+
target.dispatchEvent(new Event("volumechange"));
|
|
33236
|
+
}
|
|
33237
|
+
});
|
|
32908
33238
|
if (ctx.duration && Number.isFinite(ctx.duration)) {
|
|
32909
33239
|
Object.defineProperty(target, "duration", {
|
|
32910
33240
|
configurable: true,
|
|
@@ -32944,10 +33274,13 @@ async function createHybridSession(ctx, target) {
|
|
|
32944
33274
|
if (!audio.isPlaying()) {
|
|
32945
33275
|
await waitForBuffer();
|
|
32946
33276
|
await audio.start();
|
|
33277
|
+
target.dispatchEvent(new Event("play"));
|
|
33278
|
+
target.dispatchEvent(new Event("playing"));
|
|
32947
33279
|
}
|
|
32948
33280
|
},
|
|
32949
33281
|
pause() {
|
|
32950
33282
|
void audio.pause();
|
|
33283
|
+
target.dispatchEvent(new Event("pause"));
|
|
32951
33284
|
},
|
|
32952
33285
|
async seek(time) {
|
|
32953
33286
|
await doSeek(time);
|
|
@@ -32969,6 +33302,9 @@ async function createHybridSession(ctx, target) {
|
|
|
32969
33302
|
try {
|
|
32970
33303
|
delete target.currentTime;
|
|
32971
33304
|
delete target.duration;
|
|
33305
|
+
delete target.paused;
|
|
33306
|
+
delete target.volume;
|
|
33307
|
+
delete target.muted;
|
|
32972
33308
|
} catch {
|
|
32973
33309
|
}
|
|
32974
33310
|
},
|
|
@@ -32980,12 +33316,13 @@ async function createHybridSession(ctx, target) {
|
|
|
32980
33316
|
|
|
32981
33317
|
// src/strategies/fallback/decoder.ts
|
|
32982
33318
|
init_libav_loader();
|
|
33319
|
+
init_debug();
|
|
32983
33320
|
async function startDecoder(opts) {
|
|
32984
33321
|
const variant = pickLibavVariant(opts.context);
|
|
32985
33322
|
const libav = await loadLibav(variant);
|
|
32986
33323
|
const bridge = await loadBridge2();
|
|
32987
33324
|
const { prepareLibavInput: prepareLibavInput2 } = await Promise.resolve().then(() => (init_libav_http_reader(), libav_http_reader_exports));
|
|
32988
|
-
const inputHandle = await prepareLibavInput2(libav, opts.filename, opts.source);
|
|
33325
|
+
const inputHandle = await prepareLibavInput2(libav, opts.filename, opts.source, opts.transport);
|
|
32989
33326
|
const readPkt = await libav.av_packet_alloc();
|
|
32990
33327
|
const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
|
|
32991
33328
|
const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
|
|
@@ -33041,6 +33378,56 @@ async function startDecoder(opts) {
|
|
|
33041
33378
|
`fallback decoder: could not initialize any libav decoders (${codecs}).${hint}`
|
|
33042
33379
|
);
|
|
33043
33380
|
}
|
|
33381
|
+
let bsfCtx = null;
|
|
33382
|
+
let bsfPkt = null;
|
|
33383
|
+
if (videoStream && opts.context.videoTracks[0]?.codec === "mpeg4") {
|
|
33384
|
+
try {
|
|
33385
|
+
bsfCtx = await libav.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
|
|
33386
|
+
if (bsfCtx != null && bsfCtx >= 0) {
|
|
33387
|
+
const parIn = await libav.AVBSFContext_par_in(bsfCtx);
|
|
33388
|
+
await libav.avcodec_parameters_copy(parIn, videoStream.codecpar);
|
|
33389
|
+
await libav.av_bsf_init(bsfCtx);
|
|
33390
|
+
bsfPkt = await libav.av_packet_alloc();
|
|
33391
|
+
dbg.info("bsf", "mpeg4_unpack_bframes BSF active");
|
|
33392
|
+
} else {
|
|
33393
|
+
console.warn("[avbridge] mpeg4_unpack_bframes BSF not available \u2014 decoding without it");
|
|
33394
|
+
bsfCtx = null;
|
|
33395
|
+
}
|
|
33396
|
+
} catch (err) {
|
|
33397
|
+
console.warn("[avbridge] failed to init mpeg4_unpack_bframes BSF:", err.message);
|
|
33398
|
+
bsfCtx = null;
|
|
33399
|
+
bsfPkt = null;
|
|
33400
|
+
}
|
|
33401
|
+
}
|
|
33402
|
+
async function applyBSF(packets) {
|
|
33403
|
+
if (!bsfCtx || !bsfPkt) return packets;
|
|
33404
|
+
const out = [];
|
|
33405
|
+
for (const pkt of packets) {
|
|
33406
|
+
await libav.ff_copyin_packet(bsfPkt, pkt);
|
|
33407
|
+
const sendErr = await libav.av_bsf_send_packet(bsfCtx, bsfPkt);
|
|
33408
|
+
if (sendErr < 0) {
|
|
33409
|
+
out.push(pkt);
|
|
33410
|
+
continue;
|
|
33411
|
+
}
|
|
33412
|
+
while (true) {
|
|
33413
|
+
const recvErr = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
|
|
33414
|
+
if (recvErr < 0) break;
|
|
33415
|
+
out.push(await libav.ff_copyout_packet(bsfPkt));
|
|
33416
|
+
}
|
|
33417
|
+
}
|
|
33418
|
+
return out;
|
|
33419
|
+
}
|
|
33420
|
+
async function flushBSF() {
|
|
33421
|
+
if (!bsfCtx || !bsfPkt) return;
|
|
33422
|
+
try {
|
|
33423
|
+
await libav.av_bsf_send_packet(bsfCtx, 0);
|
|
33424
|
+
while (true) {
|
|
33425
|
+
const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
|
|
33426
|
+
if (err < 0) break;
|
|
33427
|
+
}
|
|
33428
|
+
} catch {
|
|
33429
|
+
}
|
|
33430
|
+
}
|
|
33044
33431
|
let destroyed = false;
|
|
33045
33432
|
let pumpToken = 0;
|
|
33046
33433
|
let pumpRunning = null;
|
|
@@ -33049,7 +33436,8 @@ async function startDecoder(opts) {
|
|
|
33049
33436
|
let audioFramesDecoded = 0;
|
|
33050
33437
|
let watchdogFirstFrameMs = 0;
|
|
33051
33438
|
let watchdogSlowSinceMs = 0;
|
|
33052
|
-
let
|
|
33439
|
+
let watchdogSlowWarned = false;
|
|
33440
|
+
let watchdogOverflowWarned = false;
|
|
33053
33441
|
let syntheticVideoUs = 0;
|
|
33054
33442
|
let syntheticAudioUs = 0;
|
|
33055
33443
|
const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
|
|
@@ -33061,7 +33449,7 @@ async function startDecoder(opts) {
|
|
|
33061
33449
|
let packets;
|
|
33062
33450
|
try {
|
|
33063
33451
|
[readErr, packets] = await libav.ff_read_frame_multi(fmt_ctx, readPkt, {
|
|
33064
|
-
limit:
|
|
33452
|
+
limit: 16 * 1024
|
|
33065
33453
|
});
|
|
33066
33454
|
} catch (err) {
|
|
33067
33455
|
console.error("[avbridge] ff_read_frame_multi failed:", err);
|
|
@@ -33070,26 +33458,27 @@ async function startDecoder(opts) {
|
|
|
33070
33458
|
if (myToken !== pumpToken || destroyed) return;
|
|
33071
33459
|
const videoPackets = videoStream ? packets[videoStream.index] : void 0;
|
|
33072
33460
|
const audioPackets = audioStream ? packets[audioStream.index] : void 0;
|
|
33073
|
-
if (videoDec && videoPackets && videoPackets.length > 0) {
|
|
33074
|
-
await decodeVideoBatch(videoPackets, myToken);
|
|
33075
|
-
}
|
|
33076
|
-
if (myToken !== pumpToken || destroyed) return;
|
|
33077
33461
|
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
33078
33462
|
await decodeAudioBatch(audioPackets, myToken);
|
|
33079
33463
|
}
|
|
33464
|
+
if (myToken !== pumpToken || destroyed) return;
|
|
33465
|
+
if (videoDec && videoPackets && videoPackets.length > 0) {
|
|
33466
|
+
const processed = await applyBSF(videoPackets);
|
|
33467
|
+
await decodeVideoBatch(processed, myToken);
|
|
33468
|
+
}
|
|
33080
33469
|
packetsRead += (videoPackets?.length ?? 0) + (audioPackets?.length ?? 0);
|
|
33081
33470
|
if (videoFramesDecoded > 0) {
|
|
33082
33471
|
if (watchdogFirstFrameMs === 0) {
|
|
33083
33472
|
watchdogFirstFrameMs = performance.now();
|
|
33084
33473
|
}
|
|
33085
33474
|
const elapsedSinceFirst = (performance.now() - watchdogFirstFrameMs) / 1e3;
|
|
33086
|
-
if (elapsedSinceFirst > 1 && !
|
|
33475
|
+
if (elapsedSinceFirst > 1 && !watchdogSlowWarned) {
|
|
33087
33476
|
const expectedFrames = elapsedSinceFirst * videoFps;
|
|
33088
33477
|
const ratio = videoFramesDecoded / expectedFrames;
|
|
33089
33478
|
if (ratio < 0.6) {
|
|
33090
33479
|
if (watchdogSlowSinceMs === 0) watchdogSlowSinceMs = performance.now();
|
|
33091
33480
|
if ((performance.now() - watchdogSlowSinceMs) / 1e3 > 5) {
|
|
33092
|
-
|
|
33481
|
+
watchdogSlowWarned = true;
|
|
33093
33482
|
console.warn(
|
|
33094
33483
|
"[avbridge:decode-rate]",
|
|
33095
33484
|
`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.`
|
|
@@ -33099,6 +33488,17 @@ async function startDecoder(opts) {
|
|
|
33099
33488
|
watchdogSlowSinceMs = 0;
|
|
33100
33489
|
}
|
|
33101
33490
|
}
|
|
33491
|
+
if (!watchdogOverflowWarned && videoFramesDecoded > 100) {
|
|
33492
|
+
const rendererStats = opts.renderer.stats();
|
|
33493
|
+
const overflow = rendererStats.framesDroppedOverflow ?? 0;
|
|
33494
|
+
if (overflow / videoFramesDecoded > 0.1) {
|
|
33495
|
+
watchdogOverflowWarned = true;
|
|
33496
|
+
console.warn(
|
|
33497
|
+
"[avbridge:overflow-drop]",
|
|
33498
|
+
`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.`
|
|
33499
|
+
);
|
|
33500
|
+
}
|
|
33501
|
+
}
|
|
33102
33502
|
}
|
|
33103
33503
|
while (!destroyed && myToken === pumpToken && (opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
|
|
33104
33504
|
await new Promise((r) => setTimeout(r, 50));
|
|
@@ -33210,6 +33610,14 @@ async function startDecoder(opts) {
|
|
|
33210
33610
|
await pumpRunning;
|
|
33211
33611
|
} catch {
|
|
33212
33612
|
}
|
|
33613
|
+
try {
|
|
33614
|
+
if (bsfCtx) await libav.av_bsf_free(bsfCtx);
|
|
33615
|
+
} catch {
|
|
33616
|
+
}
|
|
33617
|
+
try {
|
|
33618
|
+
if (bsfPkt) await libav.av_packet_free?.(bsfPkt);
|
|
33619
|
+
} catch {
|
|
33620
|
+
}
|
|
33213
33621
|
try {
|
|
33214
33622
|
if (videoDec) await libav.ff_free_decoder?.(videoDec.c, videoDec.pkt, videoDec.frame);
|
|
33215
33623
|
} catch {
|
|
@@ -33261,6 +33669,7 @@ async function startDecoder(opts) {
|
|
|
33261
33669
|
if (audioDec) await libav.avcodec_flush_buffers?.(audioDec.c);
|
|
33262
33670
|
} catch {
|
|
33263
33671
|
}
|
|
33672
|
+
await flushBSF();
|
|
33264
33673
|
syntheticVideoUs = Math.round(timeSec * 1e6);
|
|
33265
33674
|
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
33266
33675
|
pumpRunning = pumpLoop(newToken).catch(
|
|
@@ -33273,6 +33682,7 @@ async function startDecoder(opts) {
|
|
|
33273
33682
|
packetsRead,
|
|
33274
33683
|
videoFramesDecoded,
|
|
33275
33684
|
audioFramesDecoded,
|
|
33685
|
+
bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
|
|
33276
33686
|
// Confirmed transport info: once prepareLibavInput returns
|
|
33277
33687
|
// successfully, we *know* whether the source is http-range (probe
|
|
33278
33688
|
// succeeded and returned 206) or in-memory blob. Diagnostics hoists
|
|
@@ -33429,7 +33839,7 @@ async function loadBridge2() {
|
|
|
33429
33839
|
init_debug();
|
|
33430
33840
|
var READY_AUDIO_BUFFER_SECONDS2 = 0.04;
|
|
33431
33841
|
var READY_TIMEOUT_SECONDS2 = 3;
|
|
33432
|
-
async function createFallbackSession(ctx, target) {
|
|
33842
|
+
async function createFallbackSession(ctx, target, transport) {
|
|
33433
33843
|
const { normalizeSource: normalizeSource2 } = await Promise.resolve().then(() => (init_source(), source_exports));
|
|
33434
33844
|
const source = await normalizeSource2(ctx.source);
|
|
33435
33845
|
const fps = ctx.videoTracks[0]?.fps ?? 30;
|
|
@@ -33442,7 +33852,8 @@ async function createFallbackSession(ctx, target) {
|
|
|
33442
33852
|
filename: ctx.name ?? "input.bin",
|
|
33443
33853
|
context: ctx,
|
|
33444
33854
|
renderer,
|
|
33445
|
-
audio
|
|
33855
|
+
audio,
|
|
33856
|
+
transport
|
|
33446
33857
|
});
|
|
33447
33858
|
} catch (err) {
|
|
33448
33859
|
audio.destroy();
|
|
@@ -33456,6 +33867,26 @@ async function createFallbackSession(ctx, target) {
|
|
|
33456
33867
|
void doSeek(v);
|
|
33457
33868
|
}
|
|
33458
33869
|
});
|
|
33870
|
+
Object.defineProperty(target, "paused", {
|
|
33871
|
+
configurable: true,
|
|
33872
|
+
get: () => !audio.isPlaying()
|
|
33873
|
+
});
|
|
33874
|
+
Object.defineProperty(target, "volume", {
|
|
33875
|
+
configurable: true,
|
|
33876
|
+
get: () => audio.getVolume(),
|
|
33877
|
+
set: (v) => {
|
|
33878
|
+
audio.setVolume(v);
|
|
33879
|
+
target.dispatchEvent(new Event("volumechange"));
|
|
33880
|
+
}
|
|
33881
|
+
});
|
|
33882
|
+
Object.defineProperty(target, "muted", {
|
|
33883
|
+
configurable: true,
|
|
33884
|
+
get: () => audio.getMuted(),
|
|
33885
|
+
set: (m) => {
|
|
33886
|
+
audio.setMuted(m);
|
|
33887
|
+
target.dispatchEvent(new Event("volumechange"));
|
|
33888
|
+
}
|
|
33889
|
+
});
|
|
33459
33890
|
if (ctx.duration && Number.isFinite(ctx.duration)) {
|
|
33460
33891
|
Object.defineProperty(target, "duration", {
|
|
33461
33892
|
configurable: true,
|
|
@@ -33464,25 +33895,35 @@ async function createFallbackSession(ctx, target) {
|
|
|
33464
33895
|
}
|
|
33465
33896
|
async function waitForBuffer() {
|
|
33466
33897
|
const start = performance.now();
|
|
33898
|
+
let firstFrameAtMs = 0;
|
|
33467
33899
|
dbg.info(
|
|
33468
33900
|
"cold-start",
|
|
33469
|
-
`gate entry:
|
|
33901
|
+
`gate entry: want audio \u2265 ${READY_AUDIO_BUFFER_SECONDS2 * 1e3}ms + 1 frame`
|
|
33470
33902
|
);
|
|
33471
33903
|
while (true) {
|
|
33472
33904
|
const audioAhead = audio.isNoAudio() ? Infinity : audio.bufferAhead();
|
|
33473
33905
|
const audioReady = audio.isNoAudio() || audioAhead >= READY_AUDIO_BUFFER_SECONDS2;
|
|
33474
33906
|
const hasFrames = renderer.hasFrames();
|
|
33907
|
+
const nowMs = performance.now();
|
|
33908
|
+
if (hasFrames && firstFrameAtMs === 0) firstFrameAtMs = nowMs;
|
|
33475
33909
|
if (audioReady && hasFrames) {
|
|
33476
33910
|
dbg.info(
|
|
33477
33911
|
"cold-start",
|
|
33478
|
-
`gate satisfied in ${(
|
|
33912
|
+
`gate satisfied in ${(nowMs - start).toFixed(0)}ms (audio=${(audioAhead * 1e3).toFixed(0)}ms, frames=${renderer.queueDepth()})`
|
|
33479
33913
|
);
|
|
33480
33914
|
return;
|
|
33481
33915
|
}
|
|
33482
|
-
if (
|
|
33916
|
+
if (hasFrames && firstFrameAtMs > 0 && nowMs - firstFrameAtMs >= 500) {
|
|
33917
|
+
dbg.info(
|
|
33918
|
+
"cold-start",
|
|
33919
|
+
`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)`
|
|
33920
|
+
);
|
|
33921
|
+
return;
|
|
33922
|
+
}
|
|
33923
|
+
if ((nowMs - start) / 1e3 > READY_TIMEOUT_SECONDS2) {
|
|
33483
33924
|
dbg.diag(
|
|
33484
33925
|
"cold-start",
|
|
33485
|
-
`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).
|
|
33926
|
+
`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.`
|
|
33486
33927
|
);
|
|
33487
33928
|
return;
|
|
33488
33929
|
}
|
|
@@ -33509,10 +33950,13 @@ async function createFallbackSession(ctx, target) {
|
|
|
33509
33950
|
if (!audio.isPlaying()) {
|
|
33510
33951
|
await waitForBuffer();
|
|
33511
33952
|
await audio.start();
|
|
33953
|
+
target.dispatchEvent(new Event("play"));
|
|
33954
|
+
target.dispatchEvent(new Event("playing"));
|
|
33512
33955
|
}
|
|
33513
33956
|
},
|
|
33514
33957
|
pause() {
|
|
33515
33958
|
void audio.pause();
|
|
33959
|
+
target.dispatchEvent(new Event("pause"));
|
|
33516
33960
|
},
|
|
33517
33961
|
async seek(time) {
|
|
33518
33962
|
await doSeek(time);
|
|
@@ -33531,6 +33975,9 @@ async function createFallbackSession(ctx, target) {
|
|
|
33531
33975
|
try {
|
|
33532
33976
|
delete target.currentTime;
|
|
33533
33977
|
delete target.duration;
|
|
33978
|
+
delete target.paused;
|
|
33979
|
+
delete target.volume;
|
|
33980
|
+
delete target.muted;
|
|
33534
33981
|
} catch {
|
|
33535
33982
|
}
|
|
33536
33983
|
},
|
|
@@ -33554,12 +34001,12 @@ var remuxPlugin = {
|
|
|
33554
34001
|
var hybridPlugin = {
|
|
33555
34002
|
name: "hybrid",
|
|
33556
34003
|
canHandle: () => typeof VideoDecoder !== "undefined",
|
|
33557
|
-
execute: (ctx, video) => createHybridSession(ctx, video)
|
|
34004
|
+
execute: (ctx, video, transport) => createHybridSession(ctx, video, transport)
|
|
33558
34005
|
};
|
|
33559
34006
|
var fallbackPlugin = {
|
|
33560
34007
|
name: "fallback",
|
|
33561
34008
|
canHandle: () => true,
|
|
33562
|
-
execute: (ctx, video) => createFallbackSession(ctx, video)
|
|
34009
|
+
execute: (ctx, video, transport) => createFallbackSession(ctx, video, transport)
|
|
33563
34010
|
};
|
|
33564
34011
|
function registerBuiltins(registry) {
|
|
33565
34012
|
registry.register(nativePlugin);
|
|
@@ -33568,6 +34015,9 @@ function registerBuiltins(registry) {
|
|
|
33568
34015
|
registry.register(fallbackPlugin);
|
|
33569
34016
|
}
|
|
33570
34017
|
|
|
34018
|
+
// src/subtitles/index.ts
|
|
34019
|
+
init_transport();
|
|
34020
|
+
|
|
33571
34021
|
// src/subtitles/srt.ts
|
|
33572
34022
|
function srtToVtt(srt) {
|
|
33573
34023
|
if (srt.charCodeAt(0) === 65279) srt = srt.slice(1);
|
|
@@ -33645,7 +34095,8 @@ var SubtitleResourceBag = class {
|
|
|
33645
34095
|
this.urls.clear();
|
|
33646
34096
|
}
|
|
33647
34097
|
};
|
|
33648
|
-
async function attachSubtitleTracks(video, tracks, bag, onError) {
|
|
34098
|
+
async function attachSubtitleTracks(video, tracks, bag, onError, transport) {
|
|
34099
|
+
const doFetch = fetchWith(transport);
|
|
33649
34100
|
for (const t of Array.from(video.querySelectorAll("track[data-avbridge]"))) {
|
|
33650
34101
|
t.remove();
|
|
33651
34102
|
}
|
|
@@ -33654,13 +34105,13 @@ async function attachSubtitleTracks(video, tracks, bag, onError) {
|
|
|
33654
34105
|
try {
|
|
33655
34106
|
let url2 = t.sidecarUrl;
|
|
33656
34107
|
if (t.format === "srt") {
|
|
33657
|
-
const res = await
|
|
34108
|
+
const res = await doFetch(t.sidecarUrl, transport?.requestInit);
|
|
33658
34109
|
const text = await res.text();
|
|
33659
34110
|
const vtt = srtToVtt(text);
|
|
33660
34111
|
const blob = new Blob([vtt], { type: "text/vtt" });
|
|
33661
34112
|
url2 = bag ? bag.createObjectURL(blob) : URL.createObjectURL(blob);
|
|
33662
34113
|
} else if (t.format === "vtt") {
|
|
33663
|
-
const res = await
|
|
34114
|
+
const res = await doFetch(t.sidecarUrl, transport?.requestInit);
|
|
33664
34115
|
const text = await res.text();
|
|
33665
34116
|
if (!isVtt(text)) {
|
|
33666
34117
|
console.warn("[avbridge] subtitle missing WEBVTT header:", t.sidecarUrl);
|
|
@@ -33682,6 +34133,7 @@ async function attachSubtitleTracks(video, tracks, bag, onError) {
|
|
|
33682
34133
|
|
|
33683
34134
|
// src/player.ts
|
|
33684
34135
|
init_debug();
|
|
34136
|
+
init_errors();
|
|
33685
34137
|
var UnifiedPlayer = class _UnifiedPlayer {
|
|
33686
34138
|
/**
|
|
33687
34139
|
* @internal Use {@link createPlayer} or {@link UnifiedPlayer.create} instead.
|
|
@@ -33689,6 +34141,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
33689
34141
|
constructor(options, registry) {
|
|
33690
34142
|
this.options = options;
|
|
33691
34143
|
this.registry = registry;
|
|
34144
|
+
const { requestInit, fetchFn } = options;
|
|
34145
|
+
if (requestInit || fetchFn) {
|
|
34146
|
+
this.transport = { requestInit, fetchFn };
|
|
34147
|
+
}
|
|
33692
34148
|
}
|
|
33693
34149
|
options;
|
|
33694
34150
|
registry;
|
|
@@ -33704,11 +34160,27 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
33704
34160
|
lastProgressTime = 0;
|
|
33705
34161
|
lastProgressPosition = -1;
|
|
33706
34162
|
errorListener = null;
|
|
34163
|
+
// Bound so we can removeEventListener in destroy(); without this the
|
|
34164
|
+
// listener outlives the player and accumulates on elements that swap
|
|
34165
|
+
// source (e.g. <avbridge-video>).
|
|
34166
|
+
endedListener = null;
|
|
34167
|
+
// Background tab handling. userIntent is what the user last asked for
|
|
34168
|
+
// (play vs pause) — used to decide whether to auto-resume on visibility
|
|
34169
|
+
// return. autoPausedForVisibility tracks whether we paused because the
|
|
34170
|
+
// tab was hidden, so we don't resume playback the user deliberately
|
|
34171
|
+
// paused (e.g. via media keys while hidden).
|
|
34172
|
+
userIntent = "pause";
|
|
34173
|
+
autoPausedForVisibility = false;
|
|
34174
|
+
visibilityListener = null;
|
|
33707
34175
|
// Serializes escalation / setStrategy calls
|
|
33708
34176
|
switchingPromise = Promise.resolve();
|
|
33709
34177
|
// Owns blob URLs created during sidecar discovery + SRT->VTT conversion.
|
|
33710
34178
|
// Revoked at destroy() so repeated source swaps don't leak.
|
|
33711
34179
|
subtitleResources = new SubtitleResourceBag();
|
|
34180
|
+
// Transport config extracted from CreatePlayerOptions. Threaded to probe,
|
|
34181
|
+
// subtitle fetches, and strategy session creators. Not stored on MediaContext
|
|
34182
|
+
// because it's runtime config, not media analysis.
|
|
34183
|
+
transport;
|
|
33712
34184
|
static async create(options) {
|
|
33713
34185
|
const registry = new PluginRegistry();
|
|
33714
34186
|
registerBuiltins(registry);
|
|
@@ -33732,7 +34204,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
33732
34204
|
const bootstrapStart = performance.now();
|
|
33733
34205
|
try {
|
|
33734
34206
|
dbg.info("bootstrap", "start");
|
|
33735
|
-
const ctx = await dbg.timed("probe", "probe", 3e3, () => probe(this.options.source));
|
|
34207
|
+
const ctx = await dbg.timed("probe", "probe", 3e3, () => probe(this.options.source, this.transport));
|
|
33736
34208
|
dbg.info(
|
|
33737
34209
|
"probe",
|
|
33738
34210
|
`container=${ctx.container} video=${ctx.videoTracks[0]?.codec ?? "-"} audio=${ctx.audioTracks[0]?.codec ?? "-"} probedBy=${ctx.probedBy}`
|
|
@@ -33780,7 +34252,8 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
33780
34252
|
this.subtitleResources,
|
|
33781
34253
|
(err, track) => {
|
|
33782
34254
|
console.warn(`[avbridge] subtitle ${track.id} failed: ${err.message}`);
|
|
33783
|
-
}
|
|
34255
|
+
},
|
|
34256
|
+
this.transport
|
|
33784
34257
|
);
|
|
33785
34258
|
}
|
|
33786
34259
|
this.emitter.emitSticky("tracks", {
|
|
@@ -33789,7 +34262,12 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
33789
34262
|
subtitle: ctx.subtitleTracks
|
|
33790
34263
|
});
|
|
33791
34264
|
this.startTimeupdateLoop();
|
|
33792
|
-
this.
|
|
34265
|
+
this.endedListener = () => this.emitter.emit("ended", void 0);
|
|
34266
|
+
this.options.target.addEventListener("ended", this.endedListener);
|
|
34267
|
+
if (this.options.backgroundBehavior !== "continue" && typeof document !== "undefined") {
|
|
34268
|
+
this.visibilityListener = () => this.onVisibilityChange();
|
|
34269
|
+
document.addEventListener("visibilitychange", this.visibilityListener);
|
|
34270
|
+
}
|
|
33793
34271
|
this.emitter.emitSticky("ready", void 0);
|
|
33794
34272
|
const bootstrapElapsed = performance.now() - bootstrapStart;
|
|
33795
34273
|
dbg.info("bootstrap", `ready in ${bootstrapElapsed.toFixed(0)}ms`);
|
|
@@ -33816,7 +34294,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
33816
34294
|
throw new Error(`no plugin available for strategy "${strategy}"`);
|
|
33817
34295
|
}
|
|
33818
34296
|
try {
|
|
33819
|
-
this.session = await plugin.execute(this.mediaContext, this.options.target);
|
|
34297
|
+
this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
|
|
33820
34298
|
} catch (err) {
|
|
33821
34299
|
const chain2 = this.classification?.fallbackChain;
|
|
33822
34300
|
if (chain2 && chain2.length > 0) {
|
|
@@ -33889,7 +34367,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
33889
34367
|
continue;
|
|
33890
34368
|
}
|
|
33891
34369
|
try {
|
|
33892
|
-
this.session = await plugin.execute(this.mediaContext, this.options.target);
|
|
34370
|
+
this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
|
|
33893
34371
|
} catch (err) {
|
|
33894
34372
|
const msg = err instanceof Error ? err.message : String(err);
|
|
33895
34373
|
errors.push(`${nextStrategy}: ${msg}`);
|
|
@@ -33912,8 +34390,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
33912
34390
|
}
|
|
33913
34391
|
return;
|
|
33914
34392
|
}
|
|
33915
|
-
this.emitter.emit("error", new
|
|
33916
|
-
|
|
34393
|
+
this.emitter.emit("error", new AvbridgeError(
|
|
34394
|
+
ERR_ALL_STRATEGIES_EXHAUSTED,
|
|
34395
|
+
`All playback strategies failed: ${errors.join("; ")}`,
|
|
34396
|
+
"This file may require a codec or container that isn't available in this browser. Try the fallback strategy or check browser codec support."
|
|
33917
34397
|
));
|
|
33918
34398
|
}
|
|
33919
34399
|
// ── Stall supervision ─────────────────────────────────────────────────
|
|
@@ -33965,7 +34445,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
33965
34445
|
// ── Public: manual strategy switch ────────────────────────────────────
|
|
33966
34446
|
/** Manually switch to a different playback strategy. Preserves current position and play/pause state. Concurrent calls are serialized. */
|
|
33967
34447
|
async setStrategy(strategy, reason) {
|
|
33968
|
-
if (!this.mediaContext) throw new
|
|
34448
|
+
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.");
|
|
33969
34449
|
if (this.session?.strategy === strategy) return;
|
|
33970
34450
|
this.switchingPromise = this.switchingPromise.then(
|
|
33971
34451
|
() => this.doSetStrategy(strategy, reason)
|
|
@@ -33994,7 +34474,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
33994
34474
|
}
|
|
33995
34475
|
const plugin = this.registry.findFor(this.mediaContext, strategy);
|
|
33996
34476
|
if (!plugin) throw new Error(`no plugin available for strategy "${strategy}"`);
|
|
33997
|
-
this.session = await plugin.execute(this.mediaContext, this.options.target);
|
|
34477
|
+
this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
|
|
33998
34478
|
this.emitter.emitSticky("strategy", {
|
|
33999
34479
|
strategy,
|
|
34000
34480
|
reason: switchReason
|
|
@@ -34028,26 +34508,56 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
34028
34508
|
}
|
|
34029
34509
|
/** Begin or resume playback. Throws if the player is not ready. */
|
|
34030
34510
|
async play() {
|
|
34031
|
-
if (!this.session) throw new
|
|
34511
|
+
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.");
|
|
34512
|
+
this.userIntent = "play";
|
|
34513
|
+
this.autoPausedForVisibility = false;
|
|
34032
34514
|
await this.session.play();
|
|
34033
34515
|
}
|
|
34034
34516
|
/** Pause playback. No-op if the player is not ready or already paused. */
|
|
34035
34517
|
pause() {
|
|
34518
|
+
this.userIntent = "pause";
|
|
34519
|
+
this.autoPausedForVisibility = false;
|
|
34036
34520
|
this.session?.pause();
|
|
34037
34521
|
}
|
|
34522
|
+
/**
|
|
34523
|
+
* Handle browser tab visibility changes. On hide: pause if the user
|
|
34524
|
+
* had been playing. On show: resume if we were the one who paused.
|
|
34525
|
+
* Skips when `backgroundBehavior: "continue"` is set (listener isn't
|
|
34526
|
+
* installed in that case).
|
|
34527
|
+
*/
|
|
34528
|
+
onVisibilityChange() {
|
|
34529
|
+
if (!this.session) return;
|
|
34530
|
+
const action = decideVisibilityAction({
|
|
34531
|
+
hidden: document.hidden,
|
|
34532
|
+
userIntent: this.userIntent,
|
|
34533
|
+
sessionIsPlaying: !this.options.target.paused,
|
|
34534
|
+
autoPausedForVisibility: this.autoPausedForVisibility
|
|
34535
|
+
});
|
|
34536
|
+
if (action === "pause") {
|
|
34537
|
+
this.autoPausedForVisibility = true;
|
|
34538
|
+
dbg.info("visibility", "tab hidden \u2014 auto-paused");
|
|
34539
|
+
this.session.pause();
|
|
34540
|
+
} else if (action === "resume") {
|
|
34541
|
+
this.autoPausedForVisibility = false;
|
|
34542
|
+
dbg.info("visibility", "tab visible \u2014 auto-resuming");
|
|
34543
|
+
void this.session.play().catch((err) => {
|
|
34544
|
+
console.warn("[avbridge] auto-resume after tab return failed:", err);
|
|
34545
|
+
});
|
|
34546
|
+
}
|
|
34547
|
+
}
|
|
34038
34548
|
/** Seek to the given time in seconds. Throws if the player is not ready. */
|
|
34039
34549
|
async seek(time) {
|
|
34040
|
-
if (!this.session) throw new
|
|
34550
|
+
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.");
|
|
34041
34551
|
await this.session.seek(time);
|
|
34042
34552
|
}
|
|
34043
34553
|
/** Switch the active audio track by track ID. Throws if the player is not ready. */
|
|
34044
34554
|
async setAudioTrack(id) {
|
|
34045
|
-
if (!this.session) throw new
|
|
34555
|
+
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.");
|
|
34046
34556
|
await this.session.setAudioTrack(id);
|
|
34047
34557
|
}
|
|
34048
34558
|
/** Switch the active subtitle track by track ID, or pass `null` to disable subtitles. */
|
|
34049
34559
|
async setSubtitleTrack(id) {
|
|
34050
|
-
if (!this.session) throw new
|
|
34560
|
+
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.");
|
|
34051
34561
|
await this.session.setSubtitleTrack(id);
|
|
34052
34562
|
}
|
|
34053
34563
|
/** Return a snapshot of current diagnostics: container, codecs, strategy, runtime stats, and strategy history. */
|
|
@@ -34075,6 +34585,14 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
34075
34585
|
this.timeupdateInterval = null;
|
|
34076
34586
|
}
|
|
34077
34587
|
this.clearSupervisor();
|
|
34588
|
+
if (this.endedListener) {
|
|
34589
|
+
this.options.target.removeEventListener("ended", this.endedListener);
|
|
34590
|
+
this.endedListener = null;
|
|
34591
|
+
}
|
|
34592
|
+
if (this.visibilityListener) {
|
|
34593
|
+
document.removeEventListener("visibilitychange", this.visibilityListener);
|
|
34594
|
+
this.visibilityListener = null;
|
|
34595
|
+
}
|
|
34078
34596
|
if (this.session) {
|
|
34079
34597
|
await this.session.destroy();
|
|
34080
34598
|
this.session = null;
|
|
@@ -34086,14 +34604,24 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
34086
34604
|
async function createPlayer(options) {
|
|
34087
34605
|
return UnifiedPlayer.create(options);
|
|
34088
34606
|
}
|
|
34607
|
+
function decideVisibilityAction(state) {
|
|
34608
|
+
if (state.hidden) {
|
|
34609
|
+
if (state.userIntent === "play" && state.sessionIsPlaying) return "pause";
|
|
34610
|
+
return "noop";
|
|
34611
|
+
}
|
|
34612
|
+
if (state.autoPausedForVisibility) return "resume";
|
|
34613
|
+
return "noop";
|
|
34614
|
+
}
|
|
34089
34615
|
function buildInitialDecision(initial, ctx) {
|
|
34090
34616
|
const natural = classifyContext(ctx);
|
|
34091
34617
|
const cls = strategyToClass(initial, natural);
|
|
34618
|
+
const inherited = natural.fallbackChain ?? defaultFallbackChain(initial);
|
|
34619
|
+
const fallbackChain = inherited.filter((s) => s !== initial);
|
|
34092
34620
|
return {
|
|
34093
34621
|
class: cls,
|
|
34094
34622
|
strategy: initial,
|
|
34095
34623
|
reason: `initial strategy "${initial}" requested via options.initialStrategy`,
|
|
34096
|
-
fallbackChain
|
|
34624
|
+
fallbackChain
|
|
34097
34625
|
};
|
|
34098
34626
|
}
|
|
34099
34627
|
function strategyToClass(strategy, natural) {
|