avbridge 2.2.1 → 2.5.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 +153 -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-2IJ66NTD.cjs +212 -0
- package/dist/chunk-2IJ66NTD.cjs.map +1 -0
- package/dist/{chunk-ILKDNBSE.js → chunk-2XW2O3YI.cjs} +55 -10
- package/dist/chunk-2XW2O3YI.cjs.map +1 -0
- package/dist/chunk-5KVLE6YI.js +167 -0
- package/dist/chunk-5KVLE6YI.js.map +1 -0
- package/dist/chunk-5YAWWKA3.js +18 -0
- package/dist/chunk-5YAWWKA3.js.map +1 -0
- package/dist/chunk-CPJLFFCC.js +189 -0
- package/dist/chunk-CPJLFFCC.js.map +1 -0
- package/dist/chunk-CPZ7PXAM.cjs +240 -0
- package/dist/chunk-CPZ7PXAM.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-HZLQNKFN.cjs → chunk-E76AMWI4.js} +40 -15
- package/dist/chunk-E76AMWI4.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-DMWARSEF.js → chunk-KY2GPCT7.js} +788 -697
- package/dist/chunk-KY2GPCT7.js.map +1 -0
- package/dist/chunk-LUFA47FP.js +19 -0
- package/dist/chunk-LUFA47FP.js.map +1 -0
- package/dist/chunk-NNVOHKXJ.cjs +204 -0
- package/dist/chunk-NNVOHKXJ.cjs.map +1 -0
- package/dist/chunk-Q2VUO52Z.cjs +374 -0
- package/dist/chunk-Q2VUO52Z.cjs.map +1 -0
- package/dist/chunk-QDJLQR53.cjs +22 -0
- package/dist/chunk-QDJLQR53.cjs.map +1 -0
- package/dist/chunk-S4WAZC2T.cjs +173 -0
- package/dist/chunk-S4WAZC2T.cjs.map +1 -0
- package/dist/chunk-SMH6IOP2.js +368 -0
- package/dist/chunk-SMH6IOP2.js.map +1 -0
- package/dist/chunk-SR3MPV4D.js +237 -0
- package/dist/chunk-SR3MPV4D.js.map +1 -0
- package/dist/{chunk-UF2N5L63.cjs → chunk-TBW26OPP.cjs} +800 -710
- package/dist/chunk-TBW26OPP.cjs.map +1 -0
- package/dist/chunk-X2K3GIWE.js +235 -0
- package/dist/chunk-X2K3GIWE.js.map +1 -0
- package/dist/{chunk-L4NPOJ36.cjs → chunk-Z33SBWL5.cjs} +7 -4
- package/dist/chunk-Z33SBWL5.cjs.map +1 -0
- package/dist/chunk-ZCUXHW55.cjs +242 -0
- package/dist/chunk-ZCUXHW55.cjs.map +1 -0
- package/dist/element-browser.js +1282 -503
- package/dist/element-browser.js.map +1 -1
- package/dist/element.cjs +59 -5
- package/dist/element.cjs.map +1 -1
- package/dist/element.d.cts +39 -1
- package/dist/element.d.ts +39 -1
- package/dist/element.js +58 -4
- package/dist/element.js.map +1 -1
- package/dist/index.cjs +605 -327
- 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 +528 -319
- package/dist/index.js.map +1 -1
- package/dist/libav-demux-H2GS46GH.cjs +27 -0
- package/dist/{libav-http-reader-NQJVY273.js.map → libav-demux-H2GS46GH.cjs.map} +1 -1
- package/dist/libav-demux-OWZ4T2YW.js +6 -0
- package/dist/{libav-http-reader-FPYDBMYK.cjs.map → libav-demux-OWZ4T2YW.js.map} +1 -1
- package/dist/libav-http-reader-AZLE7YFS.cjs +16 -0
- package/dist/libav-http-reader-AZLE7YFS.cjs.map +1 -0
- package/dist/libav-http-reader-WXG3Z7AI.js +3 -0
- package/dist/libav-http-reader-WXG3Z7AI.js.map +1 -0
- package/dist/{libav-import-GST2AMPL.cjs → libav-import-2ZVKV2E7.cjs} +2 -2
- package/dist/{libav-import-GST2AMPL.cjs.map → libav-import-2ZVKV2E7.cjs.map} +1 -1
- package/dist/{libav-import-2JURFHEW.js → libav-import-6MGLCXVQ.js} +2 -2
- package/dist/{libav-import-2JURFHEW.js.map → libav-import-6MGLCXVQ.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 +5631 -0
- package/dist/player.cjs.map +1 -0
- package/dist/player.d.cts +699 -0
- package/dist/player.d.ts +699 -0
- package/dist/player.js +5629 -0
- package/dist/player.js.map +1 -0
- package/dist/remux-OBSMIENG.cjs +35 -0
- package/dist/remux-OBSMIENG.cjs.map +1 -0
- package/dist/remux-WBYIZBBX.js +10 -0
- package/dist/remux-WBYIZBBX.js.map +1 -0
- package/dist/source-4TZ6KMNV.js +4 -0
- package/dist/{source-FFZ7TW2B.js.map → source-4TZ6KMNV.js.map} +1 -1
- package/dist/source-7YLO6E7X.cjs +29 -0
- package/dist/{source-CN43EI7Z.cjs.map → source-7YLO6E7X.cjs.map} +1 -1
- package/dist/source-MTX5ELUZ.js +4 -0
- package/dist/source-MTX5ELUZ.js.map +1 -0
- package/dist/source-VFLXLOCN.cjs +29 -0
- package/dist/source-VFLXLOCN.cjs.map +1 -0
- package/dist/subtitles-4T74JRGT.js +4 -0
- package/dist/subtitles-4T74JRGT.js.map +1 -0
- package/dist/subtitles-QUH4LPI4.cjs +29 -0
- package/dist/subtitles-QUH4LPI4.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 +9 -35
- package/src/convert/transcode-libav.ts +691 -0
- package/src/convert/transcode.ts +53 -12
- package/src/element/avbridge-player.ts +861 -0
- package/src/element/avbridge-video.ts +54 -0
- package/src/element/player-icons.ts +25 -0
- package/src/element/player-styles.ts +472 -0
- package/src/errors.ts +53 -0
- package/src/index.ts +23 -0
- package/src/player-element.ts +18 -0
- package/src/player.ts +118 -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 +179 -175
- package/src/strategies/fallback/index.ts +48 -6
- package/src/strategies/fallback/libav-import.ts +9 -1
- package/src/strategies/fallback/variant-routing.ts +7 -13
- package/src/strategies/fallback/video-renderer.ts +231 -32
- package/src/strategies/hybrid/decoder.ts +219 -200
- package/src/strategies/hybrid/index.ts +48 -7
- package/src/strategies/native.ts +6 -3
- package/src/strategies/remux/index.ts +14 -2
- package/src/strategies/remux/mse.ts +12 -2
- package/src/strategies/remux/pipeline.ts +72 -12
- package/src/subtitles/index.ts +7 -3
- package/src/subtitles/render.ts +8 -0
- package/src/types.ts +53 -1
- package/src/util/libav-demux.ts +405 -0
- package/src/util/libav-http-reader.ts +5 -1
- package/src/util/source.ts +28 -8
- package/src/util/transport.ts +26 -0
- package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.mjs +1 -1
- package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.wasm +0 -0
- package/dist/avi-6SJLWIWW.cjs.map +0 -1
- package/dist/avi-GCGM7OJI.js.map +0 -1
- package/dist/chunk-DMWARSEF.js.map +0 -1
- package/dist/chunk-HZLQNKFN.cjs.map +0 -1
- package/dist/chunk-ILKDNBSE.js.map +0 -1
- package/dist/chunk-J5MCMN3S.js +0 -27
- package/dist/chunk-J5MCMN3S.js.map +0 -1
- package/dist/chunk-L4NPOJ36.cjs.map +0 -1
- package/dist/chunk-NZU7W256.cjs +0 -29
- package/dist/chunk-NZU7W256.cjs.map +0 -1
- package/dist/chunk-UF2N5L63.cjs.map +0 -1
- package/dist/chunk-WD2ZNQA7.js.map +0 -1
- package/dist/libav-http-reader-FPYDBMYK.cjs +0 -16
- package/dist/libav-http-reader-NQJVY273.js +0 -3
- package/dist/source-CN43EI7Z.cjs +0 -28
- package/dist/source-FFZ7TW2B.js +0 -3
- package/dist/variant-routing-GOHB2RZN.cjs +0 -12
- package/dist/variant-routing-JOBWXYKD.js +0 -3
|
@@ -1,219 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkS4WAZC2T_cjs = require('./chunk-S4WAZC2T.cjs');
|
|
4
|
+
var chunkZCUXHW55_cjs = require('./chunk-ZCUXHW55.cjs');
|
|
5
|
+
var chunk2IJ66NTD_cjs = require('./chunk-2IJ66NTD.cjs');
|
|
6
|
+
var chunkCPZ7PXAM_cjs = require('./chunk-CPZ7PXAM.cjs');
|
|
4
7
|
var chunkG4APZMCP_cjs = require('./chunk-G4APZMCP.cjs');
|
|
5
|
-
var
|
|
6
|
-
|
|
7
|
-
// src/probe/mediabunny.ts
|
|
8
|
-
async function probeWithMediabunny(source, sniffedContainer) {
|
|
9
|
-
const mb = await import('mediabunny');
|
|
10
|
-
const input = new mb.Input({
|
|
11
|
-
source: await buildMediabunnySource(mb, source),
|
|
12
|
-
formats: mb.ALL_FORMATS
|
|
13
|
-
});
|
|
14
|
-
const allTracks = await input.getTracks();
|
|
15
|
-
const duration = await safeNumber(() => input.computeDuration());
|
|
16
|
-
const videoTracks = [];
|
|
17
|
-
const audioTracks = [];
|
|
18
|
-
for (const track of allTracks) {
|
|
19
|
-
if (track.isVideoTrack()) {
|
|
20
|
-
const codecParam = await safe(() => track.getCodecParameterString());
|
|
21
|
-
videoTracks.push({
|
|
22
|
-
id: track.id,
|
|
23
|
-
codec: mediabunnyVideoToAvbridge(track.codec),
|
|
24
|
-
width: track.displayWidth ?? track.codedWidth ?? 0,
|
|
25
|
-
height: track.displayHeight ?? track.codedHeight ?? 0,
|
|
26
|
-
codecString: codecParam ?? void 0
|
|
27
|
-
});
|
|
28
|
-
} else if (track.isAudioTrack()) {
|
|
29
|
-
const codecParam = await safe(() => track.getCodecParameterString());
|
|
30
|
-
audioTracks.push({
|
|
31
|
-
id: track.id,
|
|
32
|
-
codec: mediabunnyAudioToAvbridge(track.codec),
|
|
33
|
-
channels: track.numberOfChannels ?? 0,
|
|
34
|
-
sampleRate: track.sampleRate ?? 0,
|
|
35
|
-
language: track.languageCode,
|
|
36
|
-
codecString: codecParam ?? void 0
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
const format = await safe(() => input.getFormat());
|
|
41
|
-
const container = resolveContainer(format?.name, sniffedContainer);
|
|
42
|
-
return {
|
|
43
|
-
source: source.original,
|
|
44
|
-
name: source.name,
|
|
45
|
-
byteLength: source.byteLength,
|
|
46
|
-
container,
|
|
47
|
-
videoTracks,
|
|
48
|
-
audioTracks,
|
|
49
|
-
subtitleTracks: [],
|
|
50
|
-
probedBy: "mediabunny",
|
|
51
|
-
duration
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
async function buildMediabunnySource(mb, source) {
|
|
55
|
-
if (source.kind === "url") {
|
|
56
|
-
return new mb.UrlSource(source.url);
|
|
57
|
-
}
|
|
58
|
-
return new mb.BlobSource(source.blob);
|
|
59
|
-
}
|
|
60
|
-
async function buildMediabunnySourceFromInput(mb, source) {
|
|
61
|
-
if (typeof source === "string") return new mb.UrlSource(source);
|
|
62
|
-
if (source instanceof URL) return new mb.UrlSource(source.toString());
|
|
63
|
-
if (source instanceof Blob) return new mb.BlobSource(source);
|
|
64
|
-
if (source instanceof ArrayBuffer) return new mb.BlobSource(new Blob([source]));
|
|
65
|
-
if (source instanceof Uint8Array) return new mb.BlobSource(new Blob([source]));
|
|
66
|
-
throw new TypeError("unsupported source type for mediabunny");
|
|
67
|
-
}
|
|
68
|
-
function resolveContainer(formatName, sniffed) {
|
|
69
|
-
const name = (formatName ?? "").toLowerCase();
|
|
70
|
-
if (name.includes("matroska") || name.includes("mkv")) return "mkv";
|
|
71
|
-
if (name.includes("webm")) return "webm";
|
|
72
|
-
if (name.includes("mp4") || name.includes("isom")) return "mp4";
|
|
73
|
-
if (name.includes("mov") || name.includes("quicktime")) return "mov";
|
|
74
|
-
if (name.includes("ogg")) return "ogg";
|
|
75
|
-
if (name.includes("wav")) return "wav";
|
|
76
|
-
if (name.includes("flac")) return "flac";
|
|
77
|
-
if (name.includes("mp3")) return "mp3";
|
|
78
|
-
if (name.includes("adts") || name.includes("aac")) return "adts";
|
|
79
|
-
if (name.includes("mpegts") || name.includes("mpeg-ts") || name.includes("transport")) return "mpegts";
|
|
80
|
-
return sniffed;
|
|
81
|
-
}
|
|
82
|
-
function mediabunnyVideoToAvbridge(c) {
|
|
83
|
-
switch (c) {
|
|
84
|
-
case "avc":
|
|
85
|
-
return "h264";
|
|
86
|
-
case "hevc":
|
|
87
|
-
return "h265";
|
|
88
|
-
case "vp8":
|
|
89
|
-
return "vp8";
|
|
90
|
-
case "vp9":
|
|
91
|
-
return "vp9";
|
|
92
|
-
case "av1":
|
|
93
|
-
return "av1";
|
|
94
|
-
default:
|
|
95
|
-
return c ? c : "unknown";
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
function avbridgeVideoToMediabunny(c) {
|
|
99
|
-
switch (c) {
|
|
100
|
-
case "h264":
|
|
101
|
-
return "avc";
|
|
102
|
-
case "h265":
|
|
103
|
-
return "hevc";
|
|
104
|
-
case "vp8":
|
|
105
|
-
return "vp8";
|
|
106
|
-
case "vp9":
|
|
107
|
-
return "vp9";
|
|
108
|
-
case "av1":
|
|
109
|
-
return "av1";
|
|
110
|
-
default:
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
function mediabunnyAudioToAvbridge(c) {
|
|
115
|
-
switch (c) {
|
|
116
|
-
case "aac":
|
|
117
|
-
return "aac";
|
|
118
|
-
case "mp3":
|
|
119
|
-
return "mp3";
|
|
120
|
-
case "opus":
|
|
121
|
-
return "opus";
|
|
122
|
-
case "vorbis":
|
|
123
|
-
return "vorbis";
|
|
124
|
-
case "flac":
|
|
125
|
-
return "flac";
|
|
126
|
-
case "ac3":
|
|
127
|
-
return "ac3";
|
|
128
|
-
case "eac3":
|
|
129
|
-
return "eac3";
|
|
130
|
-
default:
|
|
131
|
-
return c ? c : "unknown";
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
function avbridgeAudioToMediabunny(c) {
|
|
135
|
-
switch (c) {
|
|
136
|
-
case "aac":
|
|
137
|
-
return "aac";
|
|
138
|
-
case "mp3":
|
|
139
|
-
return "mp3";
|
|
140
|
-
case "opus":
|
|
141
|
-
return "opus";
|
|
142
|
-
case "vorbis":
|
|
143
|
-
return "vorbis";
|
|
144
|
-
case "flac":
|
|
145
|
-
return "flac";
|
|
146
|
-
case "ac3":
|
|
147
|
-
return "ac3";
|
|
148
|
-
case "eac3":
|
|
149
|
-
return "eac3";
|
|
150
|
-
default:
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
async function safeNumber(fn) {
|
|
155
|
-
try {
|
|
156
|
-
const v = await fn();
|
|
157
|
-
return typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
158
|
-
} catch {
|
|
159
|
-
return void 0;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
async function safe(fn) {
|
|
163
|
-
try {
|
|
164
|
-
return await fn();
|
|
165
|
-
} catch {
|
|
166
|
-
return void 0;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// src/probe/index.ts
|
|
171
|
-
var MEDIABUNNY_CONTAINERS = /* @__PURE__ */ new Set([
|
|
172
|
-
"mp4",
|
|
173
|
-
"mov",
|
|
174
|
-
"mkv",
|
|
175
|
-
"webm",
|
|
176
|
-
"ogg",
|
|
177
|
-
"wav",
|
|
178
|
-
"mp3",
|
|
179
|
-
"flac",
|
|
180
|
-
"adts",
|
|
181
|
-
"mpegts"
|
|
182
|
-
]);
|
|
183
|
-
async function probe(source) {
|
|
184
|
-
const normalized = await chunkHZLQNKFN_cjs.normalizeSource(source);
|
|
185
|
-
const sniffed = await chunkHZLQNKFN_cjs.sniffNormalizedSource(normalized);
|
|
186
|
-
if (MEDIABUNNY_CONTAINERS.has(sniffed)) {
|
|
187
|
-
try {
|
|
188
|
-
return await probeWithMediabunny(normalized, sniffed);
|
|
189
|
-
} catch (mediabunnyErr) {
|
|
190
|
-
console.warn(
|
|
191
|
-
`[avbridge] mediabunny rejected ${sniffed} file, falling back to libav:`,
|
|
192
|
-
mediabunnyErr.message
|
|
193
|
-
);
|
|
194
|
-
try {
|
|
195
|
-
const { probeWithLibav } = await import('./avi-6SJLWIWW.cjs');
|
|
196
|
-
return await probeWithLibav(normalized, sniffed);
|
|
197
|
-
} catch (libavErr) {
|
|
198
|
-
const mbMsg = mediabunnyErr.message || String(mediabunnyErr);
|
|
199
|
-
const lvMsg = libavErr instanceof Error ? libavErr.message : String(libavErr);
|
|
200
|
-
throw new Error(
|
|
201
|
-
`failed to probe ${sniffed} file. mediabunny: ${mbMsg}. libav fallback: ${lvMsg}.`
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
try {
|
|
207
|
-
const { probeWithLibav } = await import('./avi-6SJLWIWW.cjs');
|
|
208
|
-
return await probeWithLibav(normalized, sniffed);
|
|
209
|
-
} catch (err) {
|
|
210
|
-
const inner = err instanceof Error ? err.message : String(err);
|
|
211
|
-
console.error("[avbridge] libav probe failed for", sniffed, "file:", err);
|
|
212
|
-
throw new Error(
|
|
213
|
-
sniffed === "unknown" ? `unable to probe source: container could not be identified, and the libav.js fallback also failed: ${inner || "(no message \u2014 see browser console for the original error)"}` : `${sniffed.toUpperCase()} files require libav.js, which failed to load: ${inner || "(no message \u2014 see browser console for the original error)"}`
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
8
|
+
var chunkF3LQJKXK_cjs = require('./chunk-F3LQJKXK.cjs');
|
|
217
9
|
|
|
218
10
|
// src/util/codec-strings.ts
|
|
219
11
|
function videoCodecString(track) {
|
|
@@ -311,7 +103,9 @@ var FALLBACK_AUDIO_CODECS = /* @__PURE__ */ new Set([
|
|
|
311
103
|
"ra_144",
|
|
312
104
|
"ra_288",
|
|
313
105
|
"sipr",
|
|
314
|
-
"atrac3"
|
|
106
|
+
"atrac3",
|
|
107
|
+
"dts",
|
|
108
|
+
"truehd"
|
|
315
109
|
]);
|
|
316
110
|
var NATIVE_CONTAINERS = /* @__PURE__ */ new Set([
|
|
317
111
|
"mp4",
|
|
@@ -373,7 +167,16 @@ function classifyContext(ctx) {
|
|
|
373
167
|
reason: `video codec "${video.codec}" has no browser decoder; WASM fallback required`
|
|
374
168
|
};
|
|
375
169
|
}
|
|
376
|
-
|
|
170
|
+
const audioNeedsFallback = audio && (FALLBACK_AUDIO_CODECS.has(audio.codec) || !NATIVE_AUDIO_CODECS.has(audio.codec));
|
|
171
|
+
if (audioNeedsFallback) {
|
|
172
|
+
if (NATIVE_VIDEO_CODECS.has(video.codec) && webCodecsAvailable()) {
|
|
173
|
+
return {
|
|
174
|
+
class: "HYBRID_CANDIDATE",
|
|
175
|
+
strategy: "hybrid",
|
|
176
|
+
reason: `video "${video.codec}" is hardware-decodable via WebCodecs; audio "${audio.codec}" decoded in software by libav`,
|
|
177
|
+
fallbackChain: ["fallback"]
|
|
178
|
+
};
|
|
179
|
+
}
|
|
377
180
|
return {
|
|
378
181
|
class: "FALLBACK_REQUIRED",
|
|
379
182
|
strategy: "fallback",
|
|
@@ -453,36 +256,6 @@ function isRiskyNative(video) {
|
|
|
453
256
|
return false;
|
|
454
257
|
}
|
|
455
258
|
|
|
456
|
-
// src/subtitles/srt.ts
|
|
457
|
-
function srtToVtt(srt) {
|
|
458
|
-
if (srt.charCodeAt(0) === 65279) srt = srt.slice(1);
|
|
459
|
-
const normalized = srt.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
|
|
460
|
-
const blocks = normalized.split(/\n{2,}/);
|
|
461
|
-
const out = ["WEBVTT", ""];
|
|
462
|
-
for (const block of blocks) {
|
|
463
|
-
const lines = block.split("\n");
|
|
464
|
-
if (lines.length > 0 && /^\d+$/.test(lines[0].trim())) {
|
|
465
|
-
lines.shift();
|
|
466
|
-
}
|
|
467
|
-
if (lines.length === 0) continue;
|
|
468
|
-
const timing = lines.shift();
|
|
469
|
-
const vttTiming = convertTiming(timing);
|
|
470
|
-
if (!vttTiming) continue;
|
|
471
|
-
out.push(vttTiming);
|
|
472
|
-
for (const l of lines) out.push(l);
|
|
473
|
-
out.push("");
|
|
474
|
-
}
|
|
475
|
-
return out.join("\n");
|
|
476
|
-
}
|
|
477
|
-
function convertTiming(line) {
|
|
478
|
-
const m = /^(\d{1,2}):(\d{2}):(\d{2})[,.](\d{1,3})\s*-->\s*(\d{1,2}):(\d{2}):(\d{2})[,.](\d{1,3})(.*)$/.exec(
|
|
479
|
-
line.trim()
|
|
480
|
-
);
|
|
481
|
-
if (!m) return null;
|
|
482
|
-
const fmt = (h, mm, s, ms) => `${h.padStart(2, "0")}:${mm}:${s}.${ms.padEnd(3, "0").slice(0, 3)}`;
|
|
483
|
-
return `${fmt(m[1], m[2], m[3], m[4])} --> ${fmt(m[5], m[6], m[7], m[8])}${m[9] ?? ""}`;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
259
|
// src/events.ts
|
|
487
260
|
var TypedEmitter = class {
|
|
488
261
|
listeners = {};
|
|
@@ -688,7 +461,7 @@ async function createNativeSession(context, video) {
|
|
|
688
461
|
},
|
|
689
462
|
async setAudioTrack(id) {
|
|
690
463
|
const tracks = video.audioTracks;
|
|
691
|
-
if (!tracks) return;
|
|
464
|
+
if (!tracks || tracks.length === 0) return;
|
|
692
465
|
for (let i = 0; i < tracks.length; i++) {
|
|
693
466
|
tracks[i].enabled = tracks[i].id === String(id) || i === id;
|
|
694
467
|
}
|
|
@@ -740,10 +513,18 @@ var MseSink = class {
|
|
|
740
513
|
constructor(options) {
|
|
741
514
|
this.options = options;
|
|
742
515
|
if (typeof MediaSource === "undefined") {
|
|
743
|
-
throw new
|
|
516
|
+
throw new chunk2IJ66NTD_cjs.AvbridgeError(
|
|
517
|
+
chunk2IJ66NTD_cjs.ERR_MSE_NOT_SUPPORTED,
|
|
518
|
+
"MediaSource Extensions (MSE) are not supported in this environment.",
|
|
519
|
+
"MSE is required for the remux strategy. Use a browser that supports MSE, or try the fallback strategy."
|
|
520
|
+
);
|
|
744
521
|
}
|
|
745
522
|
if (!MediaSource.isTypeSupported(options.mime)) {
|
|
746
|
-
throw new
|
|
523
|
+
throw new chunk2IJ66NTD_cjs.AvbridgeError(
|
|
524
|
+
chunk2IJ66NTD_cjs.ERR_MSE_CODEC_NOT_SUPPORTED,
|
|
525
|
+
`This browser's MSE does not support "${options.mime}".`,
|
|
526
|
+
"The codec combination can't be played via remux in this browser. The player will try the next strategy automatically."
|
|
527
|
+
);
|
|
747
528
|
}
|
|
748
529
|
this.mediaSource = new MediaSource();
|
|
749
530
|
this.objectUrl = URL.createObjectURL(this.mediaSource);
|
|
@@ -928,30 +709,49 @@ var MseSink = class {
|
|
|
928
709
|
async function createRemuxPipeline(ctx, video) {
|
|
929
710
|
const mb = await import('mediabunny');
|
|
930
711
|
const videoTrackInfo = ctx.videoTracks[0];
|
|
931
|
-
const audioTrackInfo = ctx.audioTracks[0];
|
|
932
712
|
if (!videoTrackInfo) throw new Error("remux: source has no video track");
|
|
933
|
-
const mbVideoCodec = avbridgeVideoToMediabunny(videoTrackInfo.codec);
|
|
713
|
+
const mbVideoCodec = chunkZCUXHW55_cjs.avbridgeVideoToMediabunny(videoTrackInfo.codec);
|
|
934
714
|
if (!mbVideoCodec) {
|
|
935
715
|
throw new Error(`remux: video codec "${videoTrackInfo.codec}" is not supported by mediabunny output`);
|
|
936
716
|
}
|
|
937
|
-
const mbAudioCodec = audioTrackInfo ? avbridgeAudioToMediabunny(audioTrackInfo.codec) : null;
|
|
938
717
|
const input = new mb.Input({
|
|
939
|
-
source: await buildMediabunnySourceFromInput(mb, ctx.source),
|
|
718
|
+
source: await chunkZCUXHW55_cjs.buildMediabunnySourceFromInput(mb, ctx.source),
|
|
940
719
|
formats: mb.ALL_FORMATS
|
|
941
720
|
});
|
|
942
721
|
const allTracks = await input.getTracks();
|
|
943
722
|
const inputVideo = allTracks.find((t) => t.id === videoTrackInfo.id && t.isVideoTrack());
|
|
944
|
-
const inputAudio = audioTrackInfo ? allTracks.find((t) => t.id === audioTrackInfo.id && t.isAudioTrack()) : null;
|
|
945
723
|
if (!inputVideo || !inputVideo.isVideoTrack()) {
|
|
946
724
|
throw new Error("remux: video track not found in input");
|
|
947
725
|
}
|
|
948
|
-
if (audioTrackInfo && (!inputAudio || !inputAudio.isAudioTrack())) {
|
|
949
|
-
throw new Error("remux: audio track not found in input");
|
|
950
|
-
}
|
|
951
726
|
const videoConfig = await inputVideo.getDecoderConfig();
|
|
952
|
-
const audioConfig = inputAudio && inputAudio.isAudioTrack() ? await inputAudio.getDecoderConfig() : null;
|
|
953
727
|
const videoSink = new mb.EncodedPacketSink(inputVideo);
|
|
954
|
-
|
|
728
|
+
let selectedAudioTrackId = ctx.audioTracks[0]?.id ?? null;
|
|
729
|
+
let inputAudio = null;
|
|
730
|
+
let mbAudioCodec = null;
|
|
731
|
+
let audioSink = null;
|
|
732
|
+
let audioConfig = null;
|
|
733
|
+
async function rebuildAudio() {
|
|
734
|
+
if (selectedAudioTrackId == null) {
|
|
735
|
+
inputAudio = null;
|
|
736
|
+
mbAudioCodec = null;
|
|
737
|
+
audioSink = null;
|
|
738
|
+
audioConfig = null;
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const trackInfo = ctx.audioTracks.find((t) => t.id === selectedAudioTrackId);
|
|
742
|
+
if (!trackInfo) {
|
|
743
|
+
throw new Error(`remux: no audio track with id ${selectedAudioTrackId}`);
|
|
744
|
+
}
|
|
745
|
+
const newInput = allTracks.find((t) => t.id === trackInfo.id && t.isAudioTrack());
|
|
746
|
+
if (!newInput || !newInput.isAudioTrack()) {
|
|
747
|
+
throw new Error("remux: audio track not found in input");
|
|
748
|
+
}
|
|
749
|
+
inputAudio = newInput;
|
|
750
|
+
mbAudioCodec = chunkZCUXHW55_cjs.avbridgeAudioToMediabunny(trackInfo.codec);
|
|
751
|
+
audioSink = new mb.EncodedPacketSink(newInput);
|
|
752
|
+
audioConfig = await newInput.getDecoderConfig();
|
|
753
|
+
}
|
|
754
|
+
await rebuildAudio();
|
|
955
755
|
let sink = null;
|
|
956
756
|
const stats = { videoPackets: 0, audioPackets: 0, bytesWritten: 0, fragments: 0 };
|
|
957
757
|
let destroyed = false;
|
|
@@ -1076,6 +876,30 @@ async function createRemuxPipeline(ctx, video) {
|
|
|
1076
876
|
pendingAutoPlay = autoPlay;
|
|
1077
877
|
if (sink) sink.setPlayOnSeek(autoPlay);
|
|
1078
878
|
},
|
|
879
|
+
async setAudioTrack(trackId, time, autoPlay) {
|
|
880
|
+
if (selectedAudioTrackId === trackId) return;
|
|
881
|
+
if (!ctx.audioTracks.some((t) => t.id === trackId)) {
|
|
882
|
+
console.warn("[avbridge] remux: setAudioTrack \u2014 unknown track id", trackId);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
pumpToken++;
|
|
886
|
+
selectedAudioTrackId = trackId;
|
|
887
|
+
await rebuildAudio().catch((err) => {
|
|
888
|
+
console.warn("[avbridge] remux: rebuildAudio failed:", err.message);
|
|
889
|
+
});
|
|
890
|
+
if (sink) {
|
|
891
|
+
try {
|
|
892
|
+
sink.destroy();
|
|
893
|
+
} catch {
|
|
894
|
+
}
|
|
895
|
+
sink = null;
|
|
896
|
+
}
|
|
897
|
+
pendingAutoPlay = autoPlay;
|
|
898
|
+
pendingStartTime = time;
|
|
899
|
+
pumpLoop(++pumpToken, time).catch((err) => {
|
|
900
|
+
console.error("[avbridge] remux pipeline setAudioTrack pump failed:", err);
|
|
901
|
+
});
|
|
902
|
+
},
|
|
1079
903
|
async destroy() {
|
|
1080
904
|
destroyed = true;
|
|
1081
905
|
pumpToken++;
|
|
@@ -1135,7 +959,19 @@ async function createRemuxSession(context, video) {
|
|
|
1135
959
|
const wasPlaying = !video.paused;
|
|
1136
960
|
await pipeline.seek(time, wasPlaying || wantPlay);
|
|
1137
961
|
},
|
|
1138
|
-
async setAudioTrack(
|
|
962
|
+
async setAudioTrack(id) {
|
|
963
|
+
if (!context.audioTracks.some((t) => t.id === id)) {
|
|
964
|
+
console.warn("[avbridge] remux: setAudioTrack \u2014 unknown track id", id);
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
const wasPlaying = !video.paused;
|
|
968
|
+
const time = video.currentTime || 0;
|
|
969
|
+
if (!started) {
|
|
970
|
+
started = true;
|
|
971
|
+
await pipeline.setAudioTrack(id, time, wantPlay || wasPlaying);
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
await pipeline.setAudioTrack(id, time, wasPlaying || wantPlay);
|
|
1139
975
|
},
|
|
1140
976
|
async setSubtitleTrack(id) {
|
|
1141
977
|
const tracks = video.textTracks;
|
|
@@ -1159,6 +995,10 @@ async function createRemuxSession(context, video) {
|
|
|
1159
995
|
}
|
|
1160
996
|
|
|
1161
997
|
// src/strategies/fallback/video-renderer.ts
|
|
998
|
+
function isDebug() {
|
|
999
|
+
return typeof globalThis !== "undefined" && !!globalThis.AVBRIDGE_DEBUG;
|
|
1000
|
+
}
|
|
1001
|
+
var lastDebugLog = 0;
|
|
1162
1002
|
var VideoRenderer = class {
|
|
1163
1003
|
constructor(target, clock, fps = 30) {
|
|
1164
1004
|
this.target = target;
|
|
@@ -1184,6 +1024,9 @@ var VideoRenderer = class {
|
|
|
1184
1024
|
document.body.appendChild(this.canvas);
|
|
1185
1025
|
}
|
|
1186
1026
|
target.style.visibility = "hidden";
|
|
1027
|
+
const overlayParent = parent instanceof HTMLElement ? parent : document.body;
|
|
1028
|
+
this.subtitleOverlay = new chunkS4WAZC2T_cjs.SubtitleOverlay(overlayParent);
|
|
1029
|
+
this.watchTextTracks(target);
|
|
1187
1030
|
const ctx = this.canvas.getContext("2d");
|
|
1188
1031
|
if (!ctx) throw new Error("video renderer: failed to acquire 2D context");
|
|
1189
1032
|
this.ctx = ctx;
|
|
@@ -1205,6 +1048,29 @@ var VideoRenderer = class {
|
|
|
1205
1048
|
lastPaintWall = 0;
|
|
1206
1049
|
/** Minimum ms between paints — paces video at roughly source fps. */
|
|
1207
1050
|
paintIntervalMs;
|
|
1051
|
+
/** Cumulative count of frames skipped because all PTS are in the future. */
|
|
1052
|
+
ticksWaiting = 0;
|
|
1053
|
+
/** Cumulative count of ticks where PTS mode painted a frame. */
|
|
1054
|
+
ticksPainted = 0;
|
|
1055
|
+
/**
|
|
1056
|
+
* Subtitle overlay div attached to the stage wrapper alongside the
|
|
1057
|
+
* canvas. Created lazily when subtitle tracks are attached via the
|
|
1058
|
+
* target's `<track>` children. Canvas strategies (hybrid, fallback)
|
|
1059
|
+
* hide the <video>, so we can't rely on the browser's native cue
|
|
1060
|
+
* rendering; we read TextTrack.cues and render into this overlay.
|
|
1061
|
+
*/
|
|
1062
|
+
subtitleOverlay = null;
|
|
1063
|
+
subtitleTrack = null;
|
|
1064
|
+
/**
|
|
1065
|
+
* Calibration offset (microseconds) between video PTS and audio clock.
|
|
1066
|
+
* Video PTS and AudioContext.currentTime can drift ~0.1% relative to
|
|
1067
|
+
* each other (different clock domains). Over 45 minutes that's 2.6s.
|
|
1068
|
+
* We measure the offset on the first painted frame and update it
|
|
1069
|
+
* periodically so the PTS comparison stays calibrated.
|
|
1070
|
+
*/
|
|
1071
|
+
ptsCalibrationUs = 0;
|
|
1072
|
+
ptsCalibrated = false;
|
|
1073
|
+
lastCalibrationWall = 0;
|
|
1208
1074
|
/** Resolves once the first decoded frame has been enqueued. */
|
|
1209
1075
|
firstFrameReady;
|
|
1210
1076
|
resolveFirstFrame;
|
|
@@ -1238,9 +1104,80 @@ var VideoRenderer = class {
|
|
|
1238
1104
|
this.framesDroppedOverflow++;
|
|
1239
1105
|
}
|
|
1240
1106
|
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Watch the target <video>'s textTracks list. When a track is added,
|
|
1109
|
+
* grab it and start polling cues on each render tick. Existing tracks
|
|
1110
|
+
* (if any) are picked up immediately.
|
|
1111
|
+
*/
|
|
1112
|
+
watchTextTracks(target) {
|
|
1113
|
+
const pick = () => {
|
|
1114
|
+
if (this.subtitleTrack) return;
|
|
1115
|
+
const tracks = target.textTracks;
|
|
1116
|
+
if (isDebug()) {
|
|
1117
|
+
console.log(`[avbridge:subs] watchTextTracks pick() \u2014 ${tracks.length} tracks`);
|
|
1118
|
+
}
|
|
1119
|
+
for (let i = 0; i < tracks.length; i++) {
|
|
1120
|
+
const t = tracks[i];
|
|
1121
|
+
if (isDebug()) {
|
|
1122
|
+
console.log(`[avbridge:subs] track ${i}: kind=${t.kind} mode=${t.mode} cues=${t.cues?.length ?? 0}`);
|
|
1123
|
+
}
|
|
1124
|
+
if (t.kind === "subtitles" || t.kind === "captions") {
|
|
1125
|
+
this.subtitleTrack = t;
|
|
1126
|
+
t.mode = "hidden";
|
|
1127
|
+
if (isDebug()) {
|
|
1128
|
+
console.log(`[avbridge:subs] picked track, mode=hidden`);
|
|
1129
|
+
}
|
|
1130
|
+
const trackEl = target.querySelector(`track[srclang="${t.language}"]`);
|
|
1131
|
+
if (trackEl) {
|
|
1132
|
+
trackEl.addEventListener("load", () => {
|
|
1133
|
+
if (isDebug()) {
|
|
1134
|
+
console.log(`[avbridge:subs] track element loaded, cues=${t.cues?.length ?? 0}`);
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
trackEl.addEventListener("error", (ev) => {
|
|
1138
|
+
console.warn(`[avbridge:subs] track element error:`, ev);
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
break;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1145
|
+
pick();
|
|
1146
|
+
if (typeof target.textTracks.addEventListener === "function") {
|
|
1147
|
+
target.textTracks.addEventListener("addtrack", (e) => {
|
|
1148
|
+
if (isDebug()) {
|
|
1149
|
+
console.log("[avbridge:subs] addtrack event fired");
|
|
1150
|
+
}
|
|
1151
|
+
pick();
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
_loggedCues = false;
|
|
1156
|
+
/** Find the active cue (if any) for the given media time. */
|
|
1157
|
+
updateSubtitles() {
|
|
1158
|
+
if (!this.subtitleOverlay || !this.subtitleTrack) return;
|
|
1159
|
+
const cues = this.subtitleTrack.cues;
|
|
1160
|
+
if (!cues || cues.length === 0) return;
|
|
1161
|
+
if (isDebug() && !this._loggedCues) {
|
|
1162
|
+
this._loggedCues = true;
|
|
1163
|
+
console.log(`[avbridge:subs] cues available: ${cues.length}, first start=${cues[0].startTime}, last end=${cues[cues.length - 1].endTime}`);
|
|
1164
|
+
}
|
|
1165
|
+
const t = this.clock.now();
|
|
1166
|
+
let activeText = "";
|
|
1167
|
+
for (let i = 0; i < cues.length; i++) {
|
|
1168
|
+
const c = cues[i];
|
|
1169
|
+
if (t >= c.startTime && t <= c.endTime) {
|
|
1170
|
+
const vttCue = c;
|
|
1171
|
+
activeText = vttCue.text ?? "";
|
|
1172
|
+
break;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
this.subtitleOverlay.setText(activeText.replace(/<[^>]+>/g, ""));
|
|
1176
|
+
}
|
|
1241
1177
|
tick() {
|
|
1242
1178
|
if (this.destroyed) return;
|
|
1243
1179
|
this.rafHandle = requestAnimationFrame(this.tick);
|
|
1180
|
+
this.updateSubtitles();
|
|
1244
1181
|
if (this.queue.length === 0) return;
|
|
1245
1182
|
const playing = this.clock.isPlaying();
|
|
1246
1183
|
if (!playing) {
|
|
@@ -1253,21 +1190,81 @@ var VideoRenderer = class {
|
|
|
1253
1190
|
}
|
|
1254
1191
|
return;
|
|
1255
1192
|
}
|
|
1256
|
-
const
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
if (
|
|
1260
|
-
const
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
this.
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1193
|
+
const rawAudioNowUs = this.clock.now() * 1e6;
|
|
1194
|
+
const headTs = this.queue[0].timestamp ?? 0;
|
|
1195
|
+
const hasPts = headTs > 0 || this.queue.length > 1;
|
|
1196
|
+
if (hasPts) {
|
|
1197
|
+
const wallNow2 = performance.now();
|
|
1198
|
+
if (!this.ptsCalibrated || wallNow2 - this.lastCalibrationWall > 1e4) {
|
|
1199
|
+
this.ptsCalibrationUs = headTs - rawAudioNowUs;
|
|
1200
|
+
this.ptsCalibrated = true;
|
|
1201
|
+
this.lastCalibrationWall = wallNow2;
|
|
1202
|
+
}
|
|
1203
|
+
const audioNowUs = rawAudioNowUs + this.ptsCalibrationUs;
|
|
1204
|
+
const frameDurationUs = this.paintIntervalMs * 1e3;
|
|
1205
|
+
const deadlineUs = audioNowUs + frameDurationUs;
|
|
1206
|
+
let bestIdx = -1;
|
|
1207
|
+
for (let i = 0; i < this.queue.length; i++) {
|
|
1208
|
+
const ts = this.queue[i].timestamp ?? 0;
|
|
1209
|
+
if (ts <= deadlineUs) {
|
|
1210
|
+
bestIdx = i;
|
|
1211
|
+
} else {
|
|
1212
|
+
break;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
if (bestIdx < 0) {
|
|
1216
|
+
this.ticksWaiting++;
|
|
1217
|
+
if (isDebug()) {
|
|
1218
|
+
const now = performance.now();
|
|
1219
|
+
if (now - lastDebugLog > 1e3) {
|
|
1220
|
+
const headPtsMs = (headTs / 1e3).toFixed(1);
|
|
1221
|
+
const audioMs = (audioNowUs / 1e3).toFixed(1);
|
|
1222
|
+
const rawDriftMs = ((headTs - rawAudioNowUs) / 1e3).toFixed(1);
|
|
1223
|
+
const calibMs = (this.ptsCalibrationUs / 1e3).toFixed(1);
|
|
1224
|
+
console.log(
|
|
1225
|
+
`[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}`
|
|
1226
|
+
);
|
|
1227
|
+
lastDebugLog = now;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1268
1230
|
return;
|
|
1269
1231
|
}
|
|
1232
|
+
const dropThresholdUs = audioNowUs - frameDurationUs * 2;
|
|
1233
|
+
let dropped = 0;
|
|
1234
|
+
while (bestIdx > 0) {
|
|
1235
|
+
const ts = this.queue[0].timestamp ?? 0;
|
|
1236
|
+
if (ts < dropThresholdUs) {
|
|
1237
|
+
this.queue.shift()?.close();
|
|
1238
|
+
this.framesDroppedLate++;
|
|
1239
|
+
bestIdx--;
|
|
1240
|
+
dropped++;
|
|
1241
|
+
} else {
|
|
1242
|
+
break;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
this.ticksPainted++;
|
|
1246
|
+
if (isDebug()) {
|
|
1247
|
+
const now = performance.now();
|
|
1248
|
+
if (now - lastDebugLog > 1e3) {
|
|
1249
|
+
const paintedTs = this.queue[0]?.timestamp ?? 0;
|
|
1250
|
+
const audioMs = (audioNowUs / 1e3).toFixed(1);
|
|
1251
|
+
const ptsMs = (paintedTs / 1e3).toFixed(1);
|
|
1252
|
+
const rawDriftMs = ((paintedTs - rawAudioNowUs) / 1e3).toFixed(1);
|
|
1253
|
+
const calibMs = (this.ptsCalibrationUs / 1e3).toFixed(1);
|
|
1254
|
+
console.log(
|
|
1255
|
+
`[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}`
|
|
1256
|
+
);
|
|
1257
|
+
lastDebugLog = now;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
const frame2 = this.queue.shift();
|
|
1261
|
+
this.paint(frame2);
|
|
1262
|
+
frame2.close();
|
|
1263
|
+
this.lastPaintWall = performance.now();
|
|
1264
|
+
return;
|
|
1270
1265
|
}
|
|
1266
|
+
const wallNow = performance.now();
|
|
1267
|
+
if (wallNow - this.lastPaintWall < this.paintIntervalMs - 2) return;
|
|
1271
1268
|
const frame = this.queue.shift();
|
|
1272
1269
|
this.paint(frame);
|
|
1273
1270
|
frame.close();
|
|
@@ -1289,8 +1286,13 @@ var VideoRenderer = class {
|
|
|
1289
1286
|
}
|
|
1290
1287
|
/** Discard all queued frames. Used by seek to drop stale buffers. */
|
|
1291
1288
|
flush() {
|
|
1289
|
+
const count = this.queue.length;
|
|
1292
1290
|
while (this.queue.length > 0) this.queue.shift()?.close();
|
|
1293
1291
|
this.prerolled = false;
|
|
1292
|
+
this.ptsCalibrated = false;
|
|
1293
|
+
if (isDebug() && count > 0) {
|
|
1294
|
+
console.log(`[avbridge:renderer] FLUSH discarded=${count} painted=${this.framesPainted} drops=${this.framesDroppedLate}`);
|
|
1295
|
+
}
|
|
1294
1296
|
}
|
|
1295
1297
|
stats() {
|
|
1296
1298
|
return {
|
|
@@ -1304,6 +1306,11 @@ var VideoRenderer = class {
|
|
|
1304
1306
|
this.destroyed = true;
|
|
1305
1307
|
if (this.rafHandle != null) cancelAnimationFrame(this.rafHandle);
|
|
1306
1308
|
this.flush();
|
|
1309
|
+
if (this.subtitleOverlay) {
|
|
1310
|
+
this.subtitleOverlay.destroy();
|
|
1311
|
+
this.subtitleOverlay = null;
|
|
1312
|
+
}
|
|
1313
|
+
this.subtitleTrack = null;
|
|
1307
1314
|
this.canvas.remove();
|
|
1308
1315
|
this.target.style.visibility = "";
|
|
1309
1316
|
}
|
|
@@ -1335,11 +1342,38 @@ var AudioOutput = class {
|
|
|
1335
1342
|
pendingQueue = [];
|
|
1336
1343
|
framesScheduled = 0;
|
|
1337
1344
|
destroyed = false;
|
|
1345
|
+
/** User-set volume (0..1). Applied to the gain node. */
|
|
1346
|
+
_volume = 1;
|
|
1347
|
+
/** User-set muted flag. When true, gain is forced to 0. */
|
|
1348
|
+
_muted = false;
|
|
1338
1349
|
constructor() {
|
|
1339
1350
|
this.ctx = new AudioContext();
|
|
1340
1351
|
this.gain = this.ctx.createGain();
|
|
1341
1352
|
this.gain.connect(this.ctx.destination);
|
|
1342
1353
|
}
|
|
1354
|
+
/** Set volume (0..1). Applied immediately to the gain node. */
|
|
1355
|
+
setVolume(v) {
|
|
1356
|
+
this._volume = Math.max(0, Math.min(1, v));
|
|
1357
|
+
this.applyGain();
|
|
1358
|
+
}
|
|
1359
|
+
getVolume() {
|
|
1360
|
+
return this._volume;
|
|
1361
|
+
}
|
|
1362
|
+
/** Set muted. When true, output is silenced regardless of volume. */
|
|
1363
|
+
setMuted(m) {
|
|
1364
|
+
this._muted = m;
|
|
1365
|
+
this.applyGain();
|
|
1366
|
+
}
|
|
1367
|
+
getMuted() {
|
|
1368
|
+
return this._muted;
|
|
1369
|
+
}
|
|
1370
|
+
applyGain() {
|
|
1371
|
+
const target = this._muted ? 0 : this._volume;
|
|
1372
|
+
try {
|
|
1373
|
+
this.gain.gain.value = target;
|
|
1374
|
+
} catch {
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1343
1377
|
/**
|
|
1344
1378
|
* Switch into wall-clock fallback mode. Called by the decoder when no
|
|
1345
1379
|
* audio decoder could be initialized for the source. Once set, this
|
|
@@ -1489,6 +1523,7 @@ var AudioOutput = class {
|
|
|
1489
1523
|
}
|
|
1490
1524
|
this.gain = this.ctx.createGain();
|
|
1491
1525
|
this.gain.connect(this.ctx.destination);
|
|
1526
|
+
this.applyGain();
|
|
1492
1527
|
this.pendingQueue = [];
|
|
1493
1528
|
this.mediaTimeOfAnchor = newMediaTime;
|
|
1494
1529
|
this.mediaTimeOfNext = newMediaTime;
|
|
@@ -1517,15 +1552,16 @@ var AudioOutput = class {
|
|
|
1517
1552
|
|
|
1518
1553
|
// src/strategies/hybrid/decoder.ts
|
|
1519
1554
|
async function startHybridDecoder(opts) {
|
|
1520
|
-
const variant =
|
|
1555
|
+
const variant = chunkF3LQJKXK_cjs.pickLibavVariant(opts.context);
|
|
1521
1556
|
const libav = await chunkG4APZMCP_cjs.loadLibav(variant);
|
|
1522
1557
|
const bridge = await loadBridge();
|
|
1523
|
-
const { prepareLibavInput } = await import('./libav-http-reader-
|
|
1524
|
-
const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source);
|
|
1558
|
+
const { prepareLibavInput } = await import('./libav-http-reader-AZLE7YFS.cjs');
|
|
1559
|
+
const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source, opts.transport);
|
|
1525
1560
|
const readPkt = await libav.av_packet_alloc();
|
|
1526
1561
|
const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
|
|
1527
1562
|
const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
|
|
1528
|
-
const
|
|
1563
|
+
const firstAudioTrackId = opts.context.audioTracks[0]?.id;
|
|
1564
|
+
let audioStream = (firstAudioTrackId != null ? streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO && s.index === firstAudioTrackId) : void 0) ?? streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO) ?? null;
|
|
1529
1565
|
if (!videoStream && !audioStream) {
|
|
1530
1566
|
throw new Error("hybrid decoder: file has no decodable streams");
|
|
1531
1567
|
}
|
|
@@ -1592,6 +1628,56 @@ async function startHybridDecoder(opts) {
|
|
|
1592
1628
|
});
|
|
1593
1629
|
throw new Error("hybrid decoder: could not initialize any decoders");
|
|
1594
1630
|
}
|
|
1631
|
+
let bsfCtx = null;
|
|
1632
|
+
let bsfPkt = null;
|
|
1633
|
+
if (videoStream && opts.context.videoTracks[0]?.codec === "mpeg4") {
|
|
1634
|
+
try {
|
|
1635
|
+
bsfCtx = await libav.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
|
|
1636
|
+
if (bsfCtx != null && bsfCtx >= 0) {
|
|
1637
|
+
const parIn = await libav.AVBSFContext_par_in(bsfCtx);
|
|
1638
|
+
await libav.avcodec_parameters_copy(parIn, videoStream.codecpar);
|
|
1639
|
+
await libav.av_bsf_init(bsfCtx);
|
|
1640
|
+
bsfPkt = await libav.av_packet_alloc();
|
|
1641
|
+
chunkG4APZMCP_cjs.dbg.info("bsf", "mpeg4_unpack_bframes BSF active (hybrid)");
|
|
1642
|
+
} else {
|
|
1643
|
+
console.warn("[avbridge] mpeg4_unpack_bframes BSF not available in hybrid decoder");
|
|
1644
|
+
bsfCtx = null;
|
|
1645
|
+
}
|
|
1646
|
+
} catch (err) {
|
|
1647
|
+
console.warn("[avbridge] hybrid: failed to init BSF:", err.message);
|
|
1648
|
+
bsfCtx = null;
|
|
1649
|
+
bsfPkt = null;
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
async function applyBSF(packets) {
|
|
1653
|
+
if (!bsfCtx || !bsfPkt) return packets;
|
|
1654
|
+
const out = [];
|
|
1655
|
+
for (const pkt of packets) {
|
|
1656
|
+
await libav.ff_copyin_packet(bsfPkt, pkt);
|
|
1657
|
+
const sendErr = await libav.av_bsf_send_packet(bsfCtx, bsfPkt);
|
|
1658
|
+
if (sendErr < 0) {
|
|
1659
|
+
out.push(pkt);
|
|
1660
|
+
continue;
|
|
1661
|
+
}
|
|
1662
|
+
while (true) {
|
|
1663
|
+
const recvErr = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
|
|
1664
|
+
if (recvErr < 0) break;
|
|
1665
|
+
out.push(await libav.ff_copyout_packet(bsfPkt));
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
return out;
|
|
1669
|
+
}
|
|
1670
|
+
async function flushBSF() {
|
|
1671
|
+
if (!bsfCtx || !bsfPkt) return;
|
|
1672
|
+
try {
|
|
1673
|
+
await libav.av_bsf_send_packet(bsfCtx, 0);
|
|
1674
|
+
while (true) {
|
|
1675
|
+
const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
|
|
1676
|
+
if (err < 0) break;
|
|
1677
|
+
}
|
|
1678
|
+
} catch {
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1595
1681
|
let destroyed = false;
|
|
1596
1682
|
let pumpToken = 0;
|
|
1597
1683
|
let pumpRunning = null;
|
|
@@ -1619,10 +1705,17 @@ async function startHybridDecoder(opts) {
|
|
|
1619
1705
|
if (myToken !== pumpToken || destroyed) return;
|
|
1620
1706
|
const videoPackets = videoStream ? packets[videoStream.index] : void 0;
|
|
1621
1707
|
const audioPackets = audioStream ? packets[audioStream.index] : void 0;
|
|
1708
|
+
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
1709
|
+
await decodeAudioBatch(audioPackets, myToken);
|
|
1710
|
+
}
|
|
1711
|
+
if (myToken !== pumpToken || destroyed) return;
|
|
1712
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
1713
|
+
if (myToken !== pumpToken || destroyed) return;
|
|
1622
1714
|
if (videoDecoder && videoPackets && videoPackets.length > 0) {
|
|
1623
|
-
|
|
1715
|
+
const processed = await applyBSF(videoPackets);
|
|
1716
|
+
for (const pkt of processed) {
|
|
1624
1717
|
if (myToken !== pumpToken || destroyed) return;
|
|
1625
|
-
sanitizePacketTimestamp(pkt, () => {
|
|
1718
|
+
chunkCPZ7PXAM_cjs.sanitizePacketTimestamp(pkt, () => {
|
|
1626
1719
|
const ts = syntheticVideoUs;
|
|
1627
1720
|
syntheticVideoUs += videoFrameStepUs;
|
|
1628
1721
|
return ts;
|
|
@@ -1640,9 +1733,6 @@ async function startHybridDecoder(opts) {
|
|
|
1640
1733
|
}
|
|
1641
1734
|
}
|
|
1642
1735
|
}
|
|
1643
|
-
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
1644
|
-
await decodeAudioBatch(audioPackets, myToken);
|
|
1645
|
-
}
|
|
1646
1736
|
packetsRead += (videoPackets?.length ?? 0) + (audioPackets?.length ?? 0);
|
|
1647
1737
|
while (!destroyed && myToken === pumpToken && (videoDecoder && videoDecoder.decodeQueueSize > 10 || opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
|
|
1648
1738
|
await new Promise((r) => setTimeout(r, 50));
|
|
@@ -1665,23 +1755,46 @@ async function startHybridDecoder(opts) {
|
|
|
1665
1755
|
}
|
|
1666
1756
|
async function decodeAudioBatch(pkts, myToken, flush = false) {
|
|
1667
1757
|
if (!audioDec || destroyed || myToken !== pumpToken) return;
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1758
|
+
const AUDIO_SUB_BATCH = 4;
|
|
1759
|
+
let allFrames = [];
|
|
1760
|
+
for (let i = 0; i < pkts.length; i += AUDIO_SUB_BATCH) {
|
|
1761
|
+
if (myToken !== pumpToken || destroyed) return;
|
|
1762
|
+
const slice = pkts.slice(i, i + AUDIO_SUB_BATCH);
|
|
1763
|
+
const isLast = i + AUDIO_SUB_BATCH >= pkts.length;
|
|
1764
|
+
try {
|
|
1765
|
+
const frames2 = await libav.ff_decode_multi(
|
|
1766
|
+
audioDec.c,
|
|
1767
|
+
audioDec.pkt,
|
|
1768
|
+
audioDec.frame,
|
|
1769
|
+
slice,
|
|
1770
|
+
isLast && flush ? { fin: true, ignoreErrors: true } : { ignoreErrors: true }
|
|
1771
|
+
);
|
|
1772
|
+
allFrames = allFrames.concat(frames2);
|
|
1773
|
+
} catch (err) {
|
|
1774
|
+
console.error("[avbridge] hybrid audio decode failed:", err);
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
if (!isLast) await new Promise((r) => setTimeout(r, 0));
|
|
1778
|
+
}
|
|
1779
|
+
if (pkts.length === 0 && flush) {
|
|
1780
|
+
try {
|
|
1781
|
+
allFrames = await libav.ff_decode_multi(
|
|
1782
|
+
audioDec.c,
|
|
1783
|
+
audioDec.pkt,
|
|
1784
|
+
audioDec.frame,
|
|
1785
|
+
[],
|
|
1786
|
+
{ fin: true, ignoreErrors: true }
|
|
1787
|
+
);
|
|
1788
|
+
} catch (err) {
|
|
1789
|
+
console.error("[avbridge] hybrid audio flush failed:", err);
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1680
1792
|
}
|
|
1681
1793
|
if (myToken !== pumpToken || destroyed) return;
|
|
1794
|
+
const frames = allFrames;
|
|
1682
1795
|
for (const f of frames) {
|
|
1683
1796
|
if (myToken !== pumpToken || destroyed) return;
|
|
1684
|
-
sanitizeFrameTimestamp(
|
|
1797
|
+
chunkCPZ7PXAM_cjs.sanitizeFrameTimestamp(
|
|
1685
1798
|
f,
|
|
1686
1799
|
() => {
|
|
1687
1800
|
const ts = syntheticAudioUs;
|
|
@@ -1692,7 +1805,7 @@ async function startHybridDecoder(opts) {
|
|
|
1692
1805
|
},
|
|
1693
1806
|
audioTimeBase
|
|
1694
1807
|
);
|
|
1695
|
-
const samples = libavFrameToInterleavedFloat32(f);
|
|
1808
|
+
const samples = chunkCPZ7PXAM_cjs.libavFrameToInterleavedFloat32(f);
|
|
1696
1809
|
if (samples) {
|
|
1697
1810
|
opts.audio.schedule(samples.data, samples.channels, samples.sampleRate);
|
|
1698
1811
|
audioFramesDecoded++;
|
|
@@ -1715,6 +1828,14 @@ async function startHybridDecoder(opts) {
|
|
|
1715
1828
|
await pumpRunning;
|
|
1716
1829
|
} catch {
|
|
1717
1830
|
}
|
|
1831
|
+
try {
|
|
1832
|
+
if (bsfCtx) await libav.av_bsf_free(bsfCtx);
|
|
1833
|
+
} catch {
|
|
1834
|
+
}
|
|
1835
|
+
try {
|
|
1836
|
+
if (bsfPkt) await libav.av_packet_free?.(bsfPkt);
|
|
1837
|
+
} catch {
|
|
1838
|
+
}
|
|
1718
1839
|
try {
|
|
1719
1840
|
if (videoDecoder && videoDecoder.state !== "closed") videoDecoder.close();
|
|
1720
1841
|
} catch {
|
|
@@ -1736,6 +1857,71 @@ async function startHybridDecoder(opts) {
|
|
|
1736
1857
|
} catch {
|
|
1737
1858
|
}
|
|
1738
1859
|
},
|
|
1860
|
+
async setAudioTrack(trackId, timeSec) {
|
|
1861
|
+
if (audioStream && audioStream.index === trackId) return;
|
|
1862
|
+
const newStream = streams.find(
|
|
1863
|
+
(s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO && s.index === trackId
|
|
1864
|
+
);
|
|
1865
|
+
if (!newStream) {
|
|
1866
|
+
console.warn("[avbridge] hybrid: setAudioTrack \u2014 no stream with id", trackId);
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
const newToken = ++pumpToken;
|
|
1870
|
+
if (pumpRunning) {
|
|
1871
|
+
try {
|
|
1872
|
+
await pumpRunning;
|
|
1873
|
+
} catch {
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
if (destroyed) return;
|
|
1877
|
+
if (audioDec) {
|
|
1878
|
+
try {
|
|
1879
|
+
await libav.ff_free_decoder?.(audioDec.c, audioDec.pkt, audioDec.frame);
|
|
1880
|
+
} catch {
|
|
1881
|
+
}
|
|
1882
|
+
audioDec = null;
|
|
1883
|
+
}
|
|
1884
|
+
try {
|
|
1885
|
+
const [, c, pkt, frame] = await libav.ff_init_decoder(newStream.codec_id, {
|
|
1886
|
+
codecpar: newStream.codecpar
|
|
1887
|
+
});
|
|
1888
|
+
audioDec = { c, pkt, frame };
|
|
1889
|
+
audioTimeBase = newStream.time_base_num && newStream.time_base_den ? [newStream.time_base_num, newStream.time_base_den] : void 0;
|
|
1890
|
+
} catch (err) {
|
|
1891
|
+
console.warn(
|
|
1892
|
+
"[avbridge] hybrid: setAudioTrack init failed \u2014 switching to no-audio:",
|
|
1893
|
+
err.message
|
|
1894
|
+
);
|
|
1895
|
+
audioDec = null;
|
|
1896
|
+
opts.audio.setNoAudio();
|
|
1897
|
+
}
|
|
1898
|
+
audioStream = newStream;
|
|
1899
|
+
try {
|
|
1900
|
+
const tsUs = Math.floor(timeSec * 1e6);
|
|
1901
|
+
const [tsLo, tsHi] = libav.f64toi64 ? libav.f64toi64(tsUs) : [tsUs | 0, Math.floor(tsUs / 4294967296)];
|
|
1902
|
+
await libav.av_seek_frame(
|
|
1903
|
+
fmt_ctx,
|
|
1904
|
+
-1,
|
|
1905
|
+
tsLo,
|
|
1906
|
+
tsHi,
|
|
1907
|
+
libav.AVSEEK_FLAG_BACKWARD ?? 0
|
|
1908
|
+
);
|
|
1909
|
+
} catch (err) {
|
|
1910
|
+
console.warn("[avbridge] hybrid: setAudioTrack seek failed:", err);
|
|
1911
|
+
}
|
|
1912
|
+
try {
|
|
1913
|
+
if (videoDecoder && videoDecoder.state === "configured") {
|
|
1914
|
+
await videoDecoder.flush();
|
|
1915
|
+
}
|
|
1916
|
+
} catch {
|
|
1917
|
+
}
|
|
1918
|
+
await flushBSF();
|
|
1919
|
+
syntheticVideoUs = Math.round(timeSec * 1e6);
|
|
1920
|
+
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
1921
|
+
pumpRunning = pumpLoop(newToken).catch(
|
|
1922
|
+
(err) => console.error("[avbridge] hybrid pump failed (post-setAudioTrack):", err)
|
|
1923
|
+
);
|
|
1924
|
+
},
|
|
1739
1925
|
async seek(timeSec) {
|
|
1740
1926
|
const newToken = ++pumpToken;
|
|
1741
1927
|
if (pumpRunning) {
|
|
@@ -1768,6 +1954,7 @@ async function startHybridDecoder(opts) {
|
|
|
1768
1954
|
if (audioDec) await libav.avcodec_flush_buffers?.(audioDec.c);
|
|
1769
1955
|
} catch {
|
|
1770
1956
|
}
|
|
1957
|
+
await flushBSF();
|
|
1771
1958
|
syntheticVideoUs = Math.round(timeSec * 1e6);
|
|
1772
1959
|
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
1773
1960
|
pumpRunning = pumpLoop(newToken).catch(
|
|
@@ -1781,6 +1968,7 @@ async function startHybridDecoder(opts) {
|
|
|
1781
1968
|
videoFramesDecoded,
|
|
1782
1969
|
videoChunksFed,
|
|
1783
1970
|
audioFramesDecoded,
|
|
1971
|
+
bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
|
|
1784
1972
|
videoDecodeQueueSize: videoDecoder?.decodeQueueSize ?? 0,
|
|
1785
1973
|
// Confirmed transport info — see fallback decoder for the pattern.
|
|
1786
1974
|
_transport: inputHandle.transport === "http-range" ? "http-range" : "memory",
|
|
@@ -1791,161 +1979,9 @@ async function startHybridDecoder(opts) {
|
|
|
1791
1979
|
}
|
|
1792
1980
|
};
|
|
1793
1981
|
}
|
|
1794
|
-
function sanitizePacketTimestamp(pkt, nextUs, fallbackTimeBase) {
|
|
1795
|
-
const lo = pkt.pts ?? 0;
|
|
1796
|
-
const hi = pkt.ptshi ?? 0;
|
|
1797
|
-
const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
|
|
1798
|
-
if (isInvalid) {
|
|
1799
|
-
const us2 = nextUs();
|
|
1800
|
-
pkt.pts = us2;
|
|
1801
|
-
pkt.ptshi = 0;
|
|
1802
|
-
pkt.time_base_num = 1;
|
|
1803
|
-
pkt.time_base_den = 1e6;
|
|
1804
|
-
return;
|
|
1805
|
-
}
|
|
1806
|
-
const tb = fallbackTimeBase ?? [1, 1e6];
|
|
1807
|
-
const pts64 = hi * 4294967296 + lo;
|
|
1808
|
-
const us = Math.round(pts64 * 1e6 * tb[0] / tb[1]);
|
|
1809
|
-
if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
|
|
1810
|
-
pkt.pts = us;
|
|
1811
|
-
pkt.ptshi = us < 0 ? -1 : 0;
|
|
1812
|
-
pkt.time_base_num = 1;
|
|
1813
|
-
pkt.time_base_den = 1e6;
|
|
1814
|
-
return;
|
|
1815
|
-
}
|
|
1816
|
-
const fallback = nextUs();
|
|
1817
|
-
pkt.pts = fallback;
|
|
1818
|
-
pkt.ptshi = 0;
|
|
1819
|
-
pkt.time_base_num = 1;
|
|
1820
|
-
pkt.time_base_den = 1e6;
|
|
1821
|
-
}
|
|
1822
|
-
function sanitizeFrameTimestamp(frame, nextUs, fallbackTimeBase) {
|
|
1823
|
-
const lo = frame.pts ?? 0;
|
|
1824
|
-
const hi = frame.ptshi ?? 0;
|
|
1825
|
-
const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
|
|
1826
|
-
if (isInvalid) {
|
|
1827
|
-
const us2 = nextUs();
|
|
1828
|
-
frame.pts = us2;
|
|
1829
|
-
frame.ptshi = 0;
|
|
1830
|
-
return;
|
|
1831
|
-
}
|
|
1832
|
-
const tb = fallbackTimeBase ?? [1, 1e6];
|
|
1833
|
-
const pts64 = hi * 4294967296 + lo;
|
|
1834
|
-
const us = Math.round(pts64 * 1e6 * tb[0] / tb[1]);
|
|
1835
|
-
if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
|
|
1836
|
-
frame.pts = us;
|
|
1837
|
-
frame.ptshi = us < 0 ? -1 : 0;
|
|
1838
|
-
return;
|
|
1839
|
-
}
|
|
1840
|
-
const fallback = nextUs();
|
|
1841
|
-
frame.pts = fallback;
|
|
1842
|
-
frame.ptshi = 0;
|
|
1843
|
-
}
|
|
1844
|
-
var AV_SAMPLE_FMT_U8 = 0;
|
|
1845
|
-
var AV_SAMPLE_FMT_S16 = 1;
|
|
1846
|
-
var AV_SAMPLE_FMT_S32 = 2;
|
|
1847
|
-
var AV_SAMPLE_FMT_FLT = 3;
|
|
1848
|
-
var AV_SAMPLE_FMT_U8P = 5;
|
|
1849
|
-
var AV_SAMPLE_FMT_S16P = 6;
|
|
1850
|
-
var AV_SAMPLE_FMT_S32P = 7;
|
|
1851
|
-
var AV_SAMPLE_FMT_FLTP = 8;
|
|
1852
|
-
function libavFrameToInterleavedFloat32(frame) {
|
|
1853
|
-
const channels = frame.channels ?? frame.ch_layout_nb_channels ?? 1;
|
|
1854
|
-
const sampleRate = frame.sample_rate ?? 44100;
|
|
1855
|
-
const nbSamples = frame.nb_samples ?? 0;
|
|
1856
|
-
if (nbSamples === 0) return null;
|
|
1857
|
-
const out = new Float32Array(nbSamples * channels);
|
|
1858
|
-
switch (frame.format) {
|
|
1859
|
-
case AV_SAMPLE_FMT_FLTP: {
|
|
1860
|
-
const planes = ensurePlanes(frame.data, channels);
|
|
1861
|
-
for (let ch = 0; ch < channels; ch++) {
|
|
1862
|
-
const plane = asFloat32(planes[ch]);
|
|
1863
|
-
for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i];
|
|
1864
|
-
}
|
|
1865
|
-
return { data: out, channels, sampleRate };
|
|
1866
|
-
}
|
|
1867
|
-
case AV_SAMPLE_FMT_FLT: {
|
|
1868
|
-
const flat = asFloat32(frame.data);
|
|
1869
|
-
for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i];
|
|
1870
|
-
return { data: out, channels, sampleRate };
|
|
1871
|
-
}
|
|
1872
|
-
case AV_SAMPLE_FMT_S16P: {
|
|
1873
|
-
const planes = ensurePlanes(frame.data, channels);
|
|
1874
|
-
for (let ch = 0; ch < channels; ch++) {
|
|
1875
|
-
const plane = asInt16(planes[ch]);
|
|
1876
|
-
for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 32768;
|
|
1877
|
-
}
|
|
1878
|
-
return { data: out, channels, sampleRate };
|
|
1879
|
-
}
|
|
1880
|
-
case AV_SAMPLE_FMT_S16: {
|
|
1881
|
-
const flat = asInt16(frame.data);
|
|
1882
|
-
for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 32768;
|
|
1883
|
-
return { data: out, channels, sampleRate };
|
|
1884
|
-
}
|
|
1885
|
-
case AV_SAMPLE_FMT_S32P: {
|
|
1886
|
-
const planes = ensurePlanes(frame.data, channels);
|
|
1887
|
-
for (let ch = 0; ch < channels; ch++) {
|
|
1888
|
-
const plane = asInt32(planes[ch]);
|
|
1889
|
-
for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 2147483648;
|
|
1890
|
-
}
|
|
1891
|
-
return { data: out, channels, sampleRate };
|
|
1892
|
-
}
|
|
1893
|
-
case AV_SAMPLE_FMT_S32: {
|
|
1894
|
-
const flat = asInt32(frame.data);
|
|
1895
|
-
for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 2147483648;
|
|
1896
|
-
return { data: out, channels, sampleRate };
|
|
1897
|
-
}
|
|
1898
|
-
case AV_SAMPLE_FMT_U8P: {
|
|
1899
|
-
const planes = ensurePlanes(frame.data, channels);
|
|
1900
|
-
for (let ch = 0; ch < channels; ch++) {
|
|
1901
|
-
const plane = asUint8(planes[ch]);
|
|
1902
|
-
for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = (plane[i] - 128) / 128;
|
|
1903
|
-
}
|
|
1904
|
-
return { data: out, channels, sampleRate };
|
|
1905
|
-
}
|
|
1906
|
-
case AV_SAMPLE_FMT_U8: {
|
|
1907
|
-
const flat = asUint8(frame.data);
|
|
1908
|
-
for (let i = 0; i < nbSamples * channels; i++) out[i] = (flat[i] - 128) / 128;
|
|
1909
|
-
return { data: out, channels, sampleRate };
|
|
1910
|
-
}
|
|
1911
|
-
default:
|
|
1912
|
-
return null;
|
|
1913
|
-
}
|
|
1914
|
-
}
|
|
1915
|
-
function ensurePlanes(data, channels) {
|
|
1916
|
-
if (Array.isArray(data)) return data;
|
|
1917
|
-
const arr = data;
|
|
1918
|
-
const len = arr.length;
|
|
1919
|
-
const perChannel = Math.floor(len / channels);
|
|
1920
|
-
const planes = [];
|
|
1921
|
-
for (let ch = 0; ch < channels; ch++) {
|
|
1922
|
-
planes.push(arr.subarray ? arr.subarray(ch * perChannel, (ch + 1) * perChannel) : arr);
|
|
1923
|
-
}
|
|
1924
|
-
return planes;
|
|
1925
|
-
}
|
|
1926
|
-
function asFloat32(x) {
|
|
1927
|
-
if (x instanceof Float32Array) return x;
|
|
1928
|
-
const ta = x;
|
|
1929
|
-
return new Float32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
|
|
1930
|
-
}
|
|
1931
|
-
function asInt16(x) {
|
|
1932
|
-
if (x instanceof Int16Array) return x;
|
|
1933
|
-
const ta = x;
|
|
1934
|
-
return new Int16Array(ta.buffer, ta.byteOffset, ta.byteLength / 2);
|
|
1935
|
-
}
|
|
1936
|
-
function asInt32(x) {
|
|
1937
|
-
if (x instanceof Int32Array) return x;
|
|
1938
|
-
const ta = x;
|
|
1939
|
-
return new Int32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
|
|
1940
|
-
}
|
|
1941
|
-
function asUint8(x) {
|
|
1942
|
-
if (x instanceof Uint8Array) return x;
|
|
1943
|
-
const ta = x;
|
|
1944
|
-
return new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength);
|
|
1945
|
-
}
|
|
1946
1982
|
async function loadBridge() {
|
|
1947
1983
|
try {
|
|
1948
|
-
const wrapper = await import('./libav-import-
|
|
1984
|
+
const wrapper = await import('./libav-import-2ZVKV2E7.cjs');
|
|
1949
1985
|
return wrapper.libavBridge;
|
|
1950
1986
|
} catch (err) {
|
|
1951
1987
|
throw new Error(
|
|
@@ -1957,9 +1993,9 @@ async function loadBridge() {
|
|
|
1957
1993
|
// src/strategies/hybrid/index.ts
|
|
1958
1994
|
var READY_AUDIO_BUFFER_SECONDS = 0.3;
|
|
1959
1995
|
var READY_TIMEOUT_SECONDS = 10;
|
|
1960
|
-
async function createHybridSession(ctx, target) {
|
|
1961
|
-
const { normalizeSource
|
|
1962
|
-
const source = await
|
|
1996
|
+
async function createHybridSession(ctx, target, transport) {
|
|
1997
|
+
const { normalizeSource } = await import('./source-VFLXLOCN.cjs');
|
|
1998
|
+
const source = await normalizeSource(ctx.source);
|
|
1963
1999
|
const fps = ctx.videoTracks[0]?.fps ?? 30;
|
|
1964
2000
|
const audio = new AudioOutput();
|
|
1965
2001
|
const renderer = new VideoRenderer(target, audio, fps);
|
|
@@ -1970,7 +2006,8 @@ async function createHybridSession(ctx, target) {
|
|
|
1970
2006
|
filename: ctx.name ?? "input.bin",
|
|
1971
2007
|
context: ctx,
|
|
1972
2008
|
renderer,
|
|
1973
|
-
audio
|
|
2009
|
+
audio,
|
|
2010
|
+
transport
|
|
1974
2011
|
});
|
|
1975
2012
|
} catch (err) {
|
|
1976
2013
|
audio.destroy();
|
|
@@ -1988,6 +2025,22 @@ async function createHybridSession(ctx, target) {
|
|
|
1988
2025
|
configurable: true,
|
|
1989
2026
|
get: () => !audio.isPlaying()
|
|
1990
2027
|
});
|
|
2028
|
+
Object.defineProperty(target, "volume", {
|
|
2029
|
+
configurable: true,
|
|
2030
|
+
get: () => audio.getVolume(),
|
|
2031
|
+
set: (v) => {
|
|
2032
|
+
audio.setVolume(v);
|
|
2033
|
+
target.dispatchEvent(new Event("volumechange"));
|
|
2034
|
+
}
|
|
2035
|
+
});
|
|
2036
|
+
Object.defineProperty(target, "muted", {
|
|
2037
|
+
configurable: true,
|
|
2038
|
+
get: () => audio.getMuted(),
|
|
2039
|
+
set: (m) => {
|
|
2040
|
+
audio.setMuted(m);
|
|
2041
|
+
target.dispatchEvent(new Event("volumechange"));
|
|
2042
|
+
}
|
|
2043
|
+
});
|
|
1991
2044
|
if (ctx.duration && Number.isFinite(ctx.duration)) {
|
|
1992
2045
|
Object.defineProperty(target, "duration", {
|
|
1993
2046
|
configurable: true,
|
|
@@ -2027,15 +2080,35 @@ async function createHybridSession(ctx, target) {
|
|
|
2027
2080
|
if (!audio.isPlaying()) {
|
|
2028
2081
|
await waitForBuffer();
|
|
2029
2082
|
await audio.start();
|
|
2083
|
+
target.dispatchEvent(new Event("play"));
|
|
2084
|
+
target.dispatchEvent(new Event("playing"));
|
|
2030
2085
|
}
|
|
2031
2086
|
},
|
|
2032
2087
|
pause() {
|
|
2033
2088
|
void audio.pause();
|
|
2089
|
+
target.dispatchEvent(new Event("pause"));
|
|
2034
2090
|
},
|
|
2035
2091
|
async seek(time) {
|
|
2036
2092
|
await doSeek(time);
|
|
2037
2093
|
},
|
|
2038
|
-
async setAudioTrack(
|
|
2094
|
+
async setAudioTrack(id) {
|
|
2095
|
+
if (!ctx.audioTracks.some((t) => t.id === id)) {
|
|
2096
|
+
console.warn("[avbridge] hybrid: setAudioTrack \u2014 unknown track id", id);
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
const wasPlaying = audio.isPlaying();
|
|
2100
|
+
const currentTime = audio.now();
|
|
2101
|
+
await audio.pause().catch(() => {
|
|
2102
|
+
});
|
|
2103
|
+
await handles.setAudioTrack(id, currentTime).catch(
|
|
2104
|
+
(err) => console.warn("[avbridge] hybrid: handles.setAudioTrack failed:", err)
|
|
2105
|
+
);
|
|
2106
|
+
await audio.reset(currentTime);
|
|
2107
|
+
renderer.flush();
|
|
2108
|
+
if (wasPlaying) {
|
|
2109
|
+
await waitForBuffer();
|
|
2110
|
+
await audio.start();
|
|
2111
|
+
}
|
|
2039
2112
|
},
|
|
2040
2113
|
async setSubtitleTrack(_id) {
|
|
2041
2114
|
},
|
|
@@ -2053,6 +2126,8 @@ async function createHybridSession(ctx, target) {
|
|
|
2053
2126
|
delete target.currentTime;
|
|
2054
2127
|
delete target.duration;
|
|
2055
2128
|
delete target.paused;
|
|
2129
|
+
delete target.volume;
|
|
2130
|
+
delete target.muted;
|
|
2056
2131
|
} catch {
|
|
2057
2132
|
}
|
|
2058
2133
|
},
|
|
@@ -2064,15 +2139,16 @@ async function createHybridSession(ctx, target) {
|
|
|
2064
2139
|
|
|
2065
2140
|
// src/strategies/fallback/decoder.ts
|
|
2066
2141
|
async function startDecoder(opts) {
|
|
2067
|
-
const variant =
|
|
2142
|
+
const variant = chunkF3LQJKXK_cjs.pickLibavVariant(opts.context);
|
|
2068
2143
|
const libav = await chunkG4APZMCP_cjs.loadLibav(variant);
|
|
2069
2144
|
const bridge = await loadBridge2();
|
|
2070
|
-
const { prepareLibavInput } = await import('./libav-http-reader-
|
|
2071
|
-
const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source);
|
|
2145
|
+
const { prepareLibavInput } = await import('./libav-http-reader-AZLE7YFS.cjs');
|
|
2146
|
+
const inputHandle = await prepareLibavInput(libav, opts.filename, opts.source, opts.transport);
|
|
2072
2147
|
const readPkt = await libav.av_packet_alloc();
|
|
2073
2148
|
const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
|
|
2074
2149
|
const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
|
|
2075
|
-
const
|
|
2150
|
+
const firstAudioTrackId = opts.context.audioTracks[0]?.id;
|
|
2151
|
+
let audioStream = (firstAudioTrackId != null ? streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO && s.index === firstAudioTrackId) : void 0) ?? streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO) ?? null;
|
|
2076
2152
|
if (!videoStream && !audioStream) {
|
|
2077
2153
|
throw new Error("fallback decoder: file has no decodable streams");
|
|
2078
2154
|
}
|
|
@@ -2124,6 +2200,56 @@ async function startDecoder(opts) {
|
|
|
2124
2200
|
`fallback decoder: could not initialize any libav decoders (${codecs}).${hint}`
|
|
2125
2201
|
);
|
|
2126
2202
|
}
|
|
2203
|
+
let bsfCtx = null;
|
|
2204
|
+
let bsfPkt = null;
|
|
2205
|
+
if (videoStream && opts.context.videoTracks[0]?.codec === "mpeg4") {
|
|
2206
|
+
try {
|
|
2207
|
+
bsfCtx = await libav.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
|
|
2208
|
+
if (bsfCtx != null && bsfCtx >= 0) {
|
|
2209
|
+
const parIn = await libav.AVBSFContext_par_in(bsfCtx);
|
|
2210
|
+
await libav.avcodec_parameters_copy(parIn, videoStream.codecpar);
|
|
2211
|
+
await libav.av_bsf_init(bsfCtx);
|
|
2212
|
+
bsfPkt = await libav.av_packet_alloc();
|
|
2213
|
+
chunkG4APZMCP_cjs.dbg.info("bsf", "mpeg4_unpack_bframes BSF active");
|
|
2214
|
+
} else {
|
|
2215
|
+
console.warn("[avbridge] mpeg4_unpack_bframes BSF not available \u2014 decoding without it");
|
|
2216
|
+
bsfCtx = null;
|
|
2217
|
+
}
|
|
2218
|
+
} catch (err) {
|
|
2219
|
+
console.warn("[avbridge] failed to init mpeg4_unpack_bframes BSF:", err.message);
|
|
2220
|
+
bsfCtx = null;
|
|
2221
|
+
bsfPkt = null;
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
async function applyBSF(packets) {
|
|
2225
|
+
if (!bsfCtx || !bsfPkt) return packets;
|
|
2226
|
+
const out = [];
|
|
2227
|
+
for (const pkt of packets) {
|
|
2228
|
+
await libav.ff_copyin_packet(bsfPkt, pkt);
|
|
2229
|
+
const sendErr = await libav.av_bsf_send_packet(bsfCtx, bsfPkt);
|
|
2230
|
+
if (sendErr < 0) {
|
|
2231
|
+
out.push(pkt);
|
|
2232
|
+
continue;
|
|
2233
|
+
}
|
|
2234
|
+
while (true) {
|
|
2235
|
+
const recvErr = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
|
|
2236
|
+
if (recvErr < 0) break;
|
|
2237
|
+
out.push(await libav.ff_copyout_packet(bsfPkt));
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
return out;
|
|
2241
|
+
}
|
|
2242
|
+
async function flushBSF() {
|
|
2243
|
+
if (!bsfCtx || !bsfPkt) return;
|
|
2244
|
+
try {
|
|
2245
|
+
await libav.av_bsf_send_packet(bsfCtx, 0);
|
|
2246
|
+
while (true) {
|
|
2247
|
+
const err = await libav.av_bsf_receive_packet(bsfCtx, bsfPkt);
|
|
2248
|
+
if (err < 0) break;
|
|
2249
|
+
}
|
|
2250
|
+
} catch {
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2127
2253
|
let destroyed = false;
|
|
2128
2254
|
let pumpToken = 0;
|
|
2129
2255
|
let pumpRunning = null;
|
|
@@ -2159,7 +2285,8 @@ async function startDecoder(opts) {
|
|
|
2159
2285
|
}
|
|
2160
2286
|
if (myToken !== pumpToken || destroyed) return;
|
|
2161
2287
|
if (videoDec && videoPackets && videoPackets.length > 0) {
|
|
2162
|
-
await
|
|
2288
|
+
const processed = await applyBSF(videoPackets);
|
|
2289
|
+
await decodeVideoBatch(processed, myToken);
|
|
2163
2290
|
}
|
|
2164
2291
|
packetsRead += (videoPackets?.length ?? 0) + (audioPackets?.length ?? 0);
|
|
2165
2292
|
if (videoFramesDecoded > 0) {
|
|
@@ -2237,7 +2364,7 @@ async function startDecoder(opts) {
|
|
|
2237
2364
|
if (myToken !== pumpToken || destroyed) return;
|
|
2238
2365
|
for (const f of frames) {
|
|
2239
2366
|
if (myToken !== pumpToken || destroyed) return;
|
|
2240
|
-
|
|
2367
|
+
chunkCPZ7PXAM_cjs.sanitizeFrameTimestamp(
|
|
2241
2368
|
f,
|
|
2242
2369
|
() => {
|
|
2243
2370
|
const ts = syntheticVideoUs;
|
|
@@ -2247,7 +2374,7 @@ async function startDecoder(opts) {
|
|
|
2247
2374
|
videoTimeBase
|
|
2248
2375
|
);
|
|
2249
2376
|
try {
|
|
2250
|
-
const vf = bridge.laFrameToVideoFrame(f,
|
|
2377
|
+
const vf = bridge.laFrameToVideoFrame(f, { timeBase: [1, 1e6] });
|
|
2251
2378
|
opts.renderer.enqueue(vf);
|
|
2252
2379
|
videoFramesDecoded++;
|
|
2253
2380
|
} catch (err) {
|
|
@@ -2275,7 +2402,7 @@ async function startDecoder(opts) {
|
|
|
2275
2402
|
if (myToken !== pumpToken || destroyed) return;
|
|
2276
2403
|
for (const f of frames) {
|
|
2277
2404
|
if (myToken !== pumpToken || destroyed) return;
|
|
2278
|
-
|
|
2405
|
+
chunkCPZ7PXAM_cjs.sanitizeFrameTimestamp(
|
|
2279
2406
|
f,
|
|
2280
2407
|
() => {
|
|
2281
2408
|
const ts = syntheticAudioUs;
|
|
@@ -2286,7 +2413,7 @@ async function startDecoder(opts) {
|
|
|
2286
2413
|
},
|
|
2287
2414
|
audioTimeBase
|
|
2288
2415
|
);
|
|
2289
|
-
const samples =
|
|
2416
|
+
const samples = chunkCPZ7PXAM_cjs.libavFrameToInterleavedFloat32(f);
|
|
2290
2417
|
if (samples) {
|
|
2291
2418
|
opts.audio.schedule(samples.data, samples.channels, samples.sampleRate);
|
|
2292
2419
|
audioFramesDecoded++;
|
|
@@ -2305,6 +2432,14 @@ async function startDecoder(opts) {
|
|
|
2305
2432
|
await pumpRunning;
|
|
2306
2433
|
} catch {
|
|
2307
2434
|
}
|
|
2435
|
+
try {
|
|
2436
|
+
if (bsfCtx) await libav.av_bsf_free(bsfCtx);
|
|
2437
|
+
} catch {
|
|
2438
|
+
}
|
|
2439
|
+
try {
|
|
2440
|
+
if (bsfPkt) await libav.av_packet_free?.(bsfPkt);
|
|
2441
|
+
} catch {
|
|
2442
|
+
}
|
|
2308
2443
|
try {
|
|
2309
2444
|
if (videoDec) await libav.ff_free_decoder?.(videoDec.c, videoDec.pkt, videoDec.frame);
|
|
2310
2445
|
} catch {
|
|
@@ -2326,6 +2461,69 @@ async function startDecoder(opts) {
|
|
|
2326
2461
|
} catch {
|
|
2327
2462
|
}
|
|
2328
2463
|
},
|
|
2464
|
+
async setAudioTrack(trackId, timeSec) {
|
|
2465
|
+
if (audioStream && audioStream.index === trackId) return;
|
|
2466
|
+
const newStream = streams.find(
|
|
2467
|
+
(s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO && s.index === trackId
|
|
2468
|
+
);
|
|
2469
|
+
if (!newStream) {
|
|
2470
|
+
console.warn("[avbridge] fallback: setAudioTrack \u2014 no stream with id", trackId);
|
|
2471
|
+
return;
|
|
2472
|
+
}
|
|
2473
|
+
const newToken = ++pumpToken;
|
|
2474
|
+
if (pumpRunning) {
|
|
2475
|
+
try {
|
|
2476
|
+
await pumpRunning;
|
|
2477
|
+
} catch {
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
if (destroyed) return;
|
|
2481
|
+
if (audioDec) {
|
|
2482
|
+
try {
|
|
2483
|
+
await libav.ff_free_decoder?.(audioDec.c, audioDec.pkt, audioDec.frame);
|
|
2484
|
+
} catch {
|
|
2485
|
+
}
|
|
2486
|
+
audioDec = null;
|
|
2487
|
+
}
|
|
2488
|
+
try {
|
|
2489
|
+
const [, c, pkt, frame] = await libav.ff_init_decoder(newStream.codec_id, {
|
|
2490
|
+
codecpar: newStream.codecpar
|
|
2491
|
+
});
|
|
2492
|
+
audioDec = { c, pkt, frame };
|
|
2493
|
+
audioTimeBase = newStream.time_base_num && newStream.time_base_den ? [newStream.time_base_num, newStream.time_base_den] : void 0;
|
|
2494
|
+
} catch (err) {
|
|
2495
|
+
console.warn(
|
|
2496
|
+
"[avbridge] fallback: setAudioTrack init failed \u2014 falling back to no-audio mode:",
|
|
2497
|
+
err.message
|
|
2498
|
+
);
|
|
2499
|
+
audioDec = null;
|
|
2500
|
+
opts.audio.setNoAudio();
|
|
2501
|
+
}
|
|
2502
|
+
audioStream = newStream;
|
|
2503
|
+
try {
|
|
2504
|
+
const tsUs = Math.floor(timeSec * 1e6);
|
|
2505
|
+
const [tsLo, tsHi] = libav.f64toi64 ? libav.f64toi64(tsUs) : [tsUs | 0, Math.floor(tsUs / 4294967296)];
|
|
2506
|
+
await libav.av_seek_frame(
|
|
2507
|
+
fmt_ctx,
|
|
2508
|
+
-1,
|
|
2509
|
+
tsLo,
|
|
2510
|
+
tsHi,
|
|
2511
|
+
libav.AVSEEK_FLAG_BACKWARD ?? 0
|
|
2512
|
+
);
|
|
2513
|
+
} catch (err) {
|
|
2514
|
+
console.warn("[avbridge] fallback: setAudioTrack seek failed:", err);
|
|
2515
|
+
}
|
|
2516
|
+
try {
|
|
2517
|
+
if (videoDec) await libav.avcodec_flush_buffers?.(videoDec.c);
|
|
2518
|
+
} catch {
|
|
2519
|
+
}
|
|
2520
|
+
await flushBSF();
|
|
2521
|
+
syntheticVideoUs = Math.round(timeSec * 1e6);
|
|
2522
|
+
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
2523
|
+
pumpRunning = pumpLoop(newToken).catch(
|
|
2524
|
+
(err) => console.error("[avbridge] fallback pump failed (post-setAudioTrack):", err)
|
|
2525
|
+
);
|
|
2526
|
+
},
|
|
2329
2527
|
async seek(timeSec) {
|
|
2330
2528
|
const newToken = ++pumpToken;
|
|
2331
2529
|
if (pumpRunning) {
|
|
@@ -2356,6 +2554,7 @@ async function startDecoder(opts) {
|
|
|
2356
2554
|
if (audioDec) await libav.avcodec_flush_buffers?.(audioDec.c);
|
|
2357
2555
|
} catch {
|
|
2358
2556
|
}
|
|
2557
|
+
await flushBSF();
|
|
2359
2558
|
syntheticVideoUs = Math.round(timeSec * 1e6);
|
|
2360
2559
|
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
2361
2560
|
pumpRunning = pumpLoop(newToken).catch(
|
|
@@ -2368,6 +2567,7 @@ async function startDecoder(opts) {
|
|
|
2368
2567
|
packetsRead,
|
|
2369
2568
|
videoFramesDecoded,
|
|
2370
2569
|
audioFramesDecoded,
|
|
2570
|
+
bsfApplied: bsfCtx ? ["mpeg4_unpack_bframes"] : [],
|
|
2371
2571
|
// Confirmed transport info: once prepareLibavInput returns
|
|
2372
2572
|
// successfully, we *know* whether the source is http-range (probe
|
|
2373
2573
|
// succeeded and returned 206) or in-memory blob. Diagnostics hoists
|
|
@@ -2380,138 +2580,9 @@ async function startDecoder(opts) {
|
|
|
2380
2580
|
}
|
|
2381
2581
|
};
|
|
2382
2582
|
}
|
|
2383
|
-
function sanitizeFrameTimestamp2(frame, nextUs, fallbackTimeBase) {
|
|
2384
|
-
const lo = frame.pts ?? 0;
|
|
2385
|
-
const hi = frame.ptshi ?? 0;
|
|
2386
|
-
const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
|
|
2387
|
-
if (isInvalid) {
|
|
2388
|
-
const us2 = nextUs();
|
|
2389
|
-
frame.pts = us2;
|
|
2390
|
-
frame.ptshi = 0;
|
|
2391
|
-
return { timeBase: [1, 1e6] };
|
|
2392
|
-
}
|
|
2393
|
-
const tb = fallbackTimeBase ?? [1, 1e6];
|
|
2394
|
-
const pts64 = hi * 4294967296 + lo;
|
|
2395
|
-
const us = Math.round(pts64 * 1e6 * tb[0] / tb[1]);
|
|
2396
|
-
if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
|
|
2397
|
-
frame.pts = us;
|
|
2398
|
-
frame.ptshi = us < 0 ? -1 : 0;
|
|
2399
|
-
return { timeBase: [1, 1e6] };
|
|
2400
|
-
}
|
|
2401
|
-
const fallback = nextUs();
|
|
2402
|
-
frame.pts = fallback;
|
|
2403
|
-
frame.ptshi = 0;
|
|
2404
|
-
return { timeBase: [1, 1e6] };
|
|
2405
|
-
}
|
|
2406
|
-
var AV_SAMPLE_FMT_U82 = 0;
|
|
2407
|
-
var AV_SAMPLE_FMT_S162 = 1;
|
|
2408
|
-
var AV_SAMPLE_FMT_S322 = 2;
|
|
2409
|
-
var AV_SAMPLE_FMT_FLT2 = 3;
|
|
2410
|
-
var AV_SAMPLE_FMT_U8P2 = 5;
|
|
2411
|
-
var AV_SAMPLE_FMT_S16P2 = 6;
|
|
2412
|
-
var AV_SAMPLE_FMT_S32P2 = 7;
|
|
2413
|
-
var AV_SAMPLE_FMT_FLTP2 = 8;
|
|
2414
|
-
function libavFrameToInterleavedFloat322(frame) {
|
|
2415
|
-
const channels = frame.channels ?? frame.ch_layout_nb_channels ?? 1;
|
|
2416
|
-
const sampleRate = frame.sample_rate ?? 44100;
|
|
2417
|
-
const nbSamples = frame.nb_samples ?? 0;
|
|
2418
|
-
if (nbSamples === 0) return null;
|
|
2419
|
-
const out = new Float32Array(nbSamples * channels);
|
|
2420
|
-
switch (frame.format) {
|
|
2421
|
-
case AV_SAMPLE_FMT_FLTP2: {
|
|
2422
|
-
const planes = ensurePlanes2(frame.data, channels);
|
|
2423
|
-
for (let ch = 0; ch < channels; ch++) {
|
|
2424
|
-
const plane = asFloat322(planes[ch]);
|
|
2425
|
-
for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i];
|
|
2426
|
-
}
|
|
2427
|
-
return { data: out, channels, sampleRate };
|
|
2428
|
-
}
|
|
2429
|
-
case AV_SAMPLE_FMT_FLT2: {
|
|
2430
|
-
const flat = asFloat322(frame.data);
|
|
2431
|
-
for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i];
|
|
2432
|
-
return { data: out, channels, sampleRate };
|
|
2433
|
-
}
|
|
2434
|
-
case AV_SAMPLE_FMT_S16P2: {
|
|
2435
|
-
const planes = ensurePlanes2(frame.data, channels);
|
|
2436
|
-
for (let ch = 0; ch < channels; ch++) {
|
|
2437
|
-
const plane = asInt162(planes[ch]);
|
|
2438
|
-
for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 32768;
|
|
2439
|
-
}
|
|
2440
|
-
return { data: out, channels, sampleRate };
|
|
2441
|
-
}
|
|
2442
|
-
case AV_SAMPLE_FMT_S162: {
|
|
2443
|
-
const flat = asInt162(frame.data);
|
|
2444
|
-
for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 32768;
|
|
2445
|
-
return { data: out, channels, sampleRate };
|
|
2446
|
-
}
|
|
2447
|
-
case AV_SAMPLE_FMT_S32P2: {
|
|
2448
|
-
const planes = ensurePlanes2(frame.data, channels);
|
|
2449
|
-
for (let ch = 0; ch < channels; ch++) {
|
|
2450
|
-
const plane = asInt322(planes[ch]);
|
|
2451
|
-
for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 2147483648;
|
|
2452
|
-
}
|
|
2453
|
-
return { data: out, channels, sampleRate };
|
|
2454
|
-
}
|
|
2455
|
-
case AV_SAMPLE_FMT_S322: {
|
|
2456
|
-
const flat = asInt322(frame.data);
|
|
2457
|
-
for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 2147483648;
|
|
2458
|
-
return { data: out, channels, sampleRate };
|
|
2459
|
-
}
|
|
2460
|
-
case AV_SAMPLE_FMT_U8P2: {
|
|
2461
|
-
const planes = ensurePlanes2(frame.data, channels);
|
|
2462
|
-
for (let ch = 0; ch < channels; ch++) {
|
|
2463
|
-
const plane = asUint82(planes[ch]);
|
|
2464
|
-
for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = (plane[i] - 128) / 128;
|
|
2465
|
-
}
|
|
2466
|
-
return { data: out, channels, sampleRate };
|
|
2467
|
-
}
|
|
2468
|
-
case AV_SAMPLE_FMT_U82: {
|
|
2469
|
-
const flat = asUint82(frame.data);
|
|
2470
|
-
for (let i = 0; i < nbSamples * channels; i++) out[i] = (flat[i] - 128) / 128;
|
|
2471
|
-
return { data: out, channels, sampleRate };
|
|
2472
|
-
}
|
|
2473
|
-
default:
|
|
2474
|
-
if (!globalThis.__avbridgeLoggedSampleFmt) {
|
|
2475
|
-
globalThis.__avbridgeLoggedSampleFmt = frame.format;
|
|
2476
|
-
console.warn(`[avbridge] unsupported audio sample format from libav: ${frame.format}`);
|
|
2477
|
-
}
|
|
2478
|
-
return null;
|
|
2479
|
-
}
|
|
2480
|
-
}
|
|
2481
|
-
function ensurePlanes2(data, channels) {
|
|
2482
|
-
if (Array.isArray(data)) return data;
|
|
2483
|
-
const arr = data;
|
|
2484
|
-
const len = arr.length;
|
|
2485
|
-
const perChannel = Math.floor(len / channels);
|
|
2486
|
-
const planes = [];
|
|
2487
|
-
for (let ch = 0; ch < channels; ch++) {
|
|
2488
|
-
planes.push(arr.subarray ? arr.subarray(ch * perChannel, (ch + 1) * perChannel) : arr);
|
|
2489
|
-
}
|
|
2490
|
-
return planes;
|
|
2491
|
-
}
|
|
2492
|
-
function asFloat322(x) {
|
|
2493
|
-
if (x instanceof Float32Array) return x;
|
|
2494
|
-
const ta = x;
|
|
2495
|
-
return new Float32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
|
|
2496
|
-
}
|
|
2497
|
-
function asInt162(x) {
|
|
2498
|
-
if (x instanceof Int16Array) return x;
|
|
2499
|
-
const ta = x;
|
|
2500
|
-
return new Int16Array(ta.buffer, ta.byteOffset, ta.byteLength / 2);
|
|
2501
|
-
}
|
|
2502
|
-
function asInt322(x) {
|
|
2503
|
-
if (x instanceof Int32Array) return x;
|
|
2504
|
-
const ta = x;
|
|
2505
|
-
return new Int32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
|
|
2506
|
-
}
|
|
2507
|
-
function asUint82(x) {
|
|
2508
|
-
if (x instanceof Uint8Array) return x;
|
|
2509
|
-
const ta = x;
|
|
2510
|
-
return new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength);
|
|
2511
|
-
}
|
|
2512
2583
|
async function loadBridge2() {
|
|
2513
2584
|
try {
|
|
2514
|
-
const wrapper = await import('./libav-import-
|
|
2585
|
+
const wrapper = await import('./libav-import-2ZVKV2E7.cjs');
|
|
2515
2586
|
return wrapper.libavBridge;
|
|
2516
2587
|
} catch (err) {
|
|
2517
2588
|
throw new Error(
|
|
@@ -2523,9 +2594,9 @@ async function loadBridge2() {
|
|
|
2523
2594
|
// src/strategies/fallback/index.ts
|
|
2524
2595
|
var READY_AUDIO_BUFFER_SECONDS2 = 0.04;
|
|
2525
2596
|
var READY_TIMEOUT_SECONDS2 = 3;
|
|
2526
|
-
async function createFallbackSession(ctx, target) {
|
|
2527
|
-
const { normalizeSource
|
|
2528
|
-
const source = await
|
|
2597
|
+
async function createFallbackSession(ctx, target, transport) {
|
|
2598
|
+
const { normalizeSource } = await import('./source-VFLXLOCN.cjs');
|
|
2599
|
+
const source = await normalizeSource(ctx.source);
|
|
2529
2600
|
const fps = ctx.videoTracks[0]?.fps ?? 30;
|
|
2530
2601
|
const audio = new AudioOutput();
|
|
2531
2602
|
const renderer = new VideoRenderer(target, audio, fps);
|
|
@@ -2536,7 +2607,8 @@ async function createFallbackSession(ctx, target) {
|
|
|
2536
2607
|
filename: ctx.name ?? "input.bin",
|
|
2537
2608
|
context: ctx,
|
|
2538
2609
|
renderer,
|
|
2539
|
-
audio
|
|
2610
|
+
audio,
|
|
2611
|
+
transport
|
|
2540
2612
|
});
|
|
2541
2613
|
} catch (err) {
|
|
2542
2614
|
audio.destroy();
|
|
@@ -2554,6 +2626,22 @@ async function createFallbackSession(ctx, target) {
|
|
|
2554
2626
|
configurable: true,
|
|
2555
2627
|
get: () => !audio.isPlaying()
|
|
2556
2628
|
});
|
|
2629
|
+
Object.defineProperty(target, "volume", {
|
|
2630
|
+
configurable: true,
|
|
2631
|
+
get: () => audio.getVolume(),
|
|
2632
|
+
set: (v) => {
|
|
2633
|
+
audio.setVolume(v);
|
|
2634
|
+
target.dispatchEvent(new Event("volumechange"));
|
|
2635
|
+
}
|
|
2636
|
+
});
|
|
2637
|
+
Object.defineProperty(target, "muted", {
|
|
2638
|
+
configurable: true,
|
|
2639
|
+
get: () => audio.getMuted(),
|
|
2640
|
+
set: (m) => {
|
|
2641
|
+
audio.setMuted(m);
|
|
2642
|
+
target.dispatchEvent(new Event("volumechange"));
|
|
2643
|
+
}
|
|
2644
|
+
});
|
|
2557
2645
|
if (ctx.duration && Number.isFinite(ctx.duration)) {
|
|
2558
2646
|
Object.defineProperty(target, "duration", {
|
|
2559
2647
|
configurable: true,
|
|
@@ -2617,15 +2705,35 @@ async function createFallbackSession(ctx, target) {
|
|
|
2617
2705
|
if (!audio.isPlaying()) {
|
|
2618
2706
|
await waitForBuffer();
|
|
2619
2707
|
await audio.start();
|
|
2708
|
+
target.dispatchEvent(new Event("play"));
|
|
2709
|
+
target.dispatchEvent(new Event("playing"));
|
|
2620
2710
|
}
|
|
2621
2711
|
},
|
|
2622
2712
|
pause() {
|
|
2623
2713
|
void audio.pause();
|
|
2714
|
+
target.dispatchEvent(new Event("pause"));
|
|
2624
2715
|
},
|
|
2625
2716
|
async seek(time) {
|
|
2626
2717
|
await doSeek(time);
|
|
2627
2718
|
},
|
|
2628
|
-
async setAudioTrack(
|
|
2719
|
+
async setAudioTrack(id) {
|
|
2720
|
+
if (!ctx.audioTracks.some((t) => t.id === id)) {
|
|
2721
|
+
console.warn("[avbridge] fallback: setAudioTrack \u2014 unknown track id", id);
|
|
2722
|
+
return;
|
|
2723
|
+
}
|
|
2724
|
+
const wasPlaying = audio.isPlaying();
|
|
2725
|
+
const currentTime = audio.now();
|
|
2726
|
+
await audio.pause().catch(() => {
|
|
2727
|
+
});
|
|
2728
|
+
await handles.setAudioTrack(id, currentTime).catch(
|
|
2729
|
+
(err) => console.warn("[avbridge] fallback: handles.setAudioTrack failed:", err)
|
|
2730
|
+
);
|
|
2731
|
+
await audio.reset(currentTime);
|
|
2732
|
+
renderer.flush();
|
|
2733
|
+
if (wasPlaying) {
|
|
2734
|
+
await waitForBuffer();
|
|
2735
|
+
await audio.start();
|
|
2736
|
+
}
|
|
2629
2737
|
},
|
|
2630
2738
|
async setSubtitleTrack(_id) {
|
|
2631
2739
|
},
|
|
@@ -2640,6 +2748,8 @@ async function createFallbackSession(ctx, target) {
|
|
|
2640
2748
|
delete target.currentTime;
|
|
2641
2749
|
delete target.duration;
|
|
2642
2750
|
delete target.paused;
|
|
2751
|
+
delete target.volume;
|
|
2752
|
+
delete target.muted;
|
|
2643
2753
|
} catch {
|
|
2644
2754
|
}
|
|
2645
2755
|
},
|
|
@@ -2663,12 +2773,12 @@ var remuxPlugin = {
|
|
|
2663
2773
|
var hybridPlugin = {
|
|
2664
2774
|
name: "hybrid",
|
|
2665
2775
|
canHandle: () => typeof VideoDecoder !== "undefined",
|
|
2666
|
-
execute: (ctx, video) => createHybridSession(ctx, video)
|
|
2776
|
+
execute: (ctx, video, transport) => createHybridSession(ctx, video, transport)
|
|
2667
2777
|
};
|
|
2668
2778
|
var fallbackPlugin = {
|
|
2669
2779
|
name: "fallback",
|
|
2670
2780
|
canHandle: () => true,
|
|
2671
|
-
execute: (ctx, video) => createFallbackSession(ctx, video)
|
|
2781
|
+
execute: (ctx, video, transport) => createFallbackSession(ctx, video, transport)
|
|
2672
2782
|
};
|
|
2673
2783
|
function registerBuiltins(registry) {
|
|
2674
2784
|
registry.register(nativePlugin);
|
|
@@ -2677,88 +2787,6 @@ function registerBuiltins(registry) {
|
|
|
2677
2787
|
registry.register(fallbackPlugin);
|
|
2678
2788
|
}
|
|
2679
2789
|
|
|
2680
|
-
// src/subtitles/vtt.ts
|
|
2681
|
-
function isVtt(text) {
|
|
2682
|
-
const trimmed = text.replace(/^\ufeff/, "").trimStart();
|
|
2683
|
-
return trimmed.startsWith("WEBVTT");
|
|
2684
|
-
}
|
|
2685
|
-
|
|
2686
|
-
// src/subtitles/index.ts
|
|
2687
|
-
async function discoverSidecars(file, directory) {
|
|
2688
|
-
const baseName = file.name.replace(/\.[^.]+$/, "");
|
|
2689
|
-
const found = [];
|
|
2690
|
-
for await (const [name, handle] of directory) {
|
|
2691
|
-
if (handle.kind !== "file") continue;
|
|
2692
|
-
if (!name.startsWith(baseName)) continue;
|
|
2693
|
-
const lower = name.toLowerCase();
|
|
2694
|
-
let format = null;
|
|
2695
|
-
if (lower.endsWith(".srt")) format = "srt";
|
|
2696
|
-
else if (lower.endsWith(".vtt")) format = "vtt";
|
|
2697
|
-
if (!format) continue;
|
|
2698
|
-
const sidecarFile = await handle.getFile();
|
|
2699
|
-
const url = URL.createObjectURL(sidecarFile);
|
|
2700
|
-
const langMatch = name.slice(baseName.length).match(/[._-]([a-z]{2,3})(?:[._-]|\.)/i);
|
|
2701
|
-
found.push({
|
|
2702
|
-
url,
|
|
2703
|
-
format,
|
|
2704
|
-
language: langMatch?.[1]
|
|
2705
|
-
});
|
|
2706
|
-
}
|
|
2707
|
-
return found;
|
|
2708
|
-
}
|
|
2709
|
-
var SubtitleResourceBag = class {
|
|
2710
|
-
urls = /* @__PURE__ */ new Set();
|
|
2711
|
-
/** Track an externally-created blob URL (e.g. from `discoverSidecars`). */
|
|
2712
|
-
track(url) {
|
|
2713
|
-
this.urls.add(url);
|
|
2714
|
-
}
|
|
2715
|
-
/** Convenience: create a blob URL and track it in one call. */
|
|
2716
|
-
createObjectURL(blob) {
|
|
2717
|
-
const url = URL.createObjectURL(blob);
|
|
2718
|
-
this.urls.add(url);
|
|
2719
|
-
return url;
|
|
2720
|
-
}
|
|
2721
|
-
/** Revoke every tracked URL. Idempotent — safe to call multiple times. */
|
|
2722
|
-
revokeAll() {
|
|
2723
|
-
for (const u of this.urls) URL.revokeObjectURL(u);
|
|
2724
|
-
this.urls.clear();
|
|
2725
|
-
}
|
|
2726
|
-
};
|
|
2727
|
-
async function attachSubtitleTracks(video, tracks, bag, onError) {
|
|
2728
|
-
for (const t of Array.from(video.querySelectorAll("track[data-avbridge]"))) {
|
|
2729
|
-
t.remove();
|
|
2730
|
-
}
|
|
2731
|
-
for (const t of tracks) {
|
|
2732
|
-
if (!t.sidecarUrl) continue;
|
|
2733
|
-
try {
|
|
2734
|
-
let url = t.sidecarUrl;
|
|
2735
|
-
if (t.format === "srt") {
|
|
2736
|
-
const res = await fetch(t.sidecarUrl);
|
|
2737
|
-
const text = await res.text();
|
|
2738
|
-
const vtt = srtToVtt(text);
|
|
2739
|
-
const blob = new Blob([vtt], { type: "text/vtt" });
|
|
2740
|
-
url = bag ? bag.createObjectURL(blob) : URL.createObjectURL(blob);
|
|
2741
|
-
} else if (t.format === "vtt") {
|
|
2742
|
-
const res = await fetch(t.sidecarUrl);
|
|
2743
|
-
const text = await res.text();
|
|
2744
|
-
if (!isVtt(text)) {
|
|
2745
|
-
console.warn("[avbridge] subtitle missing WEBVTT header:", t.sidecarUrl);
|
|
2746
|
-
}
|
|
2747
|
-
}
|
|
2748
|
-
const trackEl = document.createElement("track");
|
|
2749
|
-
trackEl.kind = "subtitles";
|
|
2750
|
-
trackEl.src = url;
|
|
2751
|
-
trackEl.srclang = t.language ?? "und";
|
|
2752
|
-
trackEl.label = t.language ?? `Subtitle ${t.id}`;
|
|
2753
|
-
trackEl.dataset.avbridge = "true";
|
|
2754
|
-
video.appendChild(trackEl);
|
|
2755
|
-
} catch (err) {
|
|
2756
|
-
const e = err instanceof Error ? err : new Error(String(err));
|
|
2757
|
-
onError?.(e, t);
|
|
2758
|
-
}
|
|
2759
|
-
}
|
|
2760
|
-
}
|
|
2761
|
-
|
|
2762
2790
|
// src/player.ts
|
|
2763
2791
|
var UnifiedPlayer = class _UnifiedPlayer {
|
|
2764
2792
|
/**
|
|
@@ -2767,6 +2795,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2767
2795
|
constructor(options, registry) {
|
|
2768
2796
|
this.options = options;
|
|
2769
2797
|
this.registry = registry;
|
|
2798
|
+
const { requestInit, fetchFn } = options;
|
|
2799
|
+
if (requestInit || fetchFn) {
|
|
2800
|
+
this.transport = { requestInit, fetchFn };
|
|
2801
|
+
}
|
|
2770
2802
|
}
|
|
2771
2803
|
options;
|
|
2772
2804
|
registry;
|
|
@@ -2786,11 +2818,23 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2786
2818
|
// listener outlives the player and accumulates on elements that swap
|
|
2787
2819
|
// source (e.g. <avbridge-video>).
|
|
2788
2820
|
endedListener = null;
|
|
2821
|
+
// Background tab handling. userIntent is what the user last asked for
|
|
2822
|
+
// (play vs pause) — used to decide whether to auto-resume on visibility
|
|
2823
|
+
// return. autoPausedForVisibility tracks whether we paused because the
|
|
2824
|
+
// tab was hidden, so we don't resume playback the user deliberately
|
|
2825
|
+
// paused (e.g. via media keys while hidden).
|
|
2826
|
+
userIntent = "pause";
|
|
2827
|
+
autoPausedForVisibility = false;
|
|
2828
|
+
visibilityListener = null;
|
|
2789
2829
|
// Serializes escalation / setStrategy calls
|
|
2790
2830
|
switchingPromise = Promise.resolve();
|
|
2791
2831
|
// Owns blob URLs created during sidecar discovery + SRT->VTT conversion.
|
|
2792
2832
|
// Revoked at destroy() so repeated source swaps don't leak.
|
|
2793
|
-
subtitleResources = new SubtitleResourceBag();
|
|
2833
|
+
subtitleResources = new chunkS4WAZC2T_cjs.SubtitleResourceBag();
|
|
2834
|
+
// Transport config extracted from CreatePlayerOptions. Threaded to probe,
|
|
2835
|
+
// subtitle fetches, and strategy session creators. Not stored on MediaContext
|
|
2836
|
+
// because it's runtime config, not media analysis.
|
|
2837
|
+
transport;
|
|
2794
2838
|
static async create(options) {
|
|
2795
2839
|
const registry = new PluginRegistry();
|
|
2796
2840
|
registerBuiltins(registry);
|
|
@@ -2814,7 +2858,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2814
2858
|
const bootstrapStart = performance.now();
|
|
2815
2859
|
try {
|
|
2816
2860
|
chunkG4APZMCP_cjs.dbg.info("bootstrap", "start");
|
|
2817
|
-
const ctx = await chunkG4APZMCP_cjs.dbg.timed("probe", "probe", 3e3, () => probe(this.options.source));
|
|
2861
|
+
const ctx = await chunkG4APZMCP_cjs.dbg.timed("probe", "probe", 3e3, () => chunkZCUXHW55_cjs.probe(this.options.source, this.transport));
|
|
2818
2862
|
chunkG4APZMCP_cjs.dbg.info(
|
|
2819
2863
|
"probe",
|
|
2820
2864
|
`container=${ctx.container} video=${ctx.videoTracks[0]?.codec ?? "-"} audio=${ctx.audioTracks[0]?.codec ?? "-"} probedBy=${ctx.probedBy}`
|
|
@@ -2832,7 +2876,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2832
2876
|
}
|
|
2833
2877
|
}
|
|
2834
2878
|
if (this.options.directory && this.options.source instanceof File) {
|
|
2835
|
-
const found = await discoverSidecars(this.options.source, this.options.directory);
|
|
2879
|
+
const found = await chunkS4WAZC2T_cjs.discoverSidecars(this.options.source, this.options.directory);
|
|
2836
2880
|
for (const s of found) {
|
|
2837
2881
|
this.subtitleResources.track(s.url);
|
|
2838
2882
|
ctx.subtitleTracks.push({
|
|
@@ -2855,16 +2899,15 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2855
2899
|
reason: decision.reason
|
|
2856
2900
|
});
|
|
2857
2901
|
await this.startSession(decision.strategy, decision.reason);
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
(
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
}
|
|
2902
|
+
await chunkS4WAZC2T_cjs.attachSubtitleTracks(
|
|
2903
|
+
this.options.target,
|
|
2904
|
+
ctx.subtitleTracks,
|
|
2905
|
+
this.subtitleResources,
|
|
2906
|
+
(err, track) => {
|
|
2907
|
+
console.warn(`[avbridge] subtitle ${track.id} failed: ${err.message}`);
|
|
2908
|
+
},
|
|
2909
|
+
this.transport
|
|
2910
|
+
);
|
|
2868
2911
|
this.emitter.emitSticky("tracks", {
|
|
2869
2912
|
video: ctx.videoTracks,
|
|
2870
2913
|
audio: ctx.audioTracks,
|
|
@@ -2873,6 +2916,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2873
2916
|
this.startTimeupdateLoop();
|
|
2874
2917
|
this.endedListener = () => this.emitter.emit("ended", void 0);
|
|
2875
2918
|
this.options.target.addEventListener("ended", this.endedListener);
|
|
2919
|
+
if (this.options.backgroundBehavior !== "continue" && typeof document !== "undefined") {
|
|
2920
|
+
this.visibilityListener = () => this.onVisibilityChange();
|
|
2921
|
+
document.addEventListener("visibilitychange", this.visibilityListener);
|
|
2922
|
+
}
|
|
2876
2923
|
this.emitter.emitSticky("ready", void 0);
|
|
2877
2924
|
const bootstrapElapsed = performance.now() - bootstrapStart;
|
|
2878
2925
|
chunkG4APZMCP_cjs.dbg.info("bootstrap", `ready in ${bootstrapElapsed.toFixed(0)}ms`);
|
|
@@ -2899,7 +2946,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2899
2946
|
throw new Error(`no plugin available for strategy "${strategy}"`);
|
|
2900
2947
|
}
|
|
2901
2948
|
try {
|
|
2902
|
-
this.session = await plugin.execute(this.mediaContext, this.options.target);
|
|
2949
|
+
this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
|
|
2903
2950
|
} catch (err) {
|
|
2904
2951
|
const chain = this.classification?.fallbackChain;
|
|
2905
2952
|
if (chain && chain.length > 0) {
|
|
@@ -2972,7 +3019,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2972
3019
|
continue;
|
|
2973
3020
|
}
|
|
2974
3021
|
try {
|
|
2975
|
-
this.session = await plugin.execute(this.mediaContext, this.options.target);
|
|
3022
|
+
this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
|
|
2976
3023
|
} catch (err) {
|
|
2977
3024
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2978
3025
|
errors.push(`${nextStrategy}: ${msg}`);
|
|
@@ -2995,8 +3042,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2995
3042
|
}
|
|
2996
3043
|
return;
|
|
2997
3044
|
}
|
|
2998
|
-
this.emitter.emit("error", new
|
|
2999
|
-
|
|
3045
|
+
this.emitter.emit("error", new chunk2IJ66NTD_cjs.AvbridgeError(
|
|
3046
|
+
chunk2IJ66NTD_cjs.ERR_ALL_STRATEGIES_EXHAUSTED,
|
|
3047
|
+
`All playback strategies failed: ${errors.join("; ")}`,
|
|
3048
|
+
"This file may require a codec or container that isn't available in this browser. Try the fallback strategy or check browser codec support."
|
|
3000
3049
|
));
|
|
3001
3050
|
}
|
|
3002
3051
|
// ── Stall supervision ─────────────────────────────────────────────────
|
|
@@ -3048,7 +3097,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3048
3097
|
// ── Public: manual strategy switch ────────────────────────────────────
|
|
3049
3098
|
/** Manually switch to a different playback strategy. Preserves current position and play/pause state. Concurrent calls are serialized. */
|
|
3050
3099
|
async setStrategy(strategy, reason) {
|
|
3051
|
-
if (!this.mediaContext) throw new
|
|
3100
|
+
if (!this.mediaContext) throw new chunk2IJ66NTD_cjs.AvbridgeError(chunk2IJ66NTD_cjs.ERR_PLAYER_NOT_READY, "Player not ready \u2014 wait for the 'ready' event before calling playback methods.", "Await the 'ready' event or check player.readyState before calling play/pause/seek.");
|
|
3052
3101
|
if (this.session?.strategy === strategy) return;
|
|
3053
3102
|
this.switchingPromise = this.switchingPromise.then(
|
|
3054
3103
|
() => this.doSetStrategy(strategy, reason)
|
|
@@ -3077,7 +3126,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3077
3126
|
}
|
|
3078
3127
|
const plugin = this.registry.findFor(this.mediaContext, strategy);
|
|
3079
3128
|
if (!plugin) throw new Error(`no plugin available for strategy "${strategy}"`);
|
|
3080
|
-
this.session = await plugin.execute(this.mediaContext, this.options.target);
|
|
3129
|
+
this.session = await plugin.execute(this.mediaContext, this.options.target, this.transport);
|
|
3081
3130
|
this.emitter.emitSticky("strategy", {
|
|
3082
3131
|
strategy,
|
|
3083
3132
|
reason: switchReason
|
|
@@ -3111,26 +3160,56 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3111
3160
|
}
|
|
3112
3161
|
/** Begin or resume playback. Throws if the player is not ready. */
|
|
3113
3162
|
async play() {
|
|
3114
|
-
if (!this.session) throw new
|
|
3163
|
+
if (!this.session) throw new chunk2IJ66NTD_cjs.AvbridgeError(chunk2IJ66NTD_cjs.ERR_PLAYER_NOT_READY, "Player not ready \u2014 wait for the 'ready' event before calling playback methods.", "Await the 'ready' event or check player.readyState before calling play/pause/seek.");
|
|
3164
|
+
this.userIntent = "play";
|
|
3165
|
+
this.autoPausedForVisibility = false;
|
|
3115
3166
|
await this.session.play();
|
|
3116
3167
|
}
|
|
3117
3168
|
/** Pause playback. No-op if the player is not ready or already paused. */
|
|
3118
3169
|
pause() {
|
|
3170
|
+
this.userIntent = "pause";
|
|
3171
|
+
this.autoPausedForVisibility = false;
|
|
3119
3172
|
this.session?.pause();
|
|
3120
3173
|
}
|
|
3174
|
+
/**
|
|
3175
|
+
* Handle browser tab visibility changes. On hide: pause if the user
|
|
3176
|
+
* had been playing. On show: resume if we were the one who paused.
|
|
3177
|
+
* Skips when `backgroundBehavior: "continue"` is set (listener isn't
|
|
3178
|
+
* installed in that case).
|
|
3179
|
+
*/
|
|
3180
|
+
onVisibilityChange() {
|
|
3181
|
+
if (!this.session) return;
|
|
3182
|
+
const action = decideVisibilityAction({
|
|
3183
|
+
hidden: document.hidden,
|
|
3184
|
+
userIntent: this.userIntent,
|
|
3185
|
+
sessionIsPlaying: !this.options.target.paused,
|
|
3186
|
+
autoPausedForVisibility: this.autoPausedForVisibility
|
|
3187
|
+
});
|
|
3188
|
+
if (action === "pause") {
|
|
3189
|
+
this.autoPausedForVisibility = true;
|
|
3190
|
+
chunkG4APZMCP_cjs.dbg.info("visibility", "tab hidden \u2014 auto-paused");
|
|
3191
|
+
this.session.pause();
|
|
3192
|
+
} else if (action === "resume") {
|
|
3193
|
+
this.autoPausedForVisibility = false;
|
|
3194
|
+
chunkG4APZMCP_cjs.dbg.info("visibility", "tab visible \u2014 auto-resuming");
|
|
3195
|
+
void this.session.play().catch((err) => {
|
|
3196
|
+
console.warn("[avbridge] auto-resume after tab return failed:", err);
|
|
3197
|
+
});
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3121
3200
|
/** Seek to the given time in seconds. Throws if the player is not ready. */
|
|
3122
3201
|
async seek(time) {
|
|
3123
|
-
if (!this.session) throw new
|
|
3202
|
+
if (!this.session) throw new chunk2IJ66NTD_cjs.AvbridgeError(chunk2IJ66NTD_cjs.ERR_PLAYER_NOT_READY, "Player not ready \u2014 wait for the 'ready' event before calling playback methods.", "Await the 'ready' event or check player.readyState before calling play/pause/seek.");
|
|
3124
3203
|
await this.session.seek(time);
|
|
3125
3204
|
}
|
|
3126
3205
|
/** Switch the active audio track by track ID. Throws if the player is not ready. */
|
|
3127
3206
|
async setAudioTrack(id) {
|
|
3128
|
-
if (!this.session) throw new
|
|
3207
|
+
if (!this.session) throw new chunk2IJ66NTD_cjs.AvbridgeError(chunk2IJ66NTD_cjs.ERR_PLAYER_NOT_READY, "Player not ready \u2014 wait for the 'ready' event before calling playback methods.", "Await the 'ready' event or check player.readyState before calling play/pause/seek.");
|
|
3129
3208
|
await this.session.setAudioTrack(id);
|
|
3130
3209
|
}
|
|
3131
3210
|
/** Switch the active subtitle track by track ID, or pass `null` to disable subtitles. */
|
|
3132
3211
|
async setSubtitleTrack(id) {
|
|
3133
|
-
if (!this.session) throw new
|
|
3212
|
+
if (!this.session) throw new chunk2IJ66NTD_cjs.AvbridgeError(chunk2IJ66NTD_cjs.ERR_PLAYER_NOT_READY, "Player not ready \u2014 wait for the 'ready' event before calling playback methods.", "Await the 'ready' event or check player.readyState before calling play/pause/seek.");
|
|
3134
3213
|
await this.session.setSubtitleTrack(id);
|
|
3135
3214
|
}
|
|
3136
3215
|
/** Return a snapshot of current diagnostics: container, codecs, strategy, runtime stats, and strategy history. */
|
|
@@ -3162,6 +3241,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3162
3241
|
this.options.target.removeEventListener("ended", this.endedListener);
|
|
3163
3242
|
this.endedListener = null;
|
|
3164
3243
|
}
|
|
3244
|
+
if (this.visibilityListener) {
|
|
3245
|
+
document.removeEventListener("visibilitychange", this.visibilityListener);
|
|
3246
|
+
this.visibilityListener = null;
|
|
3247
|
+
}
|
|
3165
3248
|
if (this.session) {
|
|
3166
3249
|
await this.session.destroy();
|
|
3167
3250
|
this.session = null;
|
|
@@ -3173,6 +3256,14 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3173
3256
|
async function createPlayer(options) {
|
|
3174
3257
|
return UnifiedPlayer.create(options);
|
|
3175
3258
|
}
|
|
3259
|
+
function decideVisibilityAction(state) {
|
|
3260
|
+
if (state.hidden) {
|
|
3261
|
+
if (state.userIntent === "play" && state.sessionIsPlaying) return "pause";
|
|
3262
|
+
return "noop";
|
|
3263
|
+
}
|
|
3264
|
+
if (state.autoPausedForVisibility) return "resume";
|
|
3265
|
+
return "noop";
|
|
3266
|
+
}
|
|
3176
3267
|
function buildInitialDecision(initial, ctx) {
|
|
3177
3268
|
const natural = classifyContext(ctx);
|
|
3178
3269
|
const cls = strategyToClass(initial, natural);
|
|
@@ -3211,13 +3302,12 @@ function defaultFallbackChain(strategy) {
|
|
|
3211
3302
|
}
|
|
3212
3303
|
}
|
|
3213
3304
|
|
|
3305
|
+
exports.FALLBACK_AUDIO_CODECS = FALLBACK_AUDIO_CODECS;
|
|
3306
|
+
exports.FALLBACK_VIDEO_CODECS = FALLBACK_VIDEO_CODECS;
|
|
3307
|
+
exports.NATIVE_AUDIO_CODECS = NATIVE_AUDIO_CODECS;
|
|
3308
|
+
exports.NATIVE_VIDEO_CODECS = NATIVE_VIDEO_CODECS;
|
|
3214
3309
|
exports.UnifiedPlayer = UnifiedPlayer;
|
|
3215
|
-
exports.avbridgeAudioToMediabunny = avbridgeAudioToMediabunny;
|
|
3216
|
-
exports.avbridgeVideoToMediabunny = avbridgeVideoToMediabunny;
|
|
3217
|
-
exports.buildMediabunnySourceFromInput = buildMediabunnySourceFromInput;
|
|
3218
3310
|
exports.classifyContext = classifyContext;
|
|
3219
3311
|
exports.createPlayer = createPlayer;
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
//# sourceMappingURL=chunk-UF2N5L63.cjs.map
|
|
3223
|
-
//# sourceMappingURL=chunk-UF2N5L63.cjs.map
|
|
3312
|
+
//# sourceMappingURL=chunk-TBW26OPP.cjs.map
|
|
3313
|
+
//# sourceMappingURL=chunk-TBW26OPP.cjs.map
|