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