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
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);
|
|
@@ -32065,6 +32172,10 @@ async function createRemuxSession(context, video) {
|
|
|
32065
32172
|
}
|
|
32066
32173
|
|
|
32067
32174
|
// src/strategies/fallback/video-renderer.ts
|
|
32175
|
+
function isDebug() {
|
|
32176
|
+
return typeof globalThis !== "undefined" && !!globalThis.AVBRIDGE_DEBUG;
|
|
32177
|
+
}
|
|
32178
|
+
var lastDebugLog = 0;
|
|
32068
32179
|
var VideoRenderer = class {
|
|
32069
32180
|
constructor(target, clock, fps = 30) {
|
|
32070
32181
|
this.target = target;
|
|
@@ -32111,6 +32222,20 @@ var VideoRenderer = class {
|
|
|
32111
32222
|
lastPaintWall = 0;
|
|
32112
32223
|
/** Minimum ms between paints — paces video at roughly source fps. */
|
|
32113
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;
|
|
32114
32239
|
/** Resolves once the first decoded frame has been enqueued. */
|
|
32115
32240
|
firstFrameReady;
|
|
32116
32241
|
resolveFirstFrame;
|
|
@@ -32159,21 +32284,81 @@ var VideoRenderer = class {
|
|
|
32159
32284
|
}
|
|
32160
32285
|
return;
|
|
32161
32286
|
}
|
|
32162
|
-
const
|
|
32163
|
-
|
|
32164
|
-
|
|
32165
|
-
if (
|
|
32166
|
-
const
|
|
32167
|
-
|
|
32168
|
-
|
|
32169
|
-
|
|
32170
|
-
this.
|
|
32171
|
-
|
|
32172
|
-
|
|
32173
|
-
|
|
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
|
+
}
|
|
32174
32324
|
return;
|
|
32175
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;
|
|
32176
32359
|
}
|
|
32360
|
+
const wallNow = performance.now();
|
|
32361
|
+
if (wallNow - this.lastPaintWall < this.paintIntervalMs - 2) return;
|
|
32177
32362
|
const frame = this.queue.shift();
|
|
32178
32363
|
this.paint(frame);
|
|
32179
32364
|
frame.close();
|
|
@@ -32195,8 +32380,13 @@ var VideoRenderer = class {
|
|
|
32195
32380
|
}
|
|
32196
32381
|
/** Discard all queued frames. Used by seek to drop stale buffers. */
|
|
32197
32382
|
flush() {
|
|
32383
|
+
const count = this.queue.length;
|
|
32198
32384
|
while (this.queue.length > 0) this.queue.shift()?.close();
|
|
32199
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
|
+
}
|
|
32200
32390
|
}
|
|
32201
32391
|
stats() {
|
|
32202
32392
|
return {
|
|
@@ -32241,11 +32431,38 @@ var AudioOutput = class {
|
|
|
32241
32431
|
pendingQueue = [];
|
|
32242
32432
|
framesScheduled = 0;
|
|
32243
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;
|
|
32244
32438
|
constructor() {
|
|
32245
32439
|
this.ctx = new AudioContext();
|
|
32246
32440
|
this.gain = this.ctx.createGain();
|
|
32247
32441
|
this.gain.connect(this.ctx.destination);
|
|
32248
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
|
+
}
|
|
32249
32466
|
/**
|
|
32250
32467
|
* Switch into wall-clock fallback mode. Called by the decoder when no
|
|
32251
32468
|
* audio decoder could be initialized for the source. Once set, this
|
|
@@ -32395,6 +32612,7 @@ var AudioOutput = class {
|
|
|
32395
32612
|
}
|
|
32396
32613
|
this.gain = this.ctx.createGain();
|
|
32397
32614
|
this.gain.connect(this.ctx.destination);
|
|
32615
|
+
this.applyGain();
|
|
32398
32616
|
this.pendingQueue = [];
|
|
32399
32617
|
this.mediaTimeOfAnchor = newMediaTime;
|
|
32400
32618
|
this.mediaTimeOfNext = newMediaTime;
|
|
@@ -32423,27 +32641,19 @@ var AudioOutput = class {
|
|
|
32423
32641
|
|
|
32424
32642
|
// src/strategies/hybrid/decoder.ts
|
|
32425
32643
|
init_libav_loader();
|
|
32644
|
+
init_debug();
|
|
32426
32645
|
|
|
32427
32646
|
// src/strategies/fallback/variant-routing.ts
|
|
32428
32647
|
var LEGACY_CONTAINERS = /* @__PURE__ */ new Set(["avi", "asf", "flv"]);
|
|
32429
|
-
var
|
|
32430
|
-
|
|
32431
|
-
"vc1",
|
|
32432
|
-
"mpeg4",
|
|
32433
|
-
// MPEG-4 Part 2 / DivX / Xvid
|
|
32434
|
-
"rv40",
|
|
32435
|
-
"mpeg2",
|
|
32436
|
-
"mpeg1",
|
|
32437
|
-
"theora"
|
|
32438
|
-
]);
|
|
32439
|
-
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"]);
|
|
32440
32650
|
function pickLibavVariant(ctx) {
|
|
32441
32651
|
if (LEGACY_CONTAINERS.has(ctx.container)) return "avbridge";
|
|
32442
32652
|
for (const v of ctx.videoTracks) {
|
|
32443
|
-
if (
|
|
32653
|
+
if (!WEBCODECS_VIDEO.has(v.codec)) return "avbridge";
|
|
32444
32654
|
}
|
|
32445
32655
|
for (const a of ctx.audioTracks) {
|
|
32446
|
-
if (
|
|
32656
|
+
if (!WEBCODECS_AUDIO.has(a.codec)) return "avbridge";
|
|
32447
32657
|
}
|
|
32448
32658
|
return "webcodecs";
|
|
32449
32659
|
}
|
|
@@ -32454,7 +32664,7 @@ async function startHybridDecoder(opts) {
|
|
|
32454
32664
|
const libav = await loadLibav(variant);
|
|
32455
32665
|
const bridge = await loadBridge();
|
|
32456
32666
|
const { prepareLibavInput: prepareLibavInput2 } = await Promise.resolve().then(() => (init_libav_http_reader(), libav_http_reader_exports));
|
|
32457
|
-
const inputHandle = await prepareLibavInput2(libav, opts.filename, opts.source);
|
|
32667
|
+
const inputHandle = await prepareLibavInput2(libav, opts.filename, opts.source, opts.transport);
|
|
32458
32668
|
const readPkt = await libav.av_packet_alloc();
|
|
32459
32669
|
const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
|
|
32460
32670
|
const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
|
|
@@ -32525,6 +32735,56 @@ async function startHybridDecoder(opts) {
|
|
|
32525
32735
|
});
|
|
32526
32736
|
throw new Error("hybrid decoder: could not initialize any decoders");
|
|
32527
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
|
+
}
|
|
32528
32788
|
let destroyed = false;
|
|
32529
32789
|
let pumpToken = 0;
|
|
32530
32790
|
let pumpRunning = null;
|
|
@@ -32552,8 +32812,15 @@ async function startHybridDecoder(opts) {
|
|
|
32552
32812
|
if (myToken !== pumpToken || destroyed) return;
|
|
32553
32813
|
const videoPackets = videoStream ? packets[videoStream.index] : void 0;
|
|
32554
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;
|
|
32555
32821
|
if (videoDecoder && videoPackets && videoPackets.length > 0) {
|
|
32556
|
-
|
|
32822
|
+
const processed = await applyBSF(videoPackets);
|
|
32823
|
+
for (const pkt of processed) {
|
|
32557
32824
|
if (myToken !== pumpToken || destroyed) return;
|
|
32558
32825
|
sanitizePacketTimestamp(pkt, () => {
|
|
32559
32826
|
const ts = syntheticVideoUs;
|
|
@@ -32573,9 +32840,6 @@ async function startHybridDecoder(opts) {
|
|
|
32573
32840
|
}
|
|
32574
32841
|
}
|
|
32575
32842
|
}
|
|
32576
|
-
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
32577
|
-
await decodeAudioBatch(audioPackets, myToken);
|
|
32578
|
-
}
|
|
32579
32843
|
packetsRead += (videoPackets?.length ?? 0) + (audioPackets?.length ?? 0);
|
|
32580
32844
|
while (!destroyed && myToken === pumpToken && (videoDecoder && videoDecoder.decodeQueueSize > 10 || opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
|
|
32581
32845
|
await new Promise((r) => setTimeout(r, 50));
|
|
@@ -32598,20 +32862,43 @@ async function startHybridDecoder(opts) {
|
|
|
32598
32862
|
}
|
|
32599
32863
|
async function decodeAudioBatch(pkts, myToken, flush = false) {
|
|
32600
32864
|
if (!audioDec || destroyed || myToken !== pumpToken) return;
|
|
32601
|
-
|
|
32602
|
-
|
|
32603
|
-
|
|
32604
|
-
|
|
32605
|
-
|
|
32606
|
-
|
|
32607
|
-
|
|
32608
|
-
|
|
32609
|
-
|
|
32610
|
-
|
|
32611
|
-
|
|
32612
|
-
|
|
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
|
+
}
|
|
32613
32899
|
}
|
|
32614
32900
|
if (myToken !== pumpToken || destroyed) return;
|
|
32901
|
+
const frames = allFrames;
|
|
32615
32902
|
for (const f of frames) {
|
|
32616
32903
|
if (myToken !== pumpToken || destroyed) return;
|
|
32617
32904
|
sanitizeFrameTimestamp(
|
|
@@ -32648,6 +32935,14 @@ async function startHybridDecoder(opts) {
|
|
|
32648
32935
|
await pumpRunning;
|
|
32649
32936
|
} catch {
|
|
32650
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
|
+
}
|
|
32651
32946
|
try {
|
|
32652
32947
|
if (videoDecoder && videoDecoder.state !== "closed") videoDecoder.close();
|
|
32653
32948
|
} catch {
|
|
@@ -32701,6 +32996,7 @@ async function startHybridDecoder(opts) {
|
|
|
32701
32996
|
if (audioDec) await libav.avcodec_flush_buffers?.(audioDec.c);
|
|
32702
32997
|
} catch {
|
|
32703
32998
|
}
|
|
32999
|
+
await flushBSF();
|
|
32704
33000
|
syntheticVideoUs = Math.round(timeSec * 1e6);
|
|
32705
33001
|
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
32706
33002
|
pumpRunning = pumpLoop(newToken).catch(
|
|
@@ -32714,6 +33010,7 @@ async function startHybridDecoder(opts) {
|
|
|
32714
33010
|
videoFramesDecoded,
|
|
32715
33011
|
videoChunksFed,
|
|
32716
33012
|
audioFramesDecoded,
|
|
33013
|
+
bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
|
|
32717
33014
|
videoDecodeQueueSize: videoDecoder?.decodeQueueSize ?? 0,
|
|
32718
33015
|
// Confirmed transport info — see fallback decoder for the pattern.
|
|
32719
33016
|
_transport: inputHandle.transport === "http-range" ? "http-range" : "memory",
|
|
@@ -32890,7 +33187,7 @@ async function loadBridge() {
|
|
|
32890
33187
|
// src/strategies/hybrid/index.ts
|
|
32891
33188
|
var READY_AUDIO_BUFFER_SECONDS = 0.3;
|
|
32892
33189
|
var READY_TIMEOUT_SECONDS = 10;
|
|
32893
|
-
async function createHybridSession(ctx, target) {
|
|
33190
|
+
async function createHybridSession(ctx, target, transport) {
|
|
32894
33191
|
const { normalizeSource: normalizeSource2 } = await Promise.resolve().then(() => (init_source(), source_exports));
|
|
32895
33192
|
const source = await normalizeSource2(ctx.source);
|
|
32896
33193
|
const fps = ctx.videoTracks[0]?.fps ?? 30;
|
|
@@ -32903,7 +33200,8 @@ async function createHybridSession(ctx, target) {
|
|
|
32903
33200
|
filename: ctx.name ?? "input.bin",
|
|
32904
33201
|
context: ctx,
|
|
32905
33202
|
renderer,
|
|
32906
|
-
audio
|
|
33203
|
+
audio,
|
|
33204
|
+
transport
|
|
32907
33205
|
});
|
|
32908
33206
|
} catch (err) {
|
|
32909
33207
|
audio.destroy();
|
|
@@ -32921,6 +33219,22 @@ async function createHybridSession(ctx, target) {
|
|
|
32921
33219
|
configurable: true,
|
|
32922
33220
|
get: () => !audio.isPlaying()
|
|
32923
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
|
+
});
|
|
32924
33238
|
if (ctx.duration && Number.isFinite(ctx.duration)) {
|
|
32925
33239
|
Object.defineProperty(target, "duration", {
|
|
32926
33240
|
configurable: true,
|
|
@@ -32960,10 +33274,13 @@ async function createHybridSession(ctx, target) {
|
|
|
32960
33274
|
if (!audio.isPlaying()) {
|
|
32961
33275
|
await waitForBuffer();
|
|
32962
33276
|
await audio.start();
|
|
33277
|
+
target.dispatchEvent(new Event("play"));
|
|
33278
|
+
target.dispatchEvent(new Event("playing"));
|
|
32963
33279
|
}
|
|
32964
33280
|
},
|
|
32965
33281
|
pause() {
|
|
32966
33282
|
void audio.pause();
|
|
33283
|
+
target.dispatchEvent(new Event("pause"));
|
|
32967
33284
|
},
|
|
32968
33285
|
async seek(time) {
|
|
32969
33286
|
await doSeek(time);
|
|
@@ -32986,6 +33303,8 @@ async function createHybridSession(ctx, target) {
|
|
|
32986
33303
|
delete target.currentTime;
|
|
32987
33304
|
delete target.duration;
|
|
32988
33305
|
delete target.paused;
|
|
33306
|
+
delete target.volume;
|
|
33307
|
+
delete target.muted;
|
|
32989
33308
|
} catch {
|
|
32990
33309
|
}
|
|
32991
33310
|
},
|
|
@@ -32997,12 +33316,13 @@ async function createHybridSession(ctx, target) {
|
|
|
32997
33316
|
|
|
32998
33317
|
// src/strategies/fallback/decoder.ts
|
|
32999
33318
|
init_libav_loader();
|
|
33319
|
+
init_debug();
|
|
33000
33320
|
async function startDecoder(opts) {
|
|
33001
33321
|
const variant = pickLibavVariant(opts.context);
|
|
33002
33322
|
const libav = await loadLibav(variant);
|
|
33003
33323
|
const bridge = await loadBridge2();
|
|
33004
33324
|
const { prepareLibavInput: prepareLibavInput2 } = await Promise.resolve().then(() => (init_libav_http_reader(), libav_http_reader_exports));
|
|
33005
|
-
const inputHandle = await prepareLibavInput2(libav, opts.filename, opts.source);
|
|
33325
|
+
const inputHandle = await prepareLibavInput2(libav, opts.filename, opts.source, opts.transport);
|
|
33006
33326
|
const readPkt = await libav.av_packet_alloc();
|
|
33007
33327
|
const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
|
|
33008
33328
|
const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
|
|
@@ -33058,6 +33378,56 @@ async function startDecoder(opts) {
|
|
|
33058
33378
|
`fallback decoder: could not initialize any libav decoders (${codecs}).${hint}`
|
|
33059
33379
|
);
|
|
33060
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
|
+
}
|
|
33061
33431
|
let destroyed = false;
|
|
33062
33432
|
let pumpToken = 0;
|
|
33063
33433
|
let pumpRunning = null;
|
|
@@ -33093,7 +33463,8 @@ async function startDecoder(opts) {
|
|
|
33093
33463
|
}
|
|
33094
33464
|
if (myToken !== pumpToken || destroyed) return;
|
|
33095
33465
|
if (videoDec && videoPackets && videoPackets.length > 0) {
|
|
33096
|
-
await
|
|
33466
|
+
const processed = await applyBSF(videoPackets);
|
|
33467
|
+
await decodeVideoBatch(processed, myToken);
|
|
33097
33468
|
}
|
|
33098
33469
|
packetsRead += (videoPackets?.length ?? 0) + (audioPackets?.length ?? 0);
|
|
33099
33470
|
if (videoFramesDecoded > 0) {
|
|
@@ -33239,6 +33610,14 @@ async function startDecoder(opts) {
|
|
|
33239
33610
|
await pumpRunning;
|
|
33240
33611
|
} catch {
|
|
33241
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
|
+
}
|
|
33242
33621
|
try {
|
|
33243
33622
|
if (videoDec) await libav.ff_free_decoder?.(videoDec.c, videoDec.pkt, videoDec.frame);
|
|
33244
33623
|
} catch {
|
|
@@ -33290,6 +33669,7 @@ async function startDecoder(opts) {
|
|
|
33290
33669
|
if (audioDec) await libav.avcodec_flush_buffers?.(audioDec.c);
|
|
33291
33670
|
} catch {
|
|
33292
33671
|
}
|
|
33672
|
+
await flushBSF();
|
|
33293
33673
|
syntheticVideoUs = Math.round(timeSec * 1e6);
|
|
33294
33674
|
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
33295
33675
|
pumpRunning = pumpLoop(newToken).catch(
|
|
@@ -33302,6 +33682,7 @@ async function startDecoder(opts) {
|
|
|
33302
33682
|
packetsRead,
|
|
33303
33683
|
videoFramesDecoded,
|
|
33304
33684
|
audioFramesDecoded,
|
|
33685
|
+
bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
|
|
33305
33686
|
// Confirmed transport info: once prepareLibavInput returns
|
|
33306
33687
|
// successfully, we *know* whether the source is http-range (probe
|
|
33307
33688
|
// succeeded and returned 206) or in-memory blob. Diagnostics hoists
|
|
@@ -33458,7 +33839,7 @@ async function loadBridge2() {
|
|
|
33458
33839
|
init_debug();
|
|
33459
33840
|
var READY_AUDIO_BUFFER_SECONDS2 = 0.04;
|
|
33460
33841
|
var READY_TIMEOUT_SECONDS2 = 3;
|
|
33461
|
-
async function createFallbackSession(ctx, target) {
|
|
33842
|
+
async function createFallbackSession(ctx, target, transport) {
|
|
33462
33843
|
const { normalizeSource: normalizeSource2 } = await Promise.resolve().then(() => (init_source(), source_exports));
|
|
33463
33844
|
const source = await normalizeSource2(ctx.source);
|
|
33464
33845
|
const fps = ctx.videoTracks[0]?.fps ?? 30;
|
|
@@ -33471,7 +33852,8 @@ async function createFallbackSession(ctx, target) {
|
|
|
33471
33852
|
filename: ctx.name ?? "input.bin",
|
|
33472
33853
|
context: ctx,
|
|
33473
33854
|
renderer,
|
|
33474
|
-
audio
|
|
33855
|
+
audio,
|
|
33856
|
+
transport
|
|
33475
33857
|
});
|
|
33476
33858
|
} catch (err) {
|
|
33477
33859
|
audio.destroy();
|
|
@@ -33489,6 +33871,22 @@ async function createFallbackSession(ctx, target) {
|
|
|
33489
33871
|
configurable: true,
|
|
33490
33872
|
get: () => !audio.isPlaying()
|
|
33491
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
|
+
});
|
|
33492
33890
|
if (ctx.duration && Number.isFinite(ctx.duration)) {
|
|
33493
33891
|
Object.defineProperty(target, "duration", {
|
|
33494
33892
|
configurable: true,
|
|
@@ -33552,10 +33950,13 @@ async function createFallbackSession(ctx, target) {
|
|
|
33552
33950
|
if (!audio.isPlaying()) {
|
|
33553
33951
|
await waitForBuffer();
|
|
33554
33952
|
await audio.start();
|
|
33953
|
+
target.dispatchEvent(new Event("play"));
|
|
33954
|
+
target.dispatchEvent(new Event("playing"));
|
|
33555
33955
|
}
|
|
33556
33956
|
},
|
|
33557
33957
|
pause() {
|
|
33558
33958
|
void audio.pause();
|
|
33959
|
+
target.dispatchEvent(new Event("pause"));
|
|
33559
33960
|
},
|
|
33560
33961
|
async seek(time) {
|
|
33561
33962
|
await doSeek(time);
|
|
@@ -33575,6 +33976,8 @@ async function createFallbackSession(ctx, target) {
|
|
|
33575
33976
|
delete target.currentTime;
|
|
33576
33977
|
delete target.duration;
|
|
33577
33978
|
delete target.paused;
|
|
33979
|
+
delete target.volume;
|
|
33980
|
+
delete target.muted;
|
|
33578
33981
|
} catch {
|
|
33579
33982
|
}
|
|
33580
33983
|
},
|
|
@@ -33598,12 +34001,12 @@ var remuxPlugin = {
|
|
|
33598
34001
|
var hybridPlugin = {
|
|
33599
34002
|
name: "hybrid",
|
|
33600
34003
|
canHandle: () => typeof VideoDecoder !== "undefined",
|
|
33601
|
-
execute: (ctx, video) => createHybridSession(ctx, video)
|
|
34004
|
+
execute: (ctx, video, transport) => createHybridSession(ctx, video, transport)
|
|
33602
34005
|
};
|
|
33603
34006
|
var fallbackPlugin = {
|
|
33604
34007
|
name: "fallback",
|
|
33605
34008
|
canHandle: () => true,
|
|
33606
|
-
execute: (ctx, video) => createFallbackSession(ctx, video)
|
|
34009
|
+
execute: (ctx, video, transport) => createFallbackSession(ctx, video, transport)
|
|
33607
34010
|
};
|
|
33608
34011
|
function registerBuiltins(registry) {
|
|
33609
34012
|
registry.register(nativePlugin);
|
|
@@ -33612,6 +34015,9 @@ function registerBuiltins(registry) {
|
|
|
33612
34015
|
registry.register(fallbackPlugin);
|
|
33613
34016
|
}
|
|
33614
34017
|
|
|
34018
|
+
// src/subtitles/index.ts
|
|
34019
|
+
init_transport();
|
|
34020
|
+
|
|
33615
34021
|
// src/subtitles/srt.ts
|
|
33616
34022
|
function srtToVtt(srt) {
|
|
33617
34023
|
if (srt.charCodeAt(0) === 65279) srt = srt.slice(1);
|
|
@@ -33689,7 +34095,8 @@ var SubtitleResourceBag = class {
|
|
|
33689
34095
|
this.urls.clear();
|
|
33690
34096
|
}
|
|
33691
34097
|
};
|
|
33692
|
-
async function attachSubtitleTracks(video, tracks, bag, onError) {
|
|
34098
|
+
async function attachSubtitleTracks(video, tracks, bag, onError, transport) {
|
|
34099
|
+
const doFetch = fetchWith(transport);
|
|
33693
34100
|
for (const t of Array.from(video.querySelectorAll("track[data-avbridge]"))) {
|
|
33694
34101
|
t.remove();
|
|
33695
34102
|
}
|
|
@@ -33698,13 +34105,13 @@ async function attachSubtitleTracks(video, tracks, bag, onError) {
|
|
|
33698
34105
|
try {
|
|
33699
34106
|
let url2 = t.sidecarUrl;
|
|
33700
34107
|
if (t.format === "srt") {
|
|
33701
|
-
const res = await
|
|
34108
|
+
const res = await doFetch(t.sidecarUrl, transport?.requestInit);
|
|
33702
34109
|
const text = await res.text();
|
|
33703
34110
|
const vtt = srtToVtt(text);
|
|
33704
34111
|
const blob = new Blob([vtt], { type: "text/vtt" });
|
|
33705
34112
|
url2 = bag ? bag.createObjectURL(blob) : URL.createObjectURL(blob);
|
|
33706
34113
|
} else if (t.format === "vtt") {
|
|
33707
|
-
const res = await
|
|
34114
|
+
const res = await doFetch(t.sidecarUrl, transport?.requestInit);
|
|
33708
34115
|
const text = await res.text();
|
|
33709
34116
|
if (!isVtt(text)) {
|
|
33710
34117
|
console.warn("[avbridge] subtitle missing WEBVTT header:", t.sidecarUrl);
|
|
@@ -33726,6 +34133,7 @@ async function attachSubtitleTracks(video, tracks, bag, onError) {
|
|
|
33726
34133
|
|
|
33727
34134
|
// src/player.ts
|
|
33728
34135
|
init_debug();
|
|
34136
|
+
init_errors();
|
|
33729
34137
|
var UnifiedPlayer = class _UnifiedPlayer {
|
|
33730
34138
|
/**
|
|
33731
34139
|
* @internal Use {@link createPlayer} or {@link UnifiedPlayer.create} instead.
|
|
@@ -33733,6 +34141,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
33733
34141
|
constructor(options, registry) {
|
|
33734
34142
|
this.options = options;
|
|
33735
34143
|
this.registry = registry;
|
|
34144
|
+
const { requestInit, fetchFn } = options;
|
|
34145
|
+
if (requestInit || fetchFn) {
|
|
34146
|
+
this.transport = { requestInit, fetchFn };
|
|
34147
|
+
}
|
|
33736
34148
|
}
|
|
33737
34149
|
options;
|
|
33738
34150
|
registry;
|
|
@@ -33752,11 +34164,23 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
33752
34164
|
// listener outlives the player and accumulates on elements that swap
|
|
33753
34165
|
// source (e.g. <avbridge-video>).
|
|
33754
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;
|
|
33755
34175
|
// Serializes escalation / setStrategy calls
|
|
33756
34176
|
switchingPromise = Promise.resolve();
|
|
33757
34177
|
// Owns blob URLs created during sidecar discovery + SRT->VTT conversion.
|
|
33758
34178
|
// Revoked at destroy() so repeated source swaps don't leak.
|
|
33759
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;
|
|
33760
34184
|
static async create(options) {
|
|
33761
34185
|
const registry = new PluginRegistry();
|
|
33762
34186
|
registerBuiltins(registry);
|
|
@@ -33780,7 +34204,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
33780
34204
|
const bootstrapStart = performance.now();
|
|
33781
34205
|
try {
|
|
33782
34206
|
dbg.info("bootstrap", "start");
|
|
33783
|
-
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));
|
|
33784
34208
|
dbg.info(
|
|
33785
34209
|
"probe",
|
|
33786
34210
|
`container=${ctx.container} video=${ctx.videoTracks[0]?.codec ?? "-"} audio=${ctx.audioTracks[0]?.codec ?? "-"} probedBy=${ctx.probedBy}`
|
|
@@ -33828,7 +34252,8 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
33828
34252
|
this.subtitleResources,
|
|
33829
34253
|
(err, track) => {
|
|
33830
34254
|
console.warn(`[avbridge] subtitle ${track.id} failed: ${err.message}`);
|
|
33831
|
-
}
|
|
34255
|
+
},
|
|
34256
|
+
this.transport
|
|
33832
34257
|
);
|
|
33833
34258
|
}
|
|
33834
34259
|
this.emitter.emitSticky("tracks", {
|
|
@@ -33839,6 +34264,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
33839
34264
|
this.startTimeupdateLoop();
|
|
33840
34265
|
this.endedListener = () => this.emitter.emit("ended", void 0);
|
|
33841
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
|
+
}
|
|
33842
34271
|
this.emitter.emitSticky("ready", void 0);
|
|
33843
34272
|
const bootstrapElapsed = performance.now() - bootstrapStart;
|
|
33844
34273
|
dbg.info("bootstrap", `ready in ${bootstrapElapsed.toFixed(0)}ms`);
|
|
@@ -33865,7 +34294,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
33865
34294
|
throw new Error(`no plugin available for strategy "${strategy}"`);
|
|
33866
34295
|
}
|
|
33867
34296
|
try {
|
|
33868
|
-
this.session = await plugin.execute(this.mediaContext, this.options.target);
|
|
34297
|
+
this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
|
|
33869
34298
|
} catch (err) {
|
|
33870
34299
|
const chain2 = this.classification?.fallbackChain;
|
|
33871
34300
|
if (chain2 && chain2.length > 0) {
|
|
@@ -33938,7 +34367,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
33938
34367
|
continue;
|
|
33939
34368
|
}
|
|
33940
34369
|
try {
|
|
33941
|
-
this.session = await plugin.execute(this.mediaContext, this.options.target);
|
|
34370
|
+
this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
|
|
33942
34371
|
} catch (err) {
|
|
33943
34372
|
const msg = err instanceof Error ? err.message : String(err);
|
|
33944
34373
|
errors.push(`${nextStrategy}: ${msg}`);
|
|
@@ -33961,8 +34390,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
33961
34390
|
}
|
|
33962
34391
|
return;
|
|
33963
34392
|
}
|
|
33964
|
-
this.emitter.emit("error", new
|
|
33965
|
-
|
|
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."
|
|
33966
34397
|
));
|
|
33967
34398
|
}
|
|
33968
34399
|
// ── Stall supervision ─────────────────────────────────────────────────
|
|
@@ -34014,7 +34445,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
34014
34445
|
// ── Public: manual strategy switch ────────────────────────────────────
|
|
34015
34446
|
/** Manually switch to a different playback strategy. Preserves current position and play/pause state. Concurrent calls are serialized. */
|
|
34016
34447
|
async setStrategy(strategy, reason) {
|
|
34017
|
-
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.");
|
|
34018
34449
|
if (this.session?.strategy === strategy) return;
|
|
34019
34450
|
this.switchingPromise = this.switchingPromise.then(
|
|
34020
34451
|
() => this.doSetStrategy(strategy, reason)
|
|
@@ -34043,7 +34474,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
34043
34474
|
}
|
|
34044
34475
|
const plugin = this.registry.findFor(this.mediaContext, strategy);
|
|
34045
34476
|
if (!plugin) throw new Error(`no plugin available for strategy "${strategy}"`);
|
|
34046
|
-
this.session = await plugin.execute(this.mediaContext, this.options.target);
|
|
34477
|
+
this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
|
|
34047
34478
|
this.emitter.emitSticky("strategy", {
|
|
34048
34479
|
strategy,
|
|
34049
34480
|
reason: switchReason
|
|
@@ -34077,26 +34508,56 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
34077
34508
|
}
|
|
34078
34509
|
/** Begin or resume playback. Throws if the player is not ready. */
|
|
34079
34510
|
async play() {
|
|
34080
|
-
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;
|
|
34081
34514
|
await this.session.play();
|
|
34082
34515
|
}
|
|
34083
34516
|
/** Pause playback. No-op if the player is not ready or already paused. */
|
|
34084
34517
|
pause() {
|
|
34518
|
+
this.userIntent = "pause";
|
|
34519
|
+
this.autoPausedForVisibility = false;
|
|
34085
34520
|
this.session?.pause();
|
|
34086
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
|
+
}
|
|
34087
34548
|
/** Seek to the given time in seconds. Throws if the player is not ready. */
|
|
34088
34549
|
async seek(time) {
|
|
34089
|
-
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.");
|
|
34090
34551
|
await this.session.seek(time);
|
|
34091
34552
|
}
|
|
34092
34553
|
/** Switch the active audio track by track ID. Throws if the player is not ready. */
|
|
34093
34554
|
async setAudioTrack(id) {
|
|
34094
|
-
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.");
|
|
34095
34556
|
await this.session.setAudioTrack(id);
|
|
34096
34557
|
}
|
|
34097
34558
|
/** Switch the active subtitle track by track ID, or pass `null` to disable subtitles. */
|
|
34098
34559
|
async setSubtitleTrack(id) {
|
|
34099
|
-
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.");
|
|
34100
34561
|
await this.session.setSubtitleTrack(id);
|
|
34101
34562
|
}
|
|
34102
34563
|
/** Return a snapshot of current diagnostics: container, codecs, strategy, runtime stats, and strategy history. */
|
|
@@ -34128,6 +34589,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
34128
34589
|
this.options.target.removeEventListener("ended", this.endedListener);
|
|
34129
34590
|
this.endedListener = null;
|
|
34130
34591
|
}
|
|
34592
|
+
if (this.visibilityListener) {
|
|
34593
|
+
document.removeEventListener("visibilitychange", this.visibilityListener);
|
|
34594
|
+
this.visibilityListener = null;
|
|
34595
|
+
}
|
|
34131
34596
|
if (this.session) {
|
|
34132
34597
|
await this.session.destroy();
|
|
34133
34598
|
this.session = null;
|
|
@@ -34139,6 +34604,14 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
34139
34604
|
async function createPlayer(options) {
|
|
34140
34605
|
return UnifiedPlayer.create(options);
|
|
34141
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
|
+
}
|
|
34142
34615
|
function buildInitialDecision(initial, ctx) {
|
|
34143
34616
|
const natural = classifyContext(ctx);
|
|
34144
34617
|
const cls = strategyToClass(initial, natural);
|