avbridge 2.3.0 → 2.6.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 +114 -0
- package/dist/{chunk-6UUT4BEA.cjs → chunk-2IJ66NTD.cjs} +13 -20
- package/dist/chunk-2IJ66NTD.cjs.map +1 -0
- package/dist/{chunk-XKPSTC34.cjs → chunk-2XW2O3YI.cjs} +5 -20
- 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-7RGG6ME7.cjs → chunk-6SOFJV44.cjs} +422 -688
- package/dist/chunk-6SOFJV44.cjs.map +1 -0
- package/dist/{chunk-2PGRFCWB.js → chunk-CPJLFFCC.js} +8 -18
- 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-QQXBPW72.js → chunk-E76AMWI4.js} +4 -18
- package/dist/chunk-E76AMWI4.js.map +1 -0
- package/dist/chunk-LUFA47FP.js +19 -0
- package/dist/chunk-LUFA47FP.js.map +1 -0
- package/dist/{chunk-NV7ILLWH.js → chunk-OGYHFY6K.js} +404 -665
- package/dist/chunk-OGYHFY6K.js.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-X2K3GIWE.js +235 -0
- package/dist/chunk-X2K3GIWE.js.map +1 -0
- package/dist/chunk-ZCUXHW55.cjs +242 -0
- package/dist/chunk-ZCUXHW55.cjs.map +1 -0
- package/dist/element-browser.js +883 -492
- package/dist/element-browser.js.map +1 -1
- package/dist/element.cjs +88 -6
- package/dist/element.cjs.map +1 -1
- package/dist/element.d.cts +51 -1
- package/dist/element.d.ts +51 -1
- package/dist/element.js +87 -5
- package/dist/element.js.map +1 -1
- package/dist/index.cjs +523 -393
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +494 -366
- package/dist/index.js.map +1 -1
- package/dist/libav-demux-H2GS46GH.cjs +27 -0
- package/dist/libav-demux-H2GS46GH.cjs.map +1 -0
- package/dist/libav-demux-OWZ4T2YW.js +6 -0
- package/dist/libav-demux-OWZ4T2YW.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-B6WB74RD.d.ts → player-DGXeCNfD.d.cts} +41 -1
- package/dist/{player-B6WB74RD.d.cts → player-DGXeCNfD.d.ts} +41 -1
- package/dist/player.cjs +731 -472
- package/dist/player.cjs.map +1 -1
- package/dist/player.d.cts +229 -120
- package/dist/player.d.ts +229 -120
- package/dist/player.js +710 -451
- package/dist/player.js.map +1 -1
- 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-F656KYYV.js.map → source-4TZ6KMNV.js.map} +1 -1
- package/dist/source-7YLO6E7X.cjs +29 -0
- package/dist/{source-73CAH6HW.cjs.map → source-7YLO6E7X.cjs.map} +1 -1
- package/dist/source-MTX5ELUZ.js +4 -0
- package/dist/{source-QJR3OHTW.js.map → source-MTX5ELUZ.js.map} +1 -1
- package/dist/source-VFLXLOCN.cjs +29 -0
- package/dist/{source-VB74JQ7Z.cjs.map → source-VFLXLOCN.cjs.map} +1 -1
- 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/package.json +1 -1
- package/src/convert/remux.ts +1 -35
- package/src/convert/transcode-libav.ts +691 -0
- package/src/convert/transcode.ts +12 -4
- package/src/element/avbridge-player.ts +100 -0
- package/src/element/avbridge-video.ts +140 -3
- package/src/element/player-styles.ts +12 -0
- package/src/errors.ts +6 -0
- package/src/player.ts +15 -16
- package/src/strategies/fallback/decoder.ts +96 -173
- package/src/strategies/fallback/index.ts +46 -2
- package/src/strategies/fallback/libav-import.ts +9 -1
- package/src/strategies/fallback/video-renderer.ts +107 -0
- package/src/strategies/hybrid/decoder.ts +88 -180
- package/src/strategies/hybrid/index.ts +35 -2
- package/src/strategies/native.ts +6 -3
- package/src/strategies/remux/index.ts +14 -2
- package/src/strategies/remux/pipeline.ts +72 -12
- package/src/subtitles/render.ts +8 -0
- package/src/types.ts +32 -0
- package/src/util/libav-demux.ts +405 -0
- package/src/util/time-ranges.ts +40 -0
- package/dist/chunk-2PGRFCWB.js.map +0 -1
- package/dist/chunk-6UUT4BEA.cjs.map +0 -1
- package/dist/chunk-7RGG6ME7.cjs.map +0 -1
- package/dist/chunk-NV7ILLWH.js.map +0 -1
- package/dist/chunk-QQXBPW72.js.map +0 -1
- package/dist/chunk-XKPSTC34.cjs.map +0 -1
- package/dist/source-73CAH6HW.cjs +0 -28
- package/dist/source-F656KYYV.js +0 -3
- package/dist/source-QJR3OHTW.js +0 -3
- package/dist/source-VB74JQ7Z.cjs +0 -28
|
@@ -1,239 +1,10 @@
|
|
|
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
6
|
import { pickLibavVariant } from './chunk-5YAWWKA3.js';
|
|
4
7
|
|
|
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, transport) {
|
|
182
|
-
const normalized = await normalizeSource(source, transport);
|
|
183
|
-
const sniffed = await sniffNormalizedSource(normalized);
|
|
184
|
-
if (MEDIABUNNY_CONTAINERS.has(sniffed)) {
|
|
185
|
-
try {
|
|
186
|
-
const result = await probeWithMediabunny(normalized, sniffed);
|
|
187
|
-
const hasUnknownCodec = result.videoTracks.some((t) => t.codec === "unknown") || result.audioTracks.some((t) => t.codec === "unknown");
|
|
188
|
-
if (hasUnknownCodec) {
|
|
189
|
-
try {
|
|
190
|
-
const { probeWithLibav } = await import('./avi-NJXAXUXK.js');
|
|
191
|
-
return await probeWithLibav(normalized, sniffed);
|
|
192
|
-
} catch {
|
|
193
|
-
return result;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
return result;
|
|
197
|
-
} catch (mediabunnyErr) {
|
|
198
|
-
console.warn(
|
|
199
|
-
`[avbridge] mediabunny rejected ${sniffed} file, falling back to libav:`,
|
|
200
|
-
mediabunnyErr.message
|
|
201
|
-
);
|
|
202
|
-
try {
|
|
203
|
-
const { probeWithLibav } = await import('./avi-NJXAXUXK.js');
|
|
204
|
-
return await probeWithLibav(normalized, sniffed);
|
|
205
|
-
} catch (libavErr) {
|
|
206
|
-
const mbMsg = mediabunnyErr.message || String(mediabunnyErr);
|
|
207
|
-
const lvMsg = libavErr instanceof Error ? libavErr.message : String(libavErr);
|
|
208
|
-
throw new AvbridgeError(
|
|
209
|
-
ERR_PROBE_FAILED,
|
|
210
|
-
`Failed to probe ${sniffed.toUpperCase()} file. mediabunny: ${mbMsg}. libav: ${lvMsg}.`,
|
|
211
|
-
"The file may be corrupt, truncated, or in an unsupported format. Enable AVBRIDGE_DEBUG for detailed logs."
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
try {
|
|
217
|
-
const { probeWithLibav } = await import('./avi-NJXAXUXK.js');
|
|
218
|
-
return await probeWithLibav(normalized, sniffed);
|
|
219
|
-
} catch (err) {
|
|
220
|
-
const inner = err instanceof Error ? err.message : String(err);
|
|
221
|
-
console.error("[avbridge] libav probe failed for", sniffed, "file:", err);
|
|
222
|
-
if (sniffed === "unknown") {
|
|
223
|
-
throw new AvbridgeError(
|
|
224
|
-
ERR_PROBE_UNKNOWN_CONTAINER,
|
|
225
|
-
`Unable to probe source: container format could not be identified. libav fallback: ${inner || "(no details)"}`,
|
|
226
|
-
"The file may be corrupt or in a format avbridge doesn't recognize. Check the file plays in VLC or ffprobe."
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
throw new AvbridgeError(
|
|
230
|
-
ERR_LIBAV_NOT_REACHABLE,
|
|
231
|
-
`${sniffed.toUpperCase()} files require libav.js, which failed to load: ${inner || "(no details)"}`,
|
|
232
|
-
"Install @libav.js/variant-webcodecs, or check that AVBRIDGE_LIBAV_BASE points to the correct path."
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
8
|
// src/util/codec-strings.ts
|
|
238
9
|
function videoCodecString(track) {
|
|
239
10
|
if (track.codecString) return track.codecString;
|
|
@@ -483,36 +254,6 @@ function isRiskyNative(video) {
|
|
|
483
254
|
return false;
|
|
484
255
|
}
|
|
485
256
|
|
|
486
|
-
// src/subtitles/srt.ts
|
|
487
|
-
function srtToVtt(srt) {
|
|
488
|
-
if (srt.charCodeAt(0) === 65279) srt = srt.slice(1);
|
|
489
|
-
const normalized = srt.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
|
|
490
|
-
const blocks = normalized.split(/\n{2,}/);
|
|
491
|
-
const out = ["WEBVTT", ""];
|
|
492
|
-
for (const block of blocks) {
|
|
493
|
-
const lines = block.split("\n");
|
|
494
|
-
if (lines.length > 0 && /^\d+$/.test(lines[0].trim())) {
|
|
495
|
-
lines.shift();
|
|
496
|
-
}
|
|
497
|
-
if (lines.length === 0) continue;
|
|
498
|
-
const timing = lines.shift();
|
|
499
|
-
const vttTiming = convertTiming(timing);
|
|
500
|
-
if (!vttTiming) continue;
|
|
501
|
-
out.push(vttTiming);
|
|
502
|
-
for (const l of lines) out.push(l);
|
|
503
|
-
out.push("");
|
|
504
|
-
}
|
|
505
|
-
return out.join("\n");
|
|
506
|
-
}
|
|
507
|
-
function convertTiming(line) {
|
|
508
|
-
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(
|
|
509
|
-
line.trim()
|
|
510
|
-
);
|
|
511
|
-
if (!m) return null;
|
|
512
|
-
const fmt = (h, mm, s, ms) => `${h.padStart(2, "0")}:${mm}:${s}.${ms.padEnd(3, "0").slice(0, 3)}`;
|
|
513
|
-
return `${fmt(m[1], m[2], m[3], m[4])} --> ${fmt(m[5], m[6], m[7], m[8])}${m[9] ?? ""}`;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
257
|
// src/events.ts
|
|
517
258
|
var TypedEmitter = class {
|
|
518
259
|
listeners = {};
|
|
@@ -718,7 +459,7 @@ async function createNativeSession(context, video) {
|
|
|
718
459
|
},
|
|
719
460
|
async setAudioTrack(id) {
|
|
720
461
|
const tracks = video.audioTracks;
|
|
721
|
-
if (!tracks) return;
|
|
462
|
+
if (!tracks || tracks.length === 0) return;
|
|
722
463
|
for (let i = 0; i < tracks.length; i++) {
|
|
723
464
|
tracks[i].enabled = tracks[i].id === String(id) || i === id;
|
|
724
465
|
}
|
|
@@ -966,30 +707,49 @@ var MseSink = class {
|
|
|
966
707
|
async function createRemuxPipeline(ctx, video) {
|
|
967
708
|
const mb = await import('mediabunny');
|
|
968
709
|
const videoTrackInfo = ctx.videoTracks[0];
|
|
969
|
-
const audioTrackInfo = ctx.audioTracks[0];
|
|
970
710
|
if (!videoTrackInfo) throw new Error("remux: source has no video track");
|
|
971
711
|
const mbVideoCodec = avbridgeVideoToMediabunny(videoTrackInfo.codec);
|
|
972
712
|
if (!mbVideoCodec) {
|
|
973
713
|
throw new Error(`remux: video codec "${videoTrackInfo.codec}" is not supported by mediabunny output`);
|
|
974
714
|
}
|
|
975
|
-
const mbAudioCodec = audioTrackInfo ? avbridgeAudioToMediabunny(audioTrackInfo.codec) : null;
|
|
976
715
|
const input = new mb.Input({
|
|
977
716
|
source: await buildMediabunnySourceFromInput(mb, ctx.source),
|
|
978
717
|
formats: mb.ALL_FORMATS
|
|
979
718
|
});
|
|
980
719
|
const allTracks = await input.getTracks();
|
|
981
720
|
const inputVideo = allTracks.find((t) => t.id === videoTrackInfo.id && t.isVideoTrack());
|
|
982
|
-
const inputAudio = audioTrackInfo ? allTracks.find((t) => t.id === audioTrackInfo.id && t.isAudioTrack()) : null;
|
|
983
721
|
if (!inputVideo || !inputVideo.isVideoTrack()) {
|
|
984
722
|
throw new Error("remux: video track not found in input");
|
|
985
723
|
}
|
|
986
|
-
if (audioTrackInfo && (!inputAudio || !inputAudio.isAudioTrack())) {
|
|
987
|
-
throw new Error("remux: audio track not found in input");
|
|
988
|
-
}
|
|
989
724
|
const videoConfig = await inputVideo.getDecoderConfig();
|
|
990
|
-
const audioConfig = inputAudio && inputAudio.isAudioTrack() ? await inputAudio.getDecoderConfig() : null;
|
|
991
725
|
const videoSink = new mb.EncodedPacketSink(inputVideo);
|
|
992
|
-
|
|
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();
|
|
993
753
|
let sink = null;
|
|
994
754
|
const stats = { videoPackets: 0, audioPackets: 0, bytesWritten: 0, fragments: 0 };
|
|
995
755
|
let destroyed = false;
|
|
@@ -1114,6 +874,30 @@ async function createRemuxPipeline(ctx, video) {
|
|
|
1114
874
|
pendingAutoPlay = autoPlay;
|
|
1115
875
|
if (sink) sink.setPlayOnSeek(autoPlay);
|
|
1116
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
|
+
},
|
|
1117
901
|
async destroy() {
|
|
1118
902
|
destroyed = true;
|
|
1119
903
|
pumpToken++;
|
|
@@ -1173,7 +957,19 @@ async function createRemuxSession(context, video) {
|
|
|
1173
957
|
const wasPlaying = !video.paused;
|
|
1174
958
|
await pipeline.seek(time, wasPlaying || wantPlay);
|
|
1175
959
|
},
|
|
1176
|
-
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);
|
|
1177
973
|
},
|
|
1178
974
|
async setSubtitleTrack(id) {
|
|
1179
975
|
const tracks = video.textTracks;
|
|
@@ -1226,6 +1022,9 @@ var VideoRenderer = class {
|
|
|
1226
1022
|
document.body.appendChild(this.canvas);
|
|
1227
1023
|
}
|
|
1228
1024
|
target.style.visibility = "hidden";
|
|
1025
|
+
const overlayParent = parent instanceof HTMLElement ? parent : document.body;
|
|
1026
|
+
this.subtitleOverlay = new SubtitleOverlay(overlayParent);
|
|
1027
|
+
this.watchTextTracks(target);
|
|
1229
1028
|
const ctx = this.canvas.getContext("2d");
|
|
1230
1029
|
if (!ctx) throw new Error("video renderer: failed to acquire 2D context");
|
|
1231
1030
|
this.ctx = ctx;
|
|
@@ -1251,6 +1050,15 @@ var VideoRenderer = class {
|
|
|
1251
1050
|
ticksWaiting = 0;
|
|
1252
1051
|
/** Cumulative count of ticks where PTS mode painted a frame. */
|
|
1253
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;
|
|
1254
1062
|
/**
|
|
1255
1063
|
* Calibration offset (microseconds) between video PTS and audio clock.
|
|
1256
1064
|
* Video PTS and AudioContext.currentTime can drift ~0.1% relative to
|
|
@@ -1294,9 +1102,80 @@ var VideoRenderer = class {
|
|
|
1294
1102
|
this.framesDroppedOverflow++;
|
|
1295
1103
|
}
|
|
1296
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
|
+
}
|
|
1297
1175
|
tick() {
|
|
1298
1176
|
if (this.destroyed) return;
|
|
1299
1177
|
this.rafHandle = requestAnimationFrame(this.tick);
|
|
1178
|
+
this.updateSubtitles();
|
|
1300
1179
|
if (this.queue.length === 0) return;
|
|
1301
1180
|
const playing = this.clock.isPlaying();
|
|
1302
1181
|
if (!playing) {
|
|
@@ -1425,6 +1304,11 @@ var VideoRenderer = class {
|
|
|
1425
1304
|
this.destroyed = true;
|
|
1426
1305
|
if (this.rafHandle != null) cancelAnimationFrame(this.rafHandle);
|
|
1427
1306
|
this.flush();
|
|
1307
|
+
if (this.subtitleOverlay) {
|
|
1308
|
+
this.subtitleOverlay.destroy();
|
|
1309
|
+
this.subtitleOverlay = null;
|
|
1310
|
+
}
|
|
1311
|
+
this.subtitleTrack = null;
|
|
1428
1312
|
this.canvas.remove();
|
|
1429
1313
|
this.target.style.visibility = "";
|
|
1430
1314
|
}
|
|
@@ -1674,7 +1558,8 @@ async function startHybridDecoder(opts) {
|
|
|
1674
1558
|
const readPkt = await libav.av_packet_alloc();
|
|
1675
1559
|
const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
|
|
1676
1560
|
const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
|
|
1677
|
-
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;
|
|
1678
1563
|
if (!videoStream && !audioStream) {
|
|
1679
1564
|
throw new Error("hybrid decoder: file has no decodable streams");
|
|
1680
1565
|
}
|
|
@@ -1970,6 +1855,71 @@ async function startHybridDecoder(opts) {
|
|
|
1970
1855
|
} catch {
|
|
1971
1856
|
}
|
|
1972
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
|
+
},
|
|
1973
1923
|
async seek(timeSec) {
|
|
1974
1924
|
const newToken = ++pumpToken;
|
|
1975
1925
|
if (pumpRunning) {
|
|
@@ -2027,161 +1977,9 @@ async function startHybridDecoder(opts) {
|
|
|
2027
1977
|
}
|
|
2028
1978
|
};
|
|
2029
1979
|
}
|
|
2030
|
-
function sanitizePacketTimestamp(pkt, nextUs, fallbackTimeBase) {
|
|
2031
|
-
const lo = pkt.pts ?? 0;
|
|
2032
|
-
const hi = pkt.ptshi ?? 0;
|
|
2033
|
-
const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
|
|
2034
|
-
if (isInvalid) {
|
|
2035
|
-
const us2 = nextUs();
|
|
2036
|
-
pkt.pts = us2;
|
|
2037
|
-
pkt.ptshi = 0;
|
|
2038
|
-
pkt.time_base_num = 1;
|
|
2039
|
-
pkt.time_base_den = 1e6;
|
|
2040
|
-
return;
|
|
2041
|
-
}
|
|
2042
|
-
const tb = fallbackTimeBase ?? [1, 1e6];
|
|
2043
|
-
const pts64 = hi * 4294967296 + lo;
|
|
2044
|
-
const us = Math.round(pts64 * 1e6 * tb[0] / tb[1]);
|
|
2045
|
-
if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
|
|
2046
|
-
pkt.pts = us;
|
|
2047
|
-
pkt.ptshi = us < 0 ? -1 : 0;
|
|
2048
|
-
pkt.time_base_num = 1;
|
|
2049
|
-
pkt.time_base_den = 1e6;
|
|
2050
|
-
return;
|
|
2051
|
-
}
|
|
2052
|
-
const fallback = nextUs();
|
|
2053
|
-
pkt.pts = fallback;
|
|
2054
|
-
pkt.ptshi = 0;
|
|
2055
|
-
pkt.time_base_num = 1;
|
|
2056
|
-
pkt.time_base_den = 1e6;
|
|
2057
|
-
}
|
|
2058
|
-
function sanitizeFrameTimestamp(frame, nextUs, fallbackTimeBase) {
|
|
2059
|
-
const lo = frame.pts ?? 0;
|
|
2060
|
-
const hi = frame.ptshi ?? 0;
|
|
2061
|
-
const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
|
|
2062
|
-
if (isInvalid) {
|
|
2063
|
-
const us2 = nextUs();
|
|
2064
|
-
frame.pts = us2;
|
|
2065
|
-
frame.ptshi = 0;
|
|
2066
|
-
return;
|
|
2067
|
-
}
|
|
2068
|
-
const tb = fallbackTimeBase ?? [1, 1e6];
|
|
2069
|
-
const pts64 = hi * 4294967296 + lo;
|
|
2070
|
-
const us = Math.round(pts64 * 1e6 * tb[0] / tb[1]);
|
|
2071
|
-
if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
|
|
2072
|
-
frame.pts = us;
|
|
2073
|
-
frame.ptshi = us < 0 ? -1 : 0;
|
|
2074
|
-
return;
|
|
2075
|
-
}
|
|
2076
|
-
const fallback = nextUs();
|
|
2077
|
-
frame.pts = fallback;
|
|
2078
|
-
frame.ptshi = 0;
|
|
2079
|
-
}
|
|
2080
|
-
var AV_SAMPLE_FMT_U8 = 0;
|
|
2081
|
-
var AV_SAMPLE_FMT_S16 = 1;
|
|
2082
|
-
var AV_SAMPLE_FMT_S32 = 2;
|
|
2083
|
-
var AV_SAMPLE_FMT_FLT = 3;
|
|
2084
|
-
var AV_SAMPLE_FMT_U8P = 5;
|
|
2085
|
-
var AV_SAMPLE_FMT_S16P = 6;
|
|
2086
|
-
var AV_SAMPLE_FMT_S32P = 7;
|
|
2087
|
-
var AV_SAMPLE_FMT_FLTP = 8;
|
|
2088
|
-
function libavFrameToInterleavedFloat32(frame) {
|
|
2089
|
-
const channels = frame.channels ?? frame.ch_layout_nb_channels ?? 1;
|
|
2090
|
-
const sampleRate = frame.sample_rate ?? 44100;
|
|
2091
|
-
const nbSamples = frame.nb_samples ?? 0;
|
|
2092
|
-
if (nbSamples === 0) return null;
|
|
2093
|
-
const out = new Float32Array(nbSamples * channels);
|
|
2094
|
-
switch (frame.format) {
|
|
2095
|
-
case AV_SAMPLE_FMT_FLTP: {
|
|
2096
|
-
const planes = ensurePlanes(frame.data, channels);
|
|
2097
|
-
for (let ch = 0; ch < channels; ch++) {
|
|
2098
|
-
const plane = asFloat32(planes[ch]);
|
|
2099
|
-
for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i];
|
|
2100
|
-
}
|
|
2101
|
-
return { data: out, channels, sampleRate };
|
|
2102
|
-
}
|
|
2103
|
-
case AV_SAMPLE_FMT_FLT: {
|
|
2104
|
-
const flat = asFloat32(frame.data);
|
|
2105
|
-
for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i];
|
|
2106
|
-
return { data: out, channels, sampleRate };
|
|
2107
|
-
}
|
|
2108
|
-
case AV_SAMPLE_FMT_S16P: {
|
|
2109
|
-
const planes = ensurePlanes(frame.data, channels);
|
|
2110
|
-
for (let ch = 0; ch < channels; ch++) {
|
|
2111
|
-
const plane = asInt16(planes[ch]);
|
|
2112
|
-
for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 32768;
|
|
2113
|
-
}
|
|
2114
|
-
return { data: out, channels, sampleRate };
|
|
2115
|
-
}
|
|
2116
|
-
case AV_SAMPLE_FMT_S16: {
|
|
2117
|
-
const flat = asInt16(frame.data);
|
|
2118
|
-
for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 32768;
|
|
2119
|
-
return { data: out, channels, sampleRate };
|
|
2120
|
-
}
|
|
2121
|
-
case AV_SAMPLE_FMT_S32P: {
|
|
2122
|
-
const planes = ensurePlanes(frame.data, channels);
|
|
2123
|
-
for (let ch = 0; ch < channels; ch++) {
|
|
2124
|
-
const plane = asInt32(planes[ch]);
|
|
2125
|
-
for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 2147483648;
|
|
2126
|
-
}
|
|
2127
|
-
return { data: out, channels, sampleRate };
|
|
2128
|
-
}
|
|
2129
|
-
case AV_SAMPLE_FMT_S32: {
|
|
2130
|
-
const flat = asInt32(frame.data);
|
|
2131
|
-
for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 2147483648;
|
|
2132
|
-
return { data: out, channels, sampleRate };
|
|
2133
|
-
}
|
|
2134
|
-
case AV_SAMPLE_FMT_U8P: {
|
|
2135
|
-
const planes = ensurePlanes(frame.data, channels);
|
|
2136
|
-
for (let ch = 0; ch < channels; ch++) {
|
|
2137
|
-
const plane = asUint8(planes[ch]);
|
|
2138
|
-
for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = (plane[i] - 128) / 128;
|
|
2139
|
-
}
|
|
2140
|
-
return { data: out, channels, sampleRate };
|
|
2141
|
-
}
|
|
2142
|
-
case AV_SAMPLE_FMT_U8: {
|
|
2143
|
-
const flat = asUint8(frame.data);
|
|
2144
|
-
for (let i = 0; i < nbSamples * channels; i++) out[i] = (flat[i] - 128) / 128;
|
|
2145
|
-
return { data: out, channels, sampleRate };
|
|
2146
|
-
}
|
|
2147
|
-
default:
|
|
2148
|
-
return null;
|
|
2149
|
-
}
|
|
2150
|
-
}
|
|
2151
|
-
function ensurePlanes(data, channels) {
|
|
2152
|
-
if (Array.isArray(data)) return data;
|
|
2153
|
-
const arr = data;
|
|
2154
|
-
const len = arr.length;
|
|
2155
|
-
const perChannel = Math.floor(len / channels);
|
|
2156
|
-
const planes = [];
|
|
2157
|
-
for (let ch = 0; ch < channels; ch++) {
|
|
2158
|
-
planes.push(arr.subarray ? arr.subarray(ch * perChannel, (ch + 1) * perChannel) : arr);
|
|
2159
|
-
}
|
|
2160
|
-
return planes;
|
|
2161
|
-
}
|
|
2162
|
-
function asFloat32(x) {
|
|
2163
|
-
if (x instanceof Float32Array) return x;
|
|
2164
|
-
const ta = x;
|
|
2165
|
-
return new Float32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
|
|
2166
|
-
}
|
|
2167
|
-
function asInt16(x) {
|
|
2168
|
-
if (x instanceof Int16Array) return x;
|
|
2169
|
-
const ta = x;
|
|
2170
|
-
return new Int16Array(ta.buffer, ta.byteOffset, ta.byteLength / 2);
|
|
2171
|
-
}
|
|
2172
|
-
function asInt32(x) {
|
|
2173
|
-
if (x instanceof Int32Array) return x;
|
|
2174
|
-
const ta = x;
|
|
2175
|
-
return new Int32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
|
|
2176
|
-
}
|
|
2177
|
-
function asUint8(x) {
|
|
2178
|
-
if (x instanceof Uint8Array) return x;
|
|
2179
|
-
const ta = x;
|
|
2180
|
-
return new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength);
|
|
2181
|
-
}
|
|
2182
1980
|
async function loadBridge() {
|
|
2183
1981
|
try {
|
|
2184
|
-
const wrapper = await import('./libav-import-
|
|
1982
|
+
const wrapper = await import('./libav-import-6MGLCXVQ.js');
|
|
2185
1983
|
return wrapper.libavBridge;
|
|
2186
1984
|
} catch (err) {
|
|
2187
1985
|
throw new Error(
|
|
@@ -2190,12 +1988,41 @@ async function loadBridge() {
|
|
|
2190
1988
|
}
|
|
2191
1989
|
}
|
|
2192
1990
|
|
|
1991
|
+
// src/util/time-ranges.ts
|
|
1992
|
+
function makeTimeRanges(ranges) {
|
|
1993
|
+
const frozen = ranges.slice();
|
|
1994
|
+
const impl = {
|
|
1995
|
+
get length() {
|
|
1996
|
+
return frozen.length;
|
|
1997
|
+
},
|
|
1998
|
+
start(index) {
|
|
1999
|
+
if (index < 0 || index >= frozen.length) {
|
|
2000
|
+
throw new DOMException(
|
|
2001
|
+
`TimeRanges.start: index ${index} out of range (length=${frozen.length})`,
|
|
2002
|
+
"IndexSizeError"
|
|
2003
|
+
);
|
|
2004
|
+
}
|
|
2005
|
+
return frozen[index][0];
|
|
2006
|
+
},
|
|
2007
|
+
end(index) {
|
|
2008
|
+
if (index < 0 || index >= frozen.length) {
|
|
2009
|
+
throw new DOMException(
|
|
2010
|
+
`TimeRanges.end: index ${index} out of range (length=${frozen.length})`,
|
|
2011
|
+
"IndexSizeError"
|
|
2012
|
+
);
|
|
2013
|
+
}
|
|
2014
|
+
return frozen[index][1];
|
|
2015
|
+
}
|
|
2016
|
+
};
|
|
2017
|
+
return impl;
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2193
2020
|
// src/strategies/hybrid/index.ts
|
|
2194
2021
|
var READY_AUDIO_BUFFER_SECONDS = 0.3;
|
|
2195
2022
|
var READY_TIMEOUT_SECONDS = 10;
|
|
2196
2023
|
async function createHybridSession(ctx, target, transport) {
|
|
2197
|
-
const { normalizeSource
|
|
2198
|
-
const source = await
|
|
2024
|
+
const { normalizeSource } = await import('./source-4TZ6KMNV.js');
|
|
2025
|
+
const source = await normalizeSource(ctx.source);
|
|
2199
2026
|
const fps = ctx.videoTracks[0]?.fps ?? 30;
|
|
2200
2027
|
const audio = new AudioOutput();
|
|
2201
2028
|
const renderer = new VideoRenderer(target, audio, fps);
|
|
@@ -2247,6 +2074,18 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2247
2074
|
get: () => ctx.duration ?? NaN
|
|
2248
2075
|
});
|
|
2249
2076
|
}
|
|
2077
|
+
Object.defineProperty(target, "readyState", {
|
|
2078
|
+
configurable: true,
|
|
2079
|
+
get: () => {
|
|
2080
|
+
if (!renderer.hasFrames()) return 0;
|
|
2081
|
+
if (!audio.isPlaying() && audio.bufferAhead() <= 0 && !audio.isNoAudio()) return 1;
|
|
2082
|
+
return 2;
|
|
2083
|
+
}
|
|
2084
|
+
});
|
|
2085
|
+
Object.defineProperty(target, "seekable", {
|
|
2086
|
+
configurable: true,
|
|
2087
|
+
get: () => makeTimeRanges(ctx.duration && Number.isFinite(ctx.duration) && ctx.duration > 0 ? [[0, ctx.duration]] : [])
|
|
2088
|
+
});
|
|
2250
2089
|
async function waitForBuffer() {
|
|
2251
2090
|
const start = performance.now();
|
|
2252
2091
|
while (true) {
|
|
@@ -2291,7 +2130,24 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2291
2130
|
async seek(time) {
|
|
2292
2131
|
await doSeek(time);
|
|
2293
2132
|
},
|
|
2294
|
-
async setAudioTrack(
|
|
2133
|
+
async setAudioTrack(id) {
|
|
2134
|
+
if (!ctx.audioTracks.some((t) => t.id === id)) {
|
|
2135
|
+
console.warn("[avbridge] hybrid: setAudioTrack \u2014 unknown track id", id);
|
|
2136
|
+
return;
|
|
2137
|
+
}
|
|
2138
|
+
const wasPlaying = audio.isPlaying();
|
|
2139
|
+
const currentTime = audio.now();
|
|
2140
|
+
await audio.pause().catch(() => {
|
|
2141
|
+
});
|
|
2142
|
+
await handles.setAudioTrack(id, currentTime).catch(
|
|
2143
|
+
(err) => console.warn("[avbridge] hybrid: handles.setAudioTrack failed:", err)
|
|
2144
|
+
);
|
|
2145
|
+
await audio.reset(currentTime);
|
|
2146
|
+
renderer.flush();
|
|
2147
|
+
if (wasPlaying) {
|
|
2148
|
+
await waitForBuffer();
|
|
2149
|
+
await audio.start();
|
|
2150
|
+
}
|
|
2295
2151
|
},
|
|
2296
2152
|
async setSubtitleTrack(_id) {
|
|
2297
2153
|
},
|
|
@@ -2311,6 +2167,8 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2311
2167
|
delete target.paused;
|
|
2312
2168
|
delete target.volume;
|
|
2313
2169
|
delete target.muted;
|
|
2170
|
+
delete target.readyState;
|
|
2171
|
+
delete target.seekable;
|
|
2314
2172
|
} catch {
|
|
2315
2173
|
}
|
|
2316
2174
|
},
|
|
@@ -2330,7 +2188,8 @@ async function startDecoder(opts) {
|
|
|
2330
2188
|
const readPkt = await libav.av_packet_alloc();
|
|
2331
2189
|
const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(opts.filename);
|
|
2332
2190
|
const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
|
|
2333
|
-
const
|
|
2191
|
+
const firstAudioTrackId = opts.context.audioTracks[0]?.id;
|
|
2192
|
+
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;
|
|
2334
2193
|
if (!videoStream && !audioStream) {
|
|
2335
2194
|
throw new Error("fallback decoder: file has no decodable streams");
|
|
2336
2195
|
}
|
|
@@ -2546,7 +2405,7 @@ async function startDecoder(opts) {
|
|
|
2546
2405
|
if (myToken !== pumpToken || destroyed) return;
|
|
2547
2406
|
for (const f of frames) {
|
|
2548
2407
|
if (myToken !== pumpToken || destroyed) return;
|
|
2549
|
-
|
|
2408
|
+
sanitizeFrameTimestamp(
|
|
2550
2409
|
f,
|
|
2551
2410
|
() => {
|
|
2552
2411
|
const ts = syntheticVideoUs;
|
|
@@ -2556,7 +2415,7 @@ async function startDecoder(opts) {
|
|
|
2556
2415
|
videoTimeBase
|
|
2557
2416
|
);
|
|
2558
2417
|
try {
|
|
2559
|
-
const vf = bridge.laFrameToVideoFrame(f,
|
|
2418
|
+
const vf = bridge.laFrameToVideoFrame(f, { timeBase: [1, 1e6] });
|
|
2560
2419
|
opts.renderer.enqueue(vf);
|
|
2561
2420
|
videoFramesDecoded++;
|
|
2562
2421
|
} catch (err) {
|
|
@@ -2584,7 +2443,7 @@ async function startDecoder(opts) {
|
|
|
2584
2443
|
if (myToken !== pumpToken || destroyed) return;
|
|
2585
2444
|
for (const f of frames) {
|
|
2586
2445
|
if (myToken !== pumpToken || destroyed) return;
|
|
2587
|
-
|
|
2446
|
+
sanitizeFrameTimestamp(
|
|
2588
2447
|
f,
|
|
2589
2448
|
() => {
|
|
2590
2449
|
const ts = syntheticAudioUs;
|
|
@@ -2595,7 +2454,7 @@ async function startDecoder(opts) {
|
|
|
2595
2454
|
},
|
|
2596
2455
|
audioTimeBase
|
|
2597
2456
|
);
|
|
2598
|
-
const samples =
|
|
2457
|
+
const samples = libavFrameToInterleavedFloat32(f);
|
|
2599
2458
|
if (samples) {
|
|
2600
2459
|
opts.audio.schedule(samples.data, samples.channels, samples.sampleRate);
|
|
2601
2460
|
audioFramesDecoded++;
|
|
@@ -2643,6 +2502,69 @@ async function startDecoder(opts) {
|
|
|
2643
2502
|
} catch {
|
|
2644
2503
|
}
|
|
2645
2504
|
},
|
|
2505
|
+
async setAudioTrack(trackId, timeSec) {
|
|
2506
|
+
if (audioStream && audioStream.index === trackId) return;
|
|
2507
|
+
const newStream = streams.find(
|
|
2508
|
+
(s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO && s.index === trackId
|
|
2509
|
+
);
|
|
2510
|
+
if (!newStream) {
|
|
2511
|
+
console.warn("[avbridge] fallback: setAudioTrack \u2014 no stream with id", trackId);
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
const newToken = ++pumpToken;
|
|
2515
|
+
if (pumpRunning) {
|
|
2516
|
+
try {
|
|
2517
|
+
await pumpRunning;
|
|
2518
|
+
} catch {
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
if (destroyed) return;
|
|
2522
|
+
if (audioDec) {
|
|
2523
|
+
try {
|
|
2524
|
+
await libav.ff_free_decoder?.(audioDec.c, audioDec.pkt, audioDec.frame);
|
|
2525
|
+
} catch {
|
|
2526
|
+
}
|
|
2527
|
+
audioDec = null;
|
|
2528
|
+
}
|
|
2529
|
+
try {
|
|
2530
|
+
const [, c, pkt, frame] = await libav.ff_init_decoder(newStream.codec_id, {
|
|
2531
|
+
codecpar: newStream.codecpar
|
|
2532
|
+
});
|
|
2533
|
+
audioDec = { c, pkt, frame };
|
|
2534
|
+
audioTimeBase = newStream.time_base_num && newStream.time_base_den ? [newStream.time_base_num, newStream.time_base_den] : void 0;
|
|
2535
|
+
} catch (err) {
|
|
2536
|
+
console.warn(
|
|
2537
|
+
"[avbridge] fallback: setAudioTrack init failed \u2014 falling back to no-audio mode:",
|
|
2538
|
+
err.message
|
|
2539
|
+
);
|
|
2540
|
+
audioDec = null;
|
|
2541
|
+
opts.audio.setNoAudio();
|
|
2542
|
+
}
|
|
2543
|
+
audioStream = newStream;
|
|
2544
|
+
try {
|
|
2545
|
+
const tsUs = Math.floor(timeSec * 1e6);
|
|
2546
|
+
const [tsLo, tsHi] = libav.f64toi64 ? libav.f64toi64(tsUs) : [tsUs | 0, Math.floor(tsUs / 4294967296)];
|
|
2547
|
+
await libav.av_seek_frame(
|
|
2548
|
+
fmt_ctx,
|
|
2549
|
+
-1,
|
|
2550
|
+
tsLo,
|
|
2551
|
+
tsHi,
|
|
2552
|
+
libav.AVSEEK_FLAG_BACKWARD ?? 0
|
|
2553
|
+
);
|
|
2554
|
+
} catch (err) {
|
|
2555
|
+
console.warn("[avbridge] fallback: setAudioTrack seek failed:", err);
|
|
2556
|
+
}
|
|
2557
|
+
try {
|
|
2558
|
+
if (videoDec) await libav.avcodec_flush_buffers?.(videoDec.c);
|
|
2559
|
+
} catch {
|
|
2560
|
+
}
|
|
2561
|
+
await flushBSF();
|
|
2562
|
+
syntheticVideoUs = Math.round(timeSec * 1e6);
|
|
2563
|
+
syntheticAudioUs = Math.round(timeSec * 1e6);
|
|
2564
|
+
pumpRunning = pumpLoop(newToken).catch(
|
|
2565
|
+
(err) => console.error("[avbridge] fallback pump failed (post-setAudioTrack):", err)
|
|
2566
|
+
);
|
|
2567
|
+
},
|
|
2646
2568
|
async seek(timeSec) {
|
|
2647
2569
|
const newToken = ++pumpToken;
|
|
2648
2570
|
if (pumpRunning) {
|
|
@@ -2699,138 +2621,9 @@ async function startDecoder(opts) {
|
|
|
2699
2621
|
}
|
|
2700
2622
|
};
|
|
2701
2623
|
}
|
|
2702
|
-
function sanitizeFrameTimestamp2(frame, nextUs, fallbackTimeBase) {
|
|
2703
|
-
const lo = frame.pts ?? 0;
|
|
2704
|
-
const hi = frame.ptshi ?? 0;
|
|
2705
|
-
const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
|
|
2706
|
-
if (isInvalid) {
|
|
2707
|
-
const us2 = nextUs();
|
|
2708
|
-
frame.pts = us2;
|
|
2709
|
-
frame.ptshi = 0;
|
|
2710
|
-
return { timeBase: [1, 1e6] };
|
|
2711
|
-
}
|
|
2712
|
-
const tb = fallbackTimeBase ?? [1, 1e6];
|
|
2713
|
-
const pts64 = hi * 4294967296 + lo;
|
|
2714
|
-
const us = Math.round(pts64 * 1e6 * tb[0] / tb[1]);
|
|
2715
|
-
if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
|
|
2716
|
-
frame.pts = us;
|
|
2717
|
-
frame.ptshi = us < 0 ? -1 : 0;
|
|
2718
|
-
return { timeBase: [1, 1e6] };
|
|
2719
|
-
}
|
|
2720
|
-
const fallback = nextUs();
|
|
2721
|
-
frame.pts = fallback;
|
|
2722
|
-
frame.ptshi = 0;
|
|
2723
|
-
return { timeBase: [1, 1e6] };
|
|
2724
|
-
}
|
|
2725
|
-
var AV_SAMPLE_FMT_U82 = 0;
|
|
2726
|
-
var AV_SAMPLE_FMT_S162 = 1;
|
|
2727
|
-
var AV_SAMPLE_FMT_S322 = 2;
|
|
2728
|
-
var AV_SAMPLE_FMT_FLT2 = 3;
|
|
2729
|
-
var AV_SAMPLE_FMT_U8P2 = 5;
|
|
2730
|
-
var AV_SAMPLE_FMT_S16P2 = 6;
|
|
2731
|
-
var AV_SAMPLE_FMT_S32P2 = 7;
|
|
2732
|
-
var AV_SAMPLE_FMT_FLTP2 = 8;
|
|
2733
|
-
function libavFrameToInterleavedFloat322(frame) {
|
|
2734
|
-
const channels = frame.channels ?? frame.ch_layout_nb_channels ?? 1;
|
|
2735
|
-
const sampleRate = frame.sample_rate ?? 44100;
|
|
2736
|
-
const nbSamples = frame.nb_samples ?? 0;
|
|
2737
|
-
if (nbSamples === 0) return null;
|
|
2738
|
-
const out = new Float32Array(nbSamples * channels);
|
|
2739
|
-
switch (frame.format) {
|
|
2740
|
-
case AV_SAMPLE_FMT_FLTP2: {
|
|
2741
|
-
const planes = ensurePlanes2(frame.data, channels);
|
|
2742
|
-
for (let ch = 0; ch < channels; ch++) {
|
|
2743
|
-
const plane = asFloat322(planes[ch]);
|
|
2744
|
-
for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i];
|
|
2745
|
-
}
|
|
2746
|
-
return { data: out, channels, sampleRate };
|
|
2747
|
-
}
|
|
2748
|
-
case AV_SAMPLE_FMT_FLT2: {
|
|
2749
|
-
const flat = asFloat322(frame.data);
|
|
2750
|
-
for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i];
|
|
2751
|
-
return { data: out, channels, sampleRate };
|
|
2752
|
-
}
|
|
2753
|
-
case AV_SAMPLE_FMT_S16P2: {
|
|
2754
|
-
const planes = ensurePlanes2(frame.data, channels);
|
|
2755
|
-
for (let ch = 0; ch < channels; ch++) {
|
|
2756
|
-
const plane = asInt162(planes[ch]);
|
|
2757
|
-
for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 32768;
|
|
2758
|
-
}
|
|
2759
|
-
return { data: out, channels, sampleRate };
|
|
2760
|
-
}
|
|
2761
|
-
case AV_SAMPLE_FMT_S162: {
|
|
2762
|
-
const flat = asInt162(frame.data);
|
|
2763
|
-
for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 32768;
|
|
2764
|
-
return { data: out, channels, sampleRate };
|
|
2765
|
-
}
|
|
2766
|
-
case AV_SAMPLE_FMT_S32P2: {
|
|
2767
|
-
const planes = ensurePlanes2(frame.data, channels);
|
|
2768
|
-
for (let ch = 0; ch < channels; ch++) {
|
|
2769
|
-
const plane = asInt322(planes[ch]);
|
|
2770
|
-
for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = plane[i] / 2147483648;
|
|
2771
|
-
}
|
|
2772
|
-
return { data: out, channels, sampleRate };
|
|
2773
|
-
}
|
|
2774
|
-
case AV_SAMPLE_FMT_S322: {
|
|
2775
|
-
const flat = asInt322(frame.data);
|
|
2776
|
-
for (let i = 0; i < nbSamples * channels; i++) out[i] = flat[i] / 2147483648;
|
|
2777
|
-
return { data: out, channels, sampleRate };
|
|
2778
|
-
}
|
|
2779
|
-
case AV_SAMPLE_FMT_U8P2: {
|
|
2780
|
-
const planes = ensurePlanes2(frame.data, channels);
|
|
2781
|
-
for (let ch = 0; ch < channels; ch++) {
|
|
2782
|
-
const plane = asUint82(planes[ch]);
|
|
2783
|
-
for (let i = 0; i < nbSamples; i++) out[i * channels + ch] = (plane[i] - 128) / 128;
|
|
2784
|
-
}
|
|
2785
|
-
return { data: out, channels, sampleRate };
|
|
2786
|
-
}
|
|
2787
|
-
case AV_SAMPLE_FMT_U82: {
|
|
2788
|
-
const flat = asUint82(frame.data);
|
|
2789
|
-
for (let i = 0; i < nbSamples * channels; i++) out[i] = (flat[i] - 128) / 128;
|
|
2790
|
-
return { data: out, channels, sampleRate };
|
|
2791
|
-
}
|
|
2792
|
-
default:
|
|
2793
|
-
if (!globalThis.__avbridgeLoggedSampleFmt) {
|
|
2794
|
-
globalThis.__avbridgeLoggedSampleFmt = frame.format;
|
|
2795
|
-
console.warn(`[avbridge] unsupported audio sample format from libav: ${frame.format}`);
|
|
2796
|
-
}
|
|
2797
|
-
return null;
|
|
2798
|
-
}
|
|
2799
|
-
}
|
|
2800
|
-
function ensurePlanes2(data, channels) {
|
|
2801
|
-
if (Array.isArray(data)) return data;
|
|
2802
|
-
const arr = data;
|
|
2803
|
-
const len = arr.length;
|
|
2804
|
-
const perChannel = Math.floor(len / channels);
|
|
2805
|
-
const planes = [];
|
|
2806
|
-
for (let ch = 0; ch < channels; ch++) {
|
|
2807
|
-
planes.push(arr.subarray ? arr.subarray(ch * perChannel, (ch + 1) * perChannel) : arr);
|
|
2808
|
-
}
|
|
2809
|
-
return planes;
|
|
2810
|
-
}
|
|
2811
|
-
function asFloat322(x) {
|
|
2812
|
-
if (x instanceof Float32Array) return x;
|
|
2813
|
-
const ta = x;
|
|
2814
|
-
return new Float32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
|
|
2815
|
-
}
|
|
2816
|
-
function asInt162(x) {
|
|
2817
|
-
if (x instanceof Int16Array) return x;
|
|
2818
|
-
const ta = x;
|
|
2819
|
-
return new Int16Array(ta.buffer, ta.byteOffset, ta.byteLength / 2);
|
|
2820
|
-
}
|
|
2821
|
-
function asInt322(x) {
|
|
2822
|
-
if (x instanceof Int32Array) return x;
|
|
2823
|
-
const ta = x;
|
|
2824
|
-
return new Int32Array(ta.buffer, ta.byteOffset, ta.byteLength / 4);
|
|
2825
|
-
}
|
|
2826
|
-
function asUint82(x) {
|
|
2827
|
-
if (x instanceof Uint8Array) return x;
|
|
2828
|
-
const ta = x;
|
|
2829
|
-
return new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength);
|
|
2830
|
-
}
|
|
2831
2624
|
async function loadBridge2() {
|
|
2832
2625
|
try {
|
|
2833
|
-
const wrapper = await import('./libav-import-
|
|
2626
|
+
const wrapper = await import('./libav-import-6MGLCXVQ.js');
|
|
2834
2627
|
return wrapper.libavBridge;
|
|
2835
2628
|
} catch (err) {
|
|
2836
2629
|
throw new Error(
|
|
@@ -2843,8 +2636,8 @@ async function loadBridge2() {
|
|
|
2843
2636
|
var READY_AUDIO_BUFFER_SECONDS2 = 0.04;
|
|
2844
2637
|
var READY_TIMEOUT_SECONDS2 = 3;
|
|
2845
2638
|
async function createFallbackSession(ctx, target, transport) {
|
|
2846
|
-
const { normalizeSource
|
|
2847
|
-
const source = await
|
|
2639
|
+
const { normalizeSource } = await import('./source-4TZ6KMNV.js');
|
|
2640
|
+
const source = await normalizeSource(ctx.source);
|
|
2848
2641
|
const fps = ctx.videoTracks[0]?.fps ?? 30;
|
|
2849
2642
|
const audio = new AudioOutput();
|
|
2850
2643
|
const renderer = new VideoRenderer(target, audio, fps);
|
|
@@ -2896,6 +2689,18 @@ async function createFallbackSession(ctx, target, transport) {
|
|
|
2896
2689
|
get: () => ctx.duration ?? NaN
|
|
2897
2690
|
});
|
|
2898
2691
|
}
|
|
2692
|
+
Object.defineProperty(target, "readyState", {
|
|
2693
|
+
configurable: true,
|
|
2694
|
+
get: () => {
|
|
2695
|
+
if (!renderer.hasFrames()) return 0;
|
|
2696
|
+
if (!audio.isPlaying() && audio.bufferAhead() <= 0 && !audio.isNoAudio()) return 1;
|
|
2697
|
+
return 2;
|
|
2698
|
+
}
|
|
2699
|
+
});
|
|
2700
|
+
Object.defineProperty(target, "seekable", {
|
|
2701
|
+
configurable: true,
|
|
2702
|
+
get: () => makeTimeRanges(ctx.duration && Number.isFinite(ctx.duration) && ctx.duration > 0 ? [[0, ctx.duration]] : [])
|
|
2703
|
+
});
|
|
2899
2704
|
async function waitForBuffer() {
|
|
2900
2705
|
const start = performance.now();
|
|
2901
2706
|
let firstFrameAtMs = 0;
|
|
@@ -2964,7 +2769,24 @@ async function createFallbackSession(ctx, target, transport) {
|
|
|
2964
2769
|
async seek(time) {
|
|
2965
2770
|
await doSeek(time);
|
|
2966
2771
|
},
|
|
2967
|
-
async setAudioTrack(
|
|
2772
|
+
async setAudioTrack(id) {
|
|
2773
|
+
if (!ctx.audioTracks.some((t) => t.id === id)) {
|
|
2774
|
+
console.warn("[avbridge] fallback: setAudioTrack \u2014 unknown track id", id);
|
|
2775
|
+
return;
|
|
2776
|
+
}
|
|
2777
|
+
const wasPlaying = audio.isPlaying();
|
|
2778
|
+
const currentTime = audio.now();
|
|
2779
|
+
await audio.pause().catch(() => {
|
|
2780
|
+
});
|
|
2781
|
+
await handles.setAudioTrack(id, currentTime).catch(
|
|
2782
|
+
(err) => console.warn("[avbridge] fallback: handles.setAudioTrack failed:", err)
|
|
2783
|
+
);
|
|
2784
|
+
await audio.reset(currentTime);
|
|
2785
|
+
renderer.flush();
|
|
2786
|
+
if (wasPlaying) {
|
|
2787
|
+
await waitForBuffer();
|
|
2788
|
+
await audio.start();
|
|
2789
|
+
}
|
|
2968
2790
|
},
|
|
2969
2791
|
async setSubtitleTrack(_id) {
|
|
2970
2792
|
},
|
|
@@ -2981,6 +2803,8 @@ async function createFallbackSession(ctx, target, transport) {
|
|
|
2981
2803
|
delete target.paused;
|
|
2982
2804
|
delete target.volume;
|
|
2983
2805
|
delete target.muted;
|
|
2806
|
+
delete target.readyState;
|
|
2807
|
+
delete target.seekable;
|
|
2984
2808
|
} catch {
|
|
2985
2809
|
}
|
|
2986
2810
|
},
|
|
@@ -3018,89 +2842,6 @@ function registerBuiltins(registry) {
|
|
|
3018
2842
|
registry.register(fallbackPlugin);
|
|
3019
2843
|
}
|
|
3020
2844
|
|
|
3021
|
-
// src/subtitles/vtt.ts
|
|
3022
|
-
function isVtt(text) {
|
|
3023
|
-
const trimmed = text.replace(/^\ufeff/, "").trimStart();
|
|
3024
|
-
return trimmed.startsWith("WEBVTT");
|
|
3025
|
-
}
|
|
3026
|
-
|
|
3027
|
-
// src/subtitles/index.ts
|
|
3028
|
-
async function discoverSidecars(file, directory) {
|
|
3029
|
-
const baseName = file.name.replace(/\.[^.]+$/, "");
|
|
3030
|
-
const found = [];
|
|
3031
|
-
for await (const [name, handle] of directory) {
|
|
3032
|
-
if (handle.kind !== "file") continue;
|
|
3033
|
-
if (!name.startsWith(baseName)) continue;
|
|
3034
|
-
const lower = name.toLowerCase();
|
|
3035
|
-
let format = null;
|
|
3036
|
-
if (lower.endsWith(".srt")) format = "srt";
|
|
3037
|
-
else if (lower.endsWith(".vtt")) format = "vtt";
|
|
3038
|
-
if (!format) continue;
|
|
3039
|
-
const sidecarFile = await handle.getFile();
|
|
3040
|
-
const url = URL.createObjectURL(sidecarFile);
|
|
3041
|
-
const langMatch = name.slice(baseName.length).match(/[._-]([a-z]{2,3})(?:[._-]|\.)/i);
|
|
3042
|
-
found.push({
|
|
3043
|
-
url,
|
|
3044
|
-
format,
|
|
3045
|
-
language: langMatch?.[1]
|
|
3046
|
-
});
|
|
3047
|
-
}
|
|
3048
|
-
return found;
|
|
3049
|
-
}
|
|
3050
|
-
var SubtitleResourceBag = class {
|
|
3051
|
-
urls = /* @__PURE__ */ new Set();
|
|
3052
|
-
/** Track an externally-created blob URL (e.g. from `discoverSidecars`). */
|
|
3053
|
-
track(url) {
|
|
3054
|
-
this.urls.add(url);
|
|
3055
|
-
}
|
|
3056
|
-
/** Convenience: create a blob URL and track it in one call. */
|
|
3057
|
-
createObjectURL(blob) {
|
|
3058
|
-
const url = URL.createObjectURL(blob);
|
|
3059
|
-
this.urls.add(url);
|
|
3060
|
-
return url;
|
|
3061
|
-
}
|
|
3062
|
-
/** Revoke every tracked URL. Idempotent — safe to call multiple times. */
|
|
3063
|
-
revokeAll() {
|
|
3064
|
-
for (const u of this.urls) URL.revokeObjectURL(u);
|
|
3065
|
-
this.urls.clear();
|
|
3066
|
-
}
|
|
3067
|
-
};
|
|
3068
|
-
async function attachSubtitleTracks(video, tracks, bag, onError, transport) {
|
|
3069
|
-
const doFetch = fetchWith(transport);
|
|
3070
|
-
for (const t of Array.from(video.querySelectorAll("track[data-avbridge]"))) {
|
|
3071
|
-
t.remove();
|
|
3072
|
-
}
|
|
3073
|
-
for (const t of tracks) {
|
|
3074
|
-
if (!t.sidecarUrl) continue;
|
|
3075
|
-
try {
|
|
3076
|
-
let url = t.sidecarUrl;
|
|
3077
|
-
if (t.format === "srt") {
|
|
3078
|
-
const res = await doFetch(t.sidecarUrl, transport?.requestInit);
|
|
3079
|
-
const text = await res.text();
|
|
3080
|
-
const vtt = srtToVtt(text);
|
|
3081
|
-
const blob = new Blob([vtt], { type: "text/vtt" });
|
|
3082
|
-
url = bag ? bag.createObjectURL(blob) : URL.createObjectURL(blob);
|
|
3083
|
-
} else if (t.format === "vtt") {
|
|
3084
|
-
const res = await doFetch(t.sidecarUrl, transport?.requestInit);
|
|
3085
|
-
const text = await res.text();
|
|
3086
|
-
if (!isVtt(text)) {
|
|
3087
|
-
console.warn("[avbridge] subtitle missing WEBVTT header:", t.sidecarUrl);
|
|
3088
|
-
}
|
|
3089
|
-
}
|
|
3090
|
-
const trackEl = document.createElement("track");
|
|
3091
|
-
trackEl.kind = "subtitles";
|
|
3092
|
-
trackEl.src = url;
|
|
3093
|
-
trackEl.srclang = t.language ?? "und";
|
|
3094
|
-
trackEl.label = t.language ?? `Subtitle ${t.id}`;
|
|
3095
|
-
trackEl.dataset.avbridge = "true";
|
|
3096
|
-
video.appendChild(trackEl);
|
|
3097
|
-
} catch (err) {
|
|
3098
|
-
const e = err instanceof Error ? err : new Error(String(err));
|
|
3099
|
-
onError?.(e, t);
|
|
3100
|
-
}
|
|
3101
|
-
}
|
|
3102
|
-
}
|
|
3103
|
-
|
|
3104
2845
|
// src/player.ts
|
|
3105
2846
|
var UnifiedPlayer = class _UnifiedPlayer {
|
|
3106
2847
|
/**
|
|
@@ -3213,17 +2954,15 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3213
2954
|
reason: decision.reason
|
|
3214
2955
|
});
|
|
3215
2956
|
await this.startSession(decision.strategy, decision.reason);
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
(
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
);
|
|
3226
|
-
}
|
|
2957
|
+
await attachSubtitleTracks(
|
|
2958
|
+
this.options.target,
|
|
2959
|
+
ctx.subtitleTracks,
|
|
2960
|
+
this.subtitleResources,
|
|
2961
|
+
(err, track) => {
|
|
2962
|
+
console.warn(`[avbridge] subtitle ${track.id} failed: ${err.message}`);
|
|
2963
|
+
},
|
|
2964
|
+
this.transport
|
|
2965
|
+
);
|
|
3227
2966
|
this.emitter.emitSticky("tracks", {
|
|
3228
2967
|
video: ctx.videoTracks,
|
|
3229
2968
|
audio: ctx.audioTracks,
|
|
@@ -3618,6 +3357,6 @@ function defaultFallbackChain(strategy) {
|
|
|
3618
3357
|
}
|
|
3619
3358
|
}
|
|
3620
3359
|
|
|
3621
|
-
export { FALLBACK_AUDIO_CODECS, FALLBACK_VIDEO_CODECS, NATIVE_AUDIO_CODECS, NATIVE_VIDEO_CODECS, UnifiedPlayer,
|
|
3622
|
-
//# sourceMappingURL=chunk-
|
|
3623
|
-
//# sourceMappingURL=chunk-
|
|
3360
|
+
export { FALLBACK_AUDIO_CODECS, FALLBACK_VIDEO_CODECS, NATIVE_AUDIO_CODECS, NATIVE_VIDEO_CODECS, UnifiedPlayer, classifyContext, createPlayer };
|
|
3361
|
+
//# sourceMappingURL=chunk-OGYHFY6K.js.map
|
|
3362
|
+
//# sourceMappingURL=chunk-OGYHFY6K.js.map
|