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
package/dist/index.js
CHANGED
|
@@ -1,401 +1,524 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
3
|
-
|
|
4
|
-
export {
|
|
5
|
-
import {
|
|
1
|
+
import { mimeForFormat, generateFilename, createOutputFormat } from './chunk-SMH6IOP2.js';
|
|
2
|
+
export { remux } from './chunk-SMH6IOP2.js';
|
|
3
|
+
export { FALLBACK_AUDIO_CODECS, FALLBACK_VIDEO_CODECS, NATIVE_AUDIO_CODECS, NATIVE_VIDEO_CODECS, UnifiedPlayer, classifyContext as classify, createPlayer } from './chunk-OGYHFY6K.js';
|
|
4
|
+
export { srtToVtt } from './chunk-5KVLE6YI.js';
|
|
5
|
+
import { probe, buildMediabunnySourceFromInput } from './chunk-SR3MPV4D.js';
|
|
6
|
+
export { probe } from './chunk-SR3MPV4D.js';
|
|
7
|
+
import { AvbridgeError, ERR_CONTAINER_NOT_SUPPORTED, ERR_TRANSCODE_UNSUPPORTED_COMBO, ERR_TRANSCODE_ABORTED, ERR_TRANSCODE_DECODE, ERR_CODEC_NOT_SUPPORTED } from './chunk-CPJLFFCC.js';
|
|
8
|
+
export { AvbridgeError, ERR_ALL_STRATEGIES_EXHAUSTED, ERR_CODEC_NOT_SUPPORTED, ERR_FETCH_FAILED, ERR_LIBAV_NOT_REACHABLE, ERR_MSE_CODEC_NOT_SUPPORTED, ERR_MSE_NOT_SUPPORTED, ERR_PLAYER_NOT_READY, ERR_PROBE_FAILED, ERR_PROBE_FETCH_FAILED, ERR_PROBE_UNKNOWN_CONTAINER, ERR_RANGE_NOT_SUPPORTED, ERR_STRATEGY_FAILED } from './chunk-CPJLFFCC.js';
|
|
9
|
+
import './chunk-LUFA47FP.js';
|
|
10
|
+
import './chunk-X2K3GIWE.js';
|
|
11
|
+
import './chunk-DCSOQH2N.js';
|
|
6
12
|
import './chunk-5DMTJVIU.js';
|
|
7
13
|
import './chunk-5YAWWKA3.js';
|
|
8
14
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (bytes.length >= 4 && bytes[0] === 0 && bytes[1] === 0 && bytes[2] === 0 && bytes[3] === 1) return true;
|
|
13
|
-
return false;
|
|
15
|
+
// src/convert/transcode-libav.ts
|
|
16
|
+
function isLibavTranscodeContainer(container) {
|
|
17
|
+
return container === "avi" || container === "asf" || container === "flv" || container === "rm";
|
|
14
18
|
}
|
|
15
|
-
function
|
|
16
|
-
const length = bytes.length;
|
|
17
|
-
let i = 0;
|
|
18
|
-
let nalStart = -1;
|
|
19
|
-
while (i < length) {
|
|
20
|
-
let scLen = 0;
|
|
21
|
-
if (i + 3 < length && bytes[i] === 0 && bytes[i + 1] === 0 && bytes[i + 2] === 0 && bytes[i + 3] === 1) {
|
|
22
|
-
scLen = 4;
|
|
23
|
-
} else if (i + 2 < length && bytes[i] === 0 && bytes[i + 1] === 0 && bytes[i + 2] === 1) {
|
|
24
|
-
scLen = 3;
|
|
25
|
-
}
|
|
26
|
-
if (scLen > 0) {
|
|
27
|
-
if (nalStart >= 0) {
|
|
28
|
-
yield bytes.subarray(nalStart, i);
|
|
29
|
-
}
|
|
30
|
-
nalStart = i + scLen;
|
|
31
|
-
i += scLen;
|
|
32
|
-
} else {
|
|
33
|
-
i += 1;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
if (nalStart >= 0 && nalStart < length) {
|
|
37
|
-
yield bytes.subarray(nalStart, length);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
function annexBToAvcc(annexB) {
|
|
41
|
-
const nalus = [];
|
|
42
|
-
let total = 0;
|
|
43
|
-
for (const nal of iterateAnnexBNalus(annexB)) {
|
|
44
|
-
nalus.push(nal);
|
|
45
|
-
total += 4 + nal.length;
|
|
46
|
-
}
|
|
47
|
-
const out = new Uint8Array(total);
|
|
48
|
-
let off = 0;
|
|
49
|
-
for (const nal of nalus) {
|
|
50
|
-
const len = nal.length;
|
|
51
|
-
out[off++] = len >>> 24 & 255;
|
|
52
|
-
out[off++] = len >>> 16 & 255;
|
|
53
|
-
out[off++] = len >>> 8 & 255;
|
|
54
|
-
out[off++] = len & 255;
|
|
55
|
-
out.set(nal, off);
|
|
56
|
-
off += len;
|
|
57
|
-
}
|
|
58
|
-
return out;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// src/convert/remux.ts
|
|
62
|
-
var MEDIABUNNY_CONTAINERS = /* @__PURE__ */ new Set([
|
|
63
|
-
"mp4",
|
|
64
|
-
"mov",
|
|
65
|
-
"mkv",
|
|
66
|
-
"webm",
|
|
67
|
-
"ogg",
|
|
68
|
-
"wav",
|
|
69
|
-
"mp3",
|
|
70
|
-
"flac",
|
|
71
|
-
"adts"
|
|
72
|
-
]);
|
|
73
|
-
async function remux(source, options = {}) {
|
|
19
|
+
async function transcodeViaLibav(ctx, options) {
|
|
74
20
|
const outputFormat = options.outputFormat ?? "mp4";
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return remuxViaMediAbunny(ctx, outputFormat, options);
|
|
81
|
-
}
|
|
82
|
-
return remuxViaLibav(ctx, outputFormat, options);
|
|
83
|
-
}
|
|
84
|
-
function validateRemuxEligibility(ctx, strict) {
|
|
85
|
-
const video = ctx.videoTracks[0];
|
|
86
|
-
const audio = ctx.audioTracks[0];
|
|
87
|
-
if (video) {
|
|
88
|
-
const mbCodec = avbridgeVideoToMediabunny(video.codec);
|
|
89
|
-
if (!mbCodec) {
|
|
90
|
-
throw new Error(
|
|
91
|
-
`Cannot remux: video codec "${video.codec}" is not supported for remuxing. Use transcode() to re-encode to a modern codec.`
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
if (audio) {
|
|
96
|
-
const mbCodec = avbridgeAudioToMediabunny(audio.codec);
|
|
97
|
-
if (!mbCodec) {
|
|
98
|
-
throw new Error(
|
|
99
|
-
`Cannot remux: audio codec "${audio.codec}" is not supported for remuxing. Use transcode() to re-encode to a modern codec.`
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
if (strict && video?.codec === "h264" && audio?.codec === "mp3") {
|
|
104
|
-
throw new Error(
|
|
105
|
-
`Cannot remux in strict mode: H.264 + MP3 is a best-effort combination that may produce playback issues in some browsers. Set strict: false to allow, or use transcode() to re-encode audio to AAC.`
|
|
21
|
+
if (outputFormat !== "mp4" && outputFormat !== "webm" && outputFormat !== "mkv") {
|
|
22
|
+
throw new AvbridgeError(
|
|
23
|
+
ERR_TRANSCODE_UNSUPPORTED_COMBO,
|
|
24
|
+
`legacy-container transcode supports MP4, WebM, and MKV output (got "${outputFormat}").`,
|
|
25
|
+
`Use outputFormat: "mp4", "webm", or "mkv".`
|
|
106
26
|
);
|
|
107
27
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
28
|
+
const videoCodec = options.videoCodec ?? (outputFormat === "webm" ? "vp9" : "h264");
|
|
29
|
+
const audioCodec = options.audioCodec ?? (outputFormat === "webm" ? "opus" : "aac");
|
|
30
|
+
const quality = options.quality ?? "medium";
|
|
31
|
+
options.signal?.throwIfAborted();
|
|
32
|
+
const [
|
|
33
|
+
mb,
|
|
34
|
+
{ openLibavDemux, sanitizePacketTimestamp, sanitizeFrameTimestamp, libavFrameToInterleavedFloat32 },
|
|
35
|
+
{ normalizeSource },
|
|
36
|
+
{ createOutputFormat: createOutputFormat2, mimeForFormat: mimeForFormat2, generateFilename: generateFilename2 }
|
|
37
|
+
] = await Promise.all([
|
|
38
|
+
import('mediabunny'),
|
|
39
|
+
import('./libav-demux-OWZ4T2YW.js'),
|
|
40
|
+
import('./source-4TZ6KMNV.js'),
|
|
41
|
+
import('./remux-WBYIZBBX.js')
|
|
42
|
+
]);
|
|
43
|
+
const normalized = await normalizeSource(ctx.source);
|
|
44
|
+
const demux = await openLibavDemux({
|
|
45
|
+
source: normalized,
|
|
46
|
+
filename: ctx.name ?? "input.bin",
|
|
47
|
+
context: ctx
|
|
48
|
+
// transport config is not yet threaded through ConvertOptions; add
|
|
49
|
+
// later if URL-source transcode with signed URLs becomes a need.
|
|
127
50
|
});
|
|
128
|
-
if (!conversion.isValid) {
|
|
129
|
-
const reasons = conversion.discardedTracks.map((d) => `${d.track.type} track discarded: ${d.reason}`).join("; ");
|
|
130
|
-
throw new Error(`Cannot remux: mediabunny rejected the conversion. ${reasons}`);
|
|
131
|
-
}
|
|
132
|
-
if (options.onProgress) {
|
|
133
|
-
const onProgress = options.onProgress;
|
|
134
|
-
conversion.onProgress = (p) => {
|
|
135
|
-
onProgress({ percent: p * 100, bytesWritten: 0 });
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
let abortHandler;
|
|
139
|
-
if (options.signal) {
|
|
140
|
-
options.signal.throwIfAborted();
|
|
141
|
-
abortHandler = () => void conversion.cancel();
|
|
142
|
-
options.signal.addEventListener("abort", abortHandler, { once: true });
|
|
143
|
-
}
|
|
144
51
|
try {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
52
|
+
let throwIfAborted2 = function() {
|
|
53
|
+
if (ac?.aborted) {
|
|
54
|
+
throw new AvbridgeError(
|
|
55
|
+
ERR_TRANSCODE_ABORTED,
|
|
56
|
+
"transcode: aborted by caller.",
|
|
57
|
+
void 0
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}, throwIfDrainError2 = function() {
|
|
61
|
+
if (drainError) {
|
|
62
|
+
const msg = drainError.message;
|
|
63
|
+
throw new AvbridgeError(
|
|
64
|
+
ERR_TRANSCODE_DECODE,
|
|
65
|
+
`transcode: video decoder error: ${msg}`,
|
|
66
|
+
"This usually indicates the WebCodecs decoder rejected a malformed packet."
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
var throwIfAborted = throwIfAborted2, throwIfDrainError = throwIfDrainError2;
|
|
71
|
+
options.signal?.throwIfAborted();
|
|
72
|
+
if (!demux.videoStream && !demux.audioStream) {
|
|
73
|
+
throw new Error("transcode: source has no decodable tracks");
|
|
149
74
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
container: outputFormat,
|
|
162
|
-
videoCodec: ctx.videoTracks[0]?.codec,
|
|
163
|
-
audioCodec: ctx.audioTracks[0]?.codec,
|
|
164
|
-
duration: ctx.duration,
|
|
165
|
-
filename
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
async function remuxViaLibav(ctx, outputFormat, options) {
|
|
169
|
-
let loadLibav;
|
|
170
|
-
let pickLibavVariant;
|
|
171
|
-
try {
|
|
172
|
-
const loader = await import('./libav-loader-27RDIN2I.js');
|
|
173
|
-
const routing = await import('./variant-routing-434STYAB.js');
|
|
174
|
-
loadLibav = loader.loadLibav;
|
|
175
|
-
pickLibavVariant = routing.pickLibavVariant;
|
|
176
|
-
} catch {
|
|
177
|
-
throw new Error(
|
|
178
|
-
`Cannot remux ${ctx.container.toUpperCase()} source: libav.js is not available. Install @libav.js/variant-webcodecs and libavjs-webcodecs-bridge, or build the custom avbridge variant with scripts/build-libav.sh.`
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
const variant = pickLibavVariant(ctx);
|
|
182
|
-
const libav = await loadLibav(variant);
|
|
183
|
-
const normalized = await normalizeSource(ctx.source);
|
|
184
|
-
const filename = ctx.name ?? `remux-input-${Date.now()}`;
|
|
185
|
-
const handle = await prepareLibavInput(libav, filename, normalized);
|
|
186
|
-
try {
|
|
187
|
-
return await doLibavRemux(libav, filename, ctx, outputFormat, options);
|
|
188
|
-
} finally {
|
|
189
|
-
await handle.detach().catch(() => {
|
|
75
|
+
if (options.outputStream) {
|
|
76
|
+
throw new AvbridgeError(
|
|
77
|
+
ERR_TRANSCODE_UNSUPPORTED_COMBO,
|
|
78
|
+
"outputStream is not yet supported for the libav-backed transcode path.",
|
|
79
|
+
"Remove the outputStream option to receive the transcoded blob in memory. Streaming output for this path is on the roadmap."
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const bufferTarget = new mb.BufferTarget();
|
|
83
|
+
const output = new mb.Output({
|
|
84
|
+
format: createOutputFormat2(mb, outputFormat),
|
|
85
|
+
target: bufferTarget
|
|
190
86
|
});
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
|
|
87
|
+
const bridge = await loadBridge();
|
|
88
|
+
let videoDecoder = null;
|
|
89
|
+
let videoSoftDec = null;
|
|
90
|
+
let videoSource = null;
|
|
91
|
+
let videoBsfCtx = null;
|
|
92
|
+
let videoBsfPkt = null;
|
|
93
|
+
let videoWidth = 0;
|
|
94
|
+
let videoHeight = 0;
|
|
95
|
+
let videoTimeBase;
|
|
96
|
+
const frameQueue = [];
|
|
97
|
+
const MAX_QUEUE = 16;
|
|
98
|
+
let draining = false;
|
|
99
|
+
let drainError = null;
|
|
100
|
+
let activeDrain = null;
|
|
101
|
+
const drain = () => {
|
|
102
|
+
if (draining) return activeDrain ?? Promise.resolve();
|
|
103
|
+
draining = true;
|
|
104
|
+
const run = (async () => {
|
|
105
|
+
try {
|
|
106
|
+
while (frameQueue.length > 0 && !drainError) {
|
|
107
|
+
const frame = frameQueue.shift();
|
|
108
|
+
try {
|
|
109
|
+
const sample = new mb.VideoSample(frame, {
|
|
110
|
+
timestamp: (frame.timestamp ?? 0) / 1e6
|
|
111
|
+
// µs → s
|
|
112
|
+
});
|
|
113
|
+
await videoSource.add(sample);
|
|
114
|
+
} finally {
|
|
115
|
+
frame.close();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch (err) {
|
|
119
|
+
drainError = err;
|
|
120
|
+
while (frameQueue.length > 0) {
|
|
121
|
+
try {
|
|
122
|
+
frameQueue.shift().close();
|
|
123
|
+
} catch {
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} finally {
|
|
127
|
+
draining = false;
|
|
128
|
+
activeDrain = null;
|
|
129
|
+
}
|
|
130
|
+
})();
|
|
131
|
+
activeDrain = run;
|
|
132
|
+
return run;
|
|
133
|
+
};
|
|
134
|
+
if (demux.videoStream && !options.dropVideo) {
|
|
135
|
+
try {
|
|
136
|
+
const bitDepth = ctx.videoTracks[0]?.bitDepth ?? 8;
|
|
137
|
+
if (bitDepth > 8) {
|
|
138
|
+
throw new AvbridgeError(
|
|
139
|
+
ERR_TRANSCODE_UNSUPPORTED_COMBO,
|
|
140
|
+
`transcode: 10-bit video is not supported in this release (source bit depth: ${bitDepth}).`,
|
|
141
|
+
`Phase 1 transcode handles 8-bit video only. 10-bit support is on the roadmap.`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (demux.videoStream.time_base_num && demux.videoStream.time_base_den) {
|
|
145
|
+
videoTimeBase = [demux.videoStream.time_base_num, demux.videoStream.time_base_den];
|
|
146
|
+
}
|
|
147
|
+
let config = null;
|
|
148
|
+
try {
|
|
149
|
+
config = await bridge.videoStreamToConfig(demux.libav, demux.videoStream);
|
|
150
|
+
} catch {
|
|
151
|
+
config = null;
|
|
152
|
+
}
|
|
153
|
+
const supported = config ? await VideoDecoder.isConfigSupported(config).catch(() => ({ supported: false })) : { supported: false };
|
|
154
|
+
videoWidth = config?.codedWidth ?? ctx.videoTracks[0]?.width ?? 0;
|
|
155
|
+
videoHeight = config?.codedHeight ?? ctx.videoTracks[0]?.height ?? 0;
|
|
156
|
+
if (config && supported.supported) {
|
|
157
|
+
videoDecoder = new VideoDecoder({
|
|
158
|
+
output: (frame) => {
|
|
159
|
+
if (frameQueue.length >= MAX_QUEUE) {
|
|
160
|
+
frame.close();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
frameQueue.push(frame);
|
|
164
|
+
void drain();
|
|
165
|
+
},
|
|
166
|
+
error: (err) => {
|
|
167
|
+
drainError = err;
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
videoDecoder.configure(config);
|
|
171
|
+
} else {
|
|
172
|
+
const libavSoft = demux.libav;
|
|
173
|
+
const [, c, pkt, frame] = await libavSoft.ff_init_decoder(
|
|
174
|
+
demux.videoStream.codec_id,
|
|
175
|
+
{ codecpar: demux.videoStream.codecpar }
|
|
176
|
+
);
|
|
177
|
+
videoSoftDec = { c, pkt, frame };
|
|
178
|
+
}
|
|
179
|
+
videoSource = new mb.VideoSampleSource({
|
|
180
|
+
codec: avbridgeVideoToMediabunny(videoCodec),
|
|
181
|
+
bitrate: qualityToMediabunny(mb, quality, options.videoBitrate),
|
|
182
|
+
...options.frameRate !== void 0 ? { frameRate: options.frameRate } : {},
|
|
183
|
+
...options.hardwareAcceleration !== void 0 ? { hardwareAcceleration: options.hardwareAcceleration } : {},
|
|
184
|
+
// Progress reporting: media-time-based via each encoded packet.
|
|
185
|
+
onEncodedPacket: options.onProgress ? (packet) => {
|
|
186
|
+
const t = packet.timestamp;
|
|
187
|
+
if (Number.isFinite(t) && ctx.duration && ctx.duration > 0) {
|
|
188
|
+
const pct = Math.min(100, t / ctx.duration * 100);
|
|
189
|
+
options.onProgress({ percent: pct, bytesWritten: 0 });
|
|
190
|
+
}
|
|
191
|
+
} : void 0
|
|
192
|
+
});
|
|
193
|
+
const videoMeta = {};
|
|
194
|
+
if (options.width !== void 0) videoMeta.width = options.width;
|
|
195
|
+
else if (videoWidth > 0) videoMeta.width = videoWidth;
|
|
196
|
+
if (options.height !== void 0) videoMeta.height = options.height;
|
|
197
|
+
else if (videoHeight > 0) videoMeta.height = videoHeight;
|
|
198
|
+
if (options.frameRate !== void 0) videoMeta.frameRate = options.frameRate;
|
|
199
|
+
output.addVideoTrack(videoSource, videoMeta);
|
|
200
|
+
if (ctx.videoTracks[0]?.codec === "mpeg4") {
|
|
201
|
+
const runtime = demux.libav;
|
|
202
|
+
try {
|
|
203
|
+
videoBsfCtx = await runtime.av_bsf_list_parse_str_js("mpeg4_unpack_bframes");
|
|
204
|
+
if (videoBsfCtx != null && videoBsfCtx >= 0) {
|
|
205
|
+
const parIn = await runtime.AVBSFContext_par_in(videoBsfCtx);
|
|
206
|
+
await runtime.avcodec_parameters_copy(parIn, demux.videoStream.codecpar);
|
|
207
|
+
await runtime.av_bsf_init(videoBsfCtx);
|
|
208
|
+
videoBsfPkt = await demux.libav.av_packet_alloc();
|
|
209
|
+
} else {
|
|
210
|
+
videoBsfCtx = null;
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
213
|
+
videoBsfCtx = null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} catch (err) {
|
|
217
|
+
if (err instanceof AvbridgeError) throw err;
|
|
218
|
+
throw new AvbridgeError(
|
|
219
|
+
ERR_CODEC_NOT_SUPPORTED,
|
|
220
|
+
`transcode: video decoder init failed: ${err.message}`,
|
|
221
|
+
`The source's video codec may not be supported by this browser's WebCodecs implementation.`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
239
224
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
225
|
+
let audioDec = null;
|
|
226
|
+
let audioSource = null;
|
|
227
|
+
let audioTimeBase;
|
|
228
|
+
const includeAudio = demux.audioStream && !options.dropAudio;
|
|
229
|
+
if (includeAudio) {
|
|
230
|
+
try {
|
|
231
|
+
const libav = demux.libav;
|
|
232
|
+
const [, c, pkt, frame] = await libav.ff_init_decoder(
|
|
233
|
+
demux.audioStream.codec_id,
|
|
234
|
+
{ codecpar: demux.audioStream.codecpar }
|
|
235
|
+
);
|
|
236
|
+
audioDec = { c, pkt, frame };
|
|
237
|
+
if (demux.audioStream.time_base_num && demux.audioStream.time_base_den) {
|
|
238
|
+
audioTimeBase = [
|
|
239
|
+
demux.audioStream.time_base_num,
|
|
240
|
+
demux.audioStream.time_base_den
|
|
241
|
+
];
|
|
242
|
+
}
|
|
243
|
+
audioSource = new mb.AudioSampleSource({
|
|
244
|
+
codec: avbridgeAudioToMediabunny(audioCodec),
|
|
245
|
+
bitrate: qualityToMediabunny(mb, quality, options.audioBitrate)
|
|
246
|
+
});
|
|
247
|
+
output.addAudioTrack(audioSource);
|
|
248
|
+
} catch (err) {
|
|
249
|
+
const codecName = ctx.audioTracks[0]?.codec ?? "unknown";
|
|
250
|
+
throw new AvbridgeError(
|
|
251
|
+
ERR_CODEC_NOT_SUPPORTED,
|
|
252
|
+
`transcode: no decoder available for audio codec "${codecName}" in this libav variant (${err.message}).`,
|
|
253
|
+
`The file may still play via createPlayer() (fallback strategy). Pass { dropAudio: true } to transcode video-only.`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
} else if (options.dropAudio) {
|
|
257
|
+
}
|
|
258
|
+
if (!videoSource && !audioSource) {
|
|
259
|
+
throw new AvbridgeError(
|
|
260
|
+
ERR_TRANSCODE_UNSUPPORTED_COMBO,
|
|
261
|
+
"transcode: no video or audio track to encode (did you set both dropVideo and dropAudio?).",
|
|
262
|
+
"Remove dropVideo or dropAudio to include at least one track."
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
await output.start();
|
|
266
|
+
const videoFps = ctx.videoTracks[0]?.fps && ctx.videoTracks[0].fps > 0 ? ctx.videoTracks[0].fps : 30;
|
|
267
|
+
const videoFrameStepUs = Math.max(1, Math.round(1e6 / videoFps));
|
|
268
|
+
let syntheticVideoUs = 0;
|
|
269
|
+
let syntheticAudioUs = 0;
|
|
270
|
+
const libavFull = demux.libav;
|
|
271
|
+
async function applyBSF(packets) {
|
|
272
|
+
if (!videoBsfCtx || !videoBsfPkt) return packets;
|
|
273
|
+
const out = [];
|
|
274
|
+
for (const pkt of packets) {
|
|
275
|
+
await libavFull.ff_copyin_packet(videoBsfPkt, pkt);
|
|
276
|
+
const sendErr = await libavFull.av_bsf_send_packet(videoBsfCtx, videoBsfPkt);
|
|
277
|
+
if (sendErr < 0) {
|
|
278
|
+
out.push(pkt);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
while (true) {
|
|
282
|
+
const recvErr = await libavFull.av_bsf_receive_packet(videoBsfCtx, videoBsfPkt);
|
|
283
|
+
if (recvErr < 0) break;
|
|
284
|
+
out.push(await libavFull.ff_copyout_packet(videoBsfPkt));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
289
|
+
const ac = options.signal;
|
|
290
|
+
const onVideoPacketsWebCodecs = videoDecoder ? async (pkts) => {
|
|
291
|
+
throwIfAborted2();
|
|
292
|
+
throwIfDrainError2();
|
|
293
|
+
while (!ac?.aborted && (videoDecoder.decodeQueueSize > 16 || frameQueue.length >= MAX_QUEUE - 2)) {
|
|
294
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
295
|
+
}
|
|
296
|
+
throwIfAborted2();
|
|
297
|
+
const processed = await applyBSF(pkts);
|
|
298
|
+
const bridgeAny = bridge;
|
|
299
|
+
for (const pkt of processed) {
|
|
244
300
|
sanitizePacketTimestamp(pkt, () => {
|
|
245
301
|
const ts = syntheticVideoUs;
|
|
246
302
|
syntheticVideoUs += videoFrameStepUs;
|
|
247
303
|
return ts;
|
|
248
304
|
}, videoTimeBase);
|
|
249
|
-
|
|
250
|
-
|
|
305
|
+
try {
|
|
306
|
+
const chunk = bridgeAny.packetToEncodedVideoChunk(pkt, demux.videoStream);
|
|
307
|
+
videoDecoder.decode(chunk);
|
|
308
|
+
} catch (err) {
|
|
309
|
+
throw new AvbridgeError(
|
|
310
|
+
ERR_TRANSCODE_DECODE,
|
|
311
|
+
`transcode: packet \u2192 EncodedVideoChunk failed: ${err.message}`,
|
|
312
|
+
void 0
|
|
313
|
+
);
|
|
251
314
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
315
|
+
}
|
|
316
|
+
} : void 0;
|
|
317
|
+
const onVideoPacketsSoftware = videoSoftDec ? async (pkts) => {
|
|
318
|
+
throwIfAborted2();
|
|
319
|
+
throwIfDrainError2();
|
|
320
|
+
while (!ac?.aborted && frameQueue.length >= MAX_QUEUE - 2) {
|
|
321
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
322
|
+
}
|
|
323
|
+
throwIfAborted2();
|
|
324
|
+
const libavSoft = demux.libav;
|
|
325
|
+
let frames;
|
|
326
|
+
try {
|
|
327
|
+
frames = await libavSoft.ff_decode_multi(
|
|
328
|
+
videoSoftDec.c,
|
|
329
|
+
videoSoftDec.pkt,
|
|
330
|
+
videoSoftDec.frame,
|
|
331
|
+
pkts,
|
|
332
|
+
{ ignoreErrors: true }
|
|
256
333
|
);
|
|
257
|
-
|
|
334
|
+
} catch (err) {
|
|
335
|
+
throw new AvbridgeError(
|
|
336
|
+
ERR_TRANSCODE_DECODE,
|
|
337
|
+
`transcode: software video decode failed: ${err.message}`,
|
|
338
|
+
void 0
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
for (const f of frames) {
|
|
342
|
+
sanitizeFrameTimestamp(f, () => {
|
|
343
|
+
const ts = syntheticVideoUs;
|
|
344
|
+
syntheticVideoUs += videoFrameStepUs;
|
|
345
|
+
return ts;
|
|
346
|
+
}, videoTimeBase);
|
|
347
|
+
try {
|
|
348
|
+
const vf = bridge.laFrameToVideoFrame(f, { timeBase: [1, 1e6] });
|
|
349
|
+
if (frameQueue.length >= MAX_QUEUE) {
|
|
350
|
+
vf.close();
|
|
351
|
+
} else {
|
|
352
|
+
frameQueue.push(vf);
|
|
353
|
+
void drain();
|
|
354
|
+
}
|
|
355
|
+
} catch (err) {
|
|
356
|
+
throw new AvbridgeError(
|
|
357
|
+
ERR_TRANSCODE_DECODE,
|
|
358
|
+
`transcode: laFrameToVideoFrame failed: ${err.message}`,
|
|
359
|
+
void 0
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
} : void 0;
|
|
364
|
+
await demux.pump({
|
|
365
|
+
signal: ac,
|
|
366
|
+
onVideoPackets: onVideoPacketsWebCodecs ?? onVideoPacketsSoftware,
|
|
367
|
+
onAudioPackets: audioDec ? async (pkts) => {
|
|
368
|
+
throwIfAborted2();
|
|
369
|
+
await decodeAudioBatch(pkts, false);
|
|
370
|
+
} : void 0,
|
|
371
|
+
onEof: async () => {
|
|
372
|
+
if (videoDecoder && videoDecoder.state === "configured") {
|
|
373
|
+
try {
|
|
374
|
+
await videoDecoder.flush();
|
|
375
|
+
} catch {
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (videoSoftDec) {
|
|
379
|
+
const libavSoft = demux.libav;
|
|
380
|
+
try {
|
|
381
|
+
const tail = await libavSoft.ff_decode_multi(
|
|
382
|
+
videoSoftDec.c,
|
|
383
|
+
videoSoftDec.pkt,
|
|
384
|
+
videoSoftDec.frame,
|
|
385
|
+
[],
|
|
386
|
+
{ fin: true, ignoreErrors: true }
|
|
387
|
+
);
|
|
388
|
+
for (const f of tail) {
|
|
389
|
+
sanitizeFrameTimestamp(f, () => {
|
|
390
|
+
const ts = syntheticVideoUs;
|
|
391
|
+
syntheticVideoUs += videoFrameStepUs;
|
|
392
|
+
return ts;
|
|
393
|
+
}, videoTimeBase);
|
|
394
|
+
try {
|
|
395
|
+
const vf = bridge.laFrameToVideoFrame(f, { timeBase: [1, 1e6] });
|
|
396
|
+
frameQueue.push(vf);
|
|
397
|
+
void drain();
|
|
398
|
+
} catch {
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
} catch {
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
await drain();
|
|
405
|
+
if (audioDec) {
|
|
406
|
+
await decodeAudioBatch([], true);
|
|
407
|
+
}
|
|
258
408
|
}
|
|
409
|
+
});
|
|
410
|
+
throwIfAborted2();
|
|
411
|
+
throwIfDrainError2();
|
|
412
|
+
videoSource?.close();
|
|
413
|
+
audioSource?.close();
|
|
414
|
+
await output.finalize();
|
|
415
|
+
if (!bufferTarget.buffer) {
|
|
416
|
+
throw new Error("transcode: mediabunny produced no output buffer");
|
|
259
417
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
418
|
+
const mimeType = mimeForFormat2(outputFormat);
|
|
419
|
+
const blob = new Blob([bufferTarget.buffer], { type: mimeType });
|
|
420
|
+
options.onProgress?.({ percent: 100, bytesWritten: blob.size });
|
|
421
|
+
return {
|
|
422
|
+
blob,
|
|
423
|
+
mimeType,
|
|
424
|
+
container: outputFormat,
|
|
425
|
+
videoCodec: videoSource ? videoCodec : void 0,
|
|
426
|
+
audioCodec: audioSource ? audioCodec : void 0,
|
|
427
|
+
duration: ctx.duration,
|
|
428
|
+
filename: generateFilename2(ctx.name, outputFormat)
|
|
429
|
+
};
|
|
430
|
+
async function decodeAudioBatch(pkts, flush) {
|
|
431
|
+
if (!audioDec || !audioSource) return;
|
|
432
|
+
const libav = demux.libav;
|
|
433
|
+
let frames;
|
|
434
|
+
try {
|
|
435
|
+
frames = await libav.ff_decode_multi(
|
|
436
|
+
audioDec.c,
|
|
437
|
+
audioDec.pkt,
|
|
438
|
+
audioDec.frame,
|
|
439
|
+
pkts,
|
|
440
|
+
flush ? { fin: true, ignoreErrors: true } : { ignoreErrors: true }
|
|
441
|
+
);
|
|
442
|
+
} catch (err) {
|
|
443
|
+
throw new AvbridgeError(
|
|
444
|
+
ERR_TRANSCODE_DECODE,
|
|
445
|
+
`transcode: audio decode failed: ${err.message}`,
|
|
446
|
+
void 0
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
for (const f of frames) {
|
|
450
|
+
sanitizeFrameTimestamp(f, () => {
|
|
263
451
|
const ts = syntheticAudioUs;
|
|
264
|
-
const
|
|
265
|
-
|
|
452
|
+
const samples = f.nb_samples ?? 1024;
|
|
453
|
+
const sampleRate = f.sample_rate ?? 44100;
|
|
454
|
+
syntheticAudioUs += Math.round(samples * 1e6 / sampleRate);
|
|
266
455
|
return ts;
|
|
267
456
|
}, audioTimeBase);
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
457
|
+
const pcm = libavFrameToInterleavedFloat32(f);
|
|
458
|
+
if (!pcm) continue;
|
|
459
|
+
const sample = new mb.AudioSample({
|
|
460
|
+
data: pcm.data,
|
|
461
|
+
format: "f32",
|
|
462
|
+
numberOfChannels: pcm.channels,
|
|
463
|
+
sampleRate: pcm.sampleRate,
|
|
464
|
+
timestamp: (f.pts ?? 0) / 1e6
|
|
465
|
+
});
|
|
466
|
+
await audioSource.add(sample);
|
|
274
467
|
}
|
|
275
468
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const currentUs = Math.max(lastVideoTs, lastAudioTs);
|
|
281
|
-
const percent = Math.min(99, currentUs / durationUs * 100);
|
|
282
|
-
options.onProgress({ percent, bytesWritten: 0 });
|
|
283
|
-
}
|
|
284
|
-
if (readErr === libav.AVERROR_EOF) break;
|
|
285
|
-
if (readErr && readErr !== 0 && readErr !== -libav.EAGAIN) {
|
|
286
|
-
console.warn("[avbridge] remux: ff_read_frame_multi returned", readErr);
|
|
287
|
-
break;
|
|
469
|
+
} finally {
|
|
470
|
+
try {
|
|
471
|
+
await demux.destroy();
|
|
472
|
+
} catch {
|
|
288
473
|
}
|
|
289
474
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
await libav.av_packet_free?.(readPkt);
|
|
293
|
-
} catch {
|
|
294
|
-
}
|
|
475
|
+
}
|
|
476
|
+
async function loadBridge() {
|
|
295
477
|
try {
|
|
296
|
-
await libav.
|
|
297
|
-
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
throw new Error("Remux failed: mediabunny produced no output buffer.");
|
|
478
|
+
const wrapper = await import('./libav-import-6MGLCXVQ.js');
|
|
479
|
+
return wrapper.libavBridge;
|
|
480
|
+
} catch (err) {
|
|
481
|
+
throw new Error(`failed to load libavjs-webcodecs-bridge: ${err.message}`);
|
|
301
482
|
}
|
|
302
|
-
const mimeType = mimeForFormat(outputFormat);
|
|
303
|
-
const blob = new Blob([target.buffer], { type: mimeType });
|
|
304
|
-
const outputFilename = generateFilename(ctx.name, outputFormat);
|
|
305
|
-
options.onProgress?.({ percent: 100, bytesWritten: blob.size });
|
|
306
|
-
return {
|
|
307
|
-
blob,
|
|
308
|
-
mimeType,
|
|
309
|
-
container: outputFormat,
|
|
310
|
-
videoCodec: videoTrackInfo?.codec,
|
|
311
|
-
audioCodec: audioTrackInfo?.codec,
|
|
312
|
-
duration: ctx.duration,
|
|
313
|
-
filename: outputFilename
|
|
314
|
-
};
|
|
315
483
|
}
|
|
316
|
-
function
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
const tb = fallbackTimeBase ?? [1, 1e6];
|
|
329
|
-
const pts64 = hi * 4294967296 + lo;
|
|
330
|
-
const us = Math.round(pts64 * 1e6 * tb[0] / tb[1]);
|
|
331
|
-
if (Number.isFinite(us) && Math.abs(us) <= Number.MAX_SAFE_INTEGER) {
|
|
332
|
-
pkt.pts = us;
|
|
333
|
-
pkt.ptshi = us < 0 ? -1 : 0;
|
|
334
|
-
pkt.time_base_num = 1;
|
|
335
|
-
pkt.time_base_den = 1e6;
|
|
336
|
-
return;
|
|
484
|
+
function avbridgeVideoToMediabunny(c) {
|
|
485
|
+
switch (c) {
|
|
486
|
+
case "h264":
|
|
487
|
+
return "avc";
|
|
488
|
+
case "h265":
|
|
489
|
+
return "hevc";
|
|
490
|
+
case "vp9":
|
|
491
|
+
return "vp9";
|
|
492
|
+
case "av1":
|
|
493
|
+
return "av1";
|
|
337
494
|
}
|
|
338
|
-
const fallback = nextUs();
|
|
339
|
-
pkt.pts = fallback;
|
|
340
|
-
pkt.ptshi = 0;
|
|
341
|
-
pkt.time_base_num = 1;
|
|
342
|
-
pkt.time_base_den = 1e6;
|
|
343
495
|
}
|
|
344
|
-
function
|
|
345
|
-
switch (
|
|
346
|
-
case "
|
|
347
|
-
return
|
|
348
|
-
case "
|
|
349
|
-
return
|
|
350
|
-
case "
|
|
351
|
-
return
|
|
352
|
-
default:
|
|
353
|
-
return new mb.Mp4OutputFormat({ fastStart: "in-memory" });
|
|
496
|
+
function avbridgeAudioToMediabunny(c) {
|
|
497
|
+
switch (c) {
|
|
498
|
+
case "aac":
|
|
499
|
+
return "aac";
|
|
500
|
+
case "opus":
|
|
501
|
+
return "opus";
|
|
502
|
+
case "flac":
|
|
503
|
+
return "flac";
|
|
354
504
|
}
|
|
355
505
|
}
|
|
356
|
-
function
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
506
|
+
function qualityToMediabunny(mb, quality, override) {
|
|
507
|
+
if (override !== void 0) return override;
|
|
508
|
+
switch (quality) {
|
|
509
|
+
case "low":
|
|
510
|
+
return mb.QUALITY_LOW;
|
|
511
|
+
case "medium":
|
|
512
|
+
return mb.QUALITY_MEDIUM;
|
|
513
|
+
case "high":
|
|
514
|
+
return mb.QUALITY_HIGH;
|
|
515
|
+
case "very-high":
|
|
516
|
+
return mb.QUALITY_VERY_HIGH;
|
|
366
517
|
}
|
|
367
518
|
}
|
|
368
|
-
function generateFilename(originalName, format) {
|
|
369
|
-
const ext = format === "mkv" ? "mkv" : format;
|
|
370
|
-
if (!originalName) return `output.${ext}`;
|
|
371
|
-
const base = originalName.replace(/\.[^.]+$/, "");
|
|
372
|
-
return `${base}.${ext}`;
|
|
373
|
-
}
|
|
374
|
-
var _seqCounter = 0;
|
|
375
|
-
function libavPacketToMediAbunny(mb, pkt) {
|
|
376
|
-
const KEY_FRAME_FLAG = 1;
|
|
377
|
-
const timestampSec = (pkt.pts ?? 0) / 1e6;
|
|
378
|
-
const durationSec = (pkt.duration ?? 0) / 1e6;
|
|
379
|
-
const type = pkt.flags & KEY_FRAME_FLAG ? "key" : "delta";
|
|
380
|
-
return new mb.EncodedPacket(pkt.data, type, timestampSec, durationSec, _seqCounter++);
|
|
381
|
-
}
|
|
382
|
-
function buildVideoDecoderConfig(track) {
|
|
383
|
-
return {
|
|
384
|
-
codec: track.codecString ?? track.codec,
|
|
385
|
-
codedWidth: track.width,
|
|
386
|
-
codedHeight: track.height
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
function buildAudioDecoderConfig(track) {
|
|
390
|
-
return {
|
|
391
|
-
codec: track.codecString ?? track.codec,
|
|
392
|
-
numberOfChannels: track.channels,
|
|
393
|
-
sampleRate: track.sampleRate
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
519
|
|
|
397
520
|
// src/convert/transcode.ts
|
|
398
|
-
var
|
|
521
|
+
var MEDIABUNNY_CONTAINERS = /* @__PURE__ */ new Set([
|
|
399
522
|
"mp4",
|
|
400
523
|
"mov",
|
|
401
524
|
"mkv",
|
|
@@ -415,9 +538,14 @@ async function transcode(source, options = {}) {
|
|
|
415
538
|
options.signal?.throwIfAborted();
|
|
416
539
|
const ctx = await probe(source);
|
|
417
540
|
options.signal?.throwIfAborted();
|
|
418
|
-
if (
|
|
419
|
-
|
|
420
|
-
|
|
541
|
+
if (isLibavTranscodeContainer(ctx.container)) {
|
|
542
|
+
return transcodeViaLibav(ctx, options);
|
|
543
|
+
}
|
|
544
|
+
if (!MEDIABUNNY_CONTAINERS.has(ctx.container)) {
|
|
545
|
+
throw new AvbridgeError(
|
|
546
|
+
ERR_CONTAINER_NOT_SUPPORTED,
|
|
547
|
+
`Cannot transcode "${ctx.container}" sources. transcode() supports mediabunny-readable containers (MP4, MKV, WebM, OGG, MP3, FLAC, WAV, MOV) and legacy containers via the libav path (AVI, ASF, FLV).`,
|
|
548
|
+
`If this is a legacy container we don't yet support, use createPlayer() to play it. Transcode support for more containers is on the roadmap.`
|
|
421
549
|
);
|
|
422
550
|
}
|
|
423
551
|
return doTranscode(ctx, outputFormat, videoCodec, audioCodec, quality, options);
|
|
@@ -444,7 +572,7 @@ async function attemptTranscode(ctx, outputFormat, videoCodec, audioCodec, quali
|
|
|
444
572
|
});
|
|
445
573
|
const videoOptions = options.dropVideo ? { discard: true } : {
|
|
446
574
|
codec: avbridgeVideoToMediabunny2(videoCodec),
|
|
447
|
-
bitrate: options.videoBitrate ??
|
|
575
|
+
bitrate: options.videoBitrate ?? qualityToMediabunny2(mb, quality),
|
|
448
576
|
forceTranscode: true,
|
|
449
577
|
...options.width !== void 0 ? { width: options.width } : {},
|
|
450
578
|
...options.height !== void 0 ? { height: options.height } : {},
|
|
@@ -454,7 +582,7 @@ async function attemptTranscode(ctx, outputFormat, videoCodec, audioCodec, quali
|
|
|
454
582
|
};
|
|
455
583
|
const audioOptions = options.dropAudio ? { discard: true } : {
|
|
456
584
|
codec: avbridgeAudioToMediabunny2(audioCodec),
|
|
457
|
-
bitrate: options.audioBitrate ??
|
|
585
|
+
bitrate: options.audioBitrate ?? qualityToMediabunny2(mb, quality),
|
|
458
586
|
forceTranscode: true
|
|
459
587
|
};
|
|
460
588
|
const conversion = await mb.Conversion.init({
|
|
@@ -617,7 +745,7 @@ function avbridgeAudioToMediabunny2(c) {
|
|
|
617
745
|
return "flac";
|
|
618
746
|
}
|
|
619
747
|
}
|
|
620
|
-
function
|
|
748
|
+
function qualityToMediabunny2(mb, quality) {
|
|
621
749
|
switch (quality) {
|
|
622
750
|
case "low":
|
|
623
751
|
return mb.QUALITY_LOW;
|
|
@@ -630,6 +758,6 @@ function qualityToMediabunny(mb, quality) {
|
|
|
630
758
|
}
|
|
631
759
|
}
|
|
632
760
|
|
|
633
|
-
export {
|
|
761
|
+
export { transcode };
|
|
634
762
|
//# sourceMappingURL=index.js.map
|
|
635
763
|
//# sourceMappingURL=index.js.map
|