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.
Files changed (111) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/dist/{chunk-6UUT4BEA.cjs → chunk-2IJ66NTD.cjs} +13 -20
  3. package/dist/chunk-2IJ66NTD.cjs.map +1 -0
  4. package/dist/{chunk-XKPSTC34.cjs → chunk-2XW2O3YI.cjs} +5 -20
  5. package/dist/chunk-2XW2O3YI.cjs.map +1 -0
  6. package/dist/chunk-5KVLE6YI.js +167 -0
  7. package/dist/chunk-5KVLE6YI.js.map +1 -0
  8. package/dist/{chunk-7RGG6ME7.cjs → chunk-6SOFJV44.cjs} +422 -688
  9. package/dist/chunk-6SOFJV44.cjs.map +1 -0
  10. package/dist/{chunk-2PGRFCWB.js → chunk-CPJLFFCC.js} +8 -18
  11. package/dist/chunk-CPJLFFCC.js.map +1 -0
  12. package/dist/chunk-CPZ7PXAM.cjs +240 -0
  13. package/dist/chunk-CPZ7PXAM.cjs.map +1 -0
  14. package/dist/{chunk-QQXBPW72.js → chunk-E76AMWI4.js} +4 -18
  15. package/dist/chunk-E76AMWI4.js.map +1 -0
  16. package/dist/chunk-LUFA47FP.js +19 -0
  17. package/dist/chunk-LUFA47FP.js.map +1 -0
  18. package/dist/{chunk-NV7ILLWH.js → chunk-OGYHFY6K.js} +404 -665
  19. package/dist/chunk-OGYHFY6K.js.map +1 -0
  20. package/dist/chunk-Q2VUO52Z.cjs +374 -0
  21. package/dist/chunk-Q2VUO52Z.cjs.map +1 -0
  22. package/dist/chunk-QDJLQR53.cjs +22 -0
  23. package/dist/chunk-QDJLQR53.cjs.map +1 -0
  24. package/dist/chunk-S4WAZC2T.cjs +173 -0
  25. package/dist/chunk-S4WAZC2T.cjs.map +1 -0
  26. package/dist/chunk-SMH6IOP2.js +368 -0
  27. package/dist/chunk-SMH6IOP2.js.map +1 -0
  28. package/dist/chunk-SR3MPV4D.js +237 -0
  29. package/dist/chunk-SR3MPV4D.js.map +1 -0
  30. package/dist/chunk-X2K3GIWE.js +235 -0
  31. package/dist/chunk-X2K3GIWE.js.map +1 -0
  32. package/dist/chunk-ZCUXHW55.cjs +242 -0
  33. package/dist/chunk-ZCUXHW55.cjs.map +1 -0
  34. package/dist/element-browser.js +883 -492
  35. package/dist/element-browser.js.map +1 -1
  36. package/dist/element.cjs +88 -6
  37. package/dist/element.cjs.map +1 -1
  38. package/dist/element.d.cts +51 -1
  39. package/dist/element.d.ts +51 -1
  40. package/dist/element.js +87 -5
  41. package/dist/element.js.map +1 -1
  42. package/dist/index.cjs +523 -393
  43. package/dist/index.cjs.map +1 -1
  44. package/dist/index.d.cts +2 -2
  45. package/dist/index.d.ts +2 -2
  46. package/dist/index.js +494 -366
  47. package/dist/index.js.map +1 -1
  48. package/dist/libav-demux-H2GS46GH.cjs +27 -0
  49. package/dist/libav-demux-H2GS46GH.cjs.map +1 -0
  50. package/dist/libav-demux-OWZ4T2YW.js +6 -0
  51. package/dist/libav-demux-OWZ4T2YW.js.map +1 -0
  52. package/dist/{libav-import-GST2AMPL.cjs → libav-import-2ZVKV2E7.cjs} +2 -2
  53. package/dist/{libav-import-GST2AMPL.cjs.map → libav-import-2ZVKV2E7.cjs.map} +1 -1
  54. package/dist/{libav-import-2JURFHEW.js → libav-import-6MGLCXVQ.js} +2 -2
  55. package/dist/{libav-import-2JURFHEW.js.map → libav-import-6MGLCXVQ.js.map} +1 -1
  56. package/dist/{player-B6WB74RD.d.ts → player-DGXeCNfD.d.cts} +41 -1
  57. package/dist/{player-B6WB74RD.d.cts → player-DGXeCNfD.d.ts} +41 -1
  58. package/dist/player.cjs +731 -472
  59. package/dist/player.cjs.map +1 -1
  60. package/dist/player.d.cts +229 -120
  61. package/dist/player.d.ts +229 -120
  62. package/dist/player.js +710 -451
  63. package/dist/player.js.map +1 -1
  64. package/dist/remux-OBSMIENG.cjs +35 -0
  65. package/dist/remux-OBSMIENG.cjs.map +1 -0
  66. package/dist/remux-WBYIZBBX.js +10 -0
  67. package/dist/remux-WBYIZBBX.js.map +1 -0
  68. package/dist/source-4TZ6KMNV.js +4 -0
  69. package/dist/{source-F656KYYV.js.map → source-4TZ6KMNV.js.map} +1 -1
  70. package/dist/source-7YLO6E7X.cjs +29 -0
  71. package/dist/{source-73CAH6HW.cjs.map → source-7YLO6E7X.cjs.map} +1 -1
  72. package/dist/source-MTX5ELUZ.js +4 -0
  73. package/dist/{source-QJR3OHTW.js.map → source-MTX5ELUZ.js.map} +1 -1
  74. package/dist/source-VFLXLOCN.cjs +29 -0
  75. package/dist/{source-VB74JQ7Z.cjs.map → source-VFLXLOCN.cjs.map} +1 -1
  76. package/dist/subtitles-4T74JRGT.js +4 -0
  77. package/dist/subtitles-4T74JRGT.js.map +1 -0
  78. package/dist/subtitles-QUH4LPI4.cjs +29 -0
  79. package/dist/subtitles-QUH4LPI4.cjs.map +1 -0
  80. package/package.json +1 -1
  81. package/src/convert/remux.ts +1 -35
  82. package/src/convert/transcode-libav.ts +691 -0
  83. package/src/convert/transcode.ts +12 -4
  84. package/src/element/avbridge-player.ts +100 -0
  85. package/src/element/avbridge-video.ts +140 -3
  86. package/src/element/player-styles.ts +12 -0
  87. package/src/errors.ts +6 -0
  88. package/src/player.ts +15 -16
  89. package/src/strategies/fallback/decoder.ts +96 -173
  90. package/src/strategies/fallback/index.ts +46 -2
  91. package/src/strategies/fallback/libav-import.ts +9 -1
  92. package/src/strategies/fallback/video-renderer.ts +107 -0
  93. package/src/strategies/hybrid/decoder.ts +88 -180
  94. package/src/strategies/hybrid/index.ts +35 -2
  95. package/src/strategies/native.ts +6 -3
  96. package/src/strategies/remux/index.ts +14 -2
  97. package/src/strategies/remux/pipeline.ts +72 -12
  98. package/src/subtitles/render.ts +8 -0
  99. package/src/types.ts +32 -0
  100. package/src/util/libav-demux.ts +405 -0
  101. package/src/util/time-ranges.ts +40 -0
  102. package/dist/chunk-2PGRFCWB.js.map +0 -1
  103. package/dist/chunk-6UUT4BEA.cjs.map +0 -1
  104. package/dist/chunk-7RGG6ME7.cjs.map +0 -1
  105. package/dist/chunk-NV7ILLWH.js.map +0 -1
  106. package/dist/chunk-QQXBPW72.js.map +0 -1
  107. package/dist/chunk-XKPSTC34.cjs.map +0 -1
  108. package/dist/source-73CAH6HW.cjs +0 -28
  109. package/dist/source-F656KYYV.js +0 -3
  110. package/dist/source-QJR3OHTW.js +0 -3
  111. package/dist/source-VB74JQ7Z.cjs +0 -28
package/dist/index.js CHANGED
@@ -1,401 +1,524 @@
1
- import { probe, avbridgeVideoToMediabunny, avbridgeAudioToMediabunny, buildMediabunnySourceFromInput } from './chunk-NV7ILLWH.js';
2
- export { FALLBACK_AUDIO_CODECS, FALLBACK_VIDEO_CODECS, NATIVE_AUDIO_CODECS, NATIVE_VIDEO_CODECS, UnifiedPlayer, classifyContext as classify, createPlayer, probe, srtToVtt } from './chunk-NV7ILLWH.js';
3
- import { normalizeSource } from './chunk-2PGRFCWB.js';
4
- 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-2PGRFCWB.js';
5
- import { prepareLibavInput } from './chunk-DCSOQH2N.js';
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
- function isAnnexB(bytes) {
10
- if (bytes.length < 3) return false;
11
- if (bytes[0] === 0 && bytes[1] === 0 && bytes[2] === 1) return true;
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* iterateAnnexBNalus(bytes) {
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
- options.signal?.throwIfAborted();
76
- const ctx = await probe(source);
77
- options.signal?.throwIfAborted();
78
- validateRemuxEligibility(ctx, options.strict ?? false);
79
- if (MEDIABUNNY_CONTAINERS.has(ctx.container)) {
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
- if (!video && !audio) {
109
- throw new Error("Cannot remux: source has no video or audio tracks.");
110
- }
111
- }
112
- async function remuxViaMediAbunny(ctx, outputFormat, options) {
113
- const mb = await import('mediabunny');
114
- const input = new mb.Input({
115
- source: await buildMediabunnySourceFromInput(mb, ctx.source),
116
- formats: mb.ALL_FORMATS
117
- });
118
- const target = new mb.BufferTarget();
119
- const output = new mb.Output({
120
- format: createOutputFormat(mb, outputFormat),
121
- target
122
- });
123
- const conversion = await mb.Conversion.init({
124
- input,
125
- output,
126
- showWarnings: false
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
- await conversion.execute();
146
- } finally {
147
- if (abortHandler && options.signal) {
148
- options.signal.removeEventListener("abort", abortHandler);
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
- if (!target.buffer) {
152
- throw new Error("Remux failed: mediabunny produced no output buffer.");
153
- }
154
- const mimeType = mimeForFormat(outputFormat);
155
- const blob = new Blob([target.buffer], { type: mimeType });
156
- const filename = generateFilename(ctx.name, outputFormat);
157
- options.onProgress?.({ percent: 100, bytesWritten: blob.size });
158
- return {
159
- blob,
160
- mimeType,
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
- async function doLibavRemux(libav, filename, ctx, outputFormat, options) {
194
- const mb = await import('mediabunny');
195
- const readPkt = await libav.av_packet_alloc();
196
- const [fmt_ctx, streams] = await libav.ff_init_demuxer_file(filename);
197
- const videoStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_VIDEO) ?? null;
198
- const audioStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO) ?? null;
199
- const videoTrackInfo = ctx.videoTracks[0];
200
- const audioTrackInfo = ctx.audioTracks[0];
201
- const mbVideoCodec = videoTrackInfo ? avbridgeVideoToMediabunny(videoTrackInfo.codec) : null;
202
- const mbAudioCodec = audioTrackInfo ? avbridgeAudioToMediabunny(audioTrackInfo.codec) : null;
203
- const target = new mb.BufferTarget();
204
- const output = new mb.Output({
205
- format: createOutputFormat(mb, outputFormat),
206
- target
207
- });
208
- let videoSource = null;
209
- let audioSource = null;
210
- if (mbVideoCodec && videoStream) {
211
- videoSource = new mb.EncodedVideoPacketSource(mbVideoCodec);
212
- output.addVideoTrack(videoSource);
213
- }
214
- if (mbAudioCodec && audioStream) {
215
- audioSource = new mb.EncodedAudioPacketSource(mbAudioCodec);
216
- output.addAudioTrack(audioSource);
217
- }
218
- await output.start();
219
- const videoFps = videoTrackInfo?.fps && videoTrackInfo.fps > 0 ? videoTrackInfo.fps : 30;
220
- const videoFrameStepUs = Math.max(1, Math.round(1e6 / videoFps));
221
- let syntheticVideoUs = 0;
222
- let syntheticAudioUs = 0;
223
- const videoTimeBase = videoStream?.time_base_num && videoStream?.time_base_den ? [videoStream.time_base_num, videoStream.time_base_den] : void 0;
224
- const audioTimeBase = audioStream?.time_base_num && audioStream?.time_base_den ? [audioStream.time_base_num, audioStream.time_base_den] : void 0;
225
- let totalPackets = 0;
226
- const durationUs = ctx.duration ? ctx.duration * 1e6 : 0;
227
- let firstVideoMeta = true;
228
- let firstAudioMeta = true;
229
- while (true) {
230
- options.signal?.throwIfAborted();
231
- let readErr;
232
- let packets;
233
- try {
234
- [readErr, packets] = await libav.ff_read_frame_multi(fmt_ctx, readPkt, {
235
- limit: 64 * 1024
236
- });
237
- } catch (err) {
238
- throw new Error(`libav demux failed: ${err.message}`);
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
- const videoPackets = videoStream ? packets[videoStream.index] ?? [] : [];
241
- const audioPackets = audioStream ? packets[audioStream.index] ?? [] : [];
242
- if (videoSource) {
243
- for (const pkt of videoPackets) {
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
- if (videoTrackInfo && (videoTrackInfo.codec === "h264" || videoTrackInfo.codec === "h265") && isAnnexB(pkt.data)) {
250
- pkt.data = annexBToAvcc(pkt.data);
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
- const mbPacket = libavPacketToMediAbunny(mb, pkt);
253
- await videoSource.add(
254
- mbPacket,
255
- firstVideoMeta ? { decoderConfig: buildVideoDecoderConfig(videoTrackInfo) } : void 0
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
- firstVideoMeta = false;
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
- if (audioSource) {
261
- for (const pkt of audioPackets) {
262
- sanitizePacketTimestamp(pkt, () => {
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 sampleRate = audioTrackInfo?.sampleRate ?? 44100;
265
- syntheticAudioUs += Math.round(1024 * 1e6 / sampleRate);
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 mbPacket = libavPacketToMediAbunny(mb, pkt);
269
- await audioSource.add(
270
- mbPacket,
271
- firstAudioMeta ? { decoderConfig: buildAudioDecoderConfig(audioTrackInfo) } : void 0
272
- );
273
- firstAudioMeta = false;
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
- totalPackets += videoPackets.length + audioPackets.length;
277
- if (options.onProgress && durationUs > 0) {
278
- const lastVideoTs = videoPackets.length > 0 ? videoPackets[videoPackets.length - 1].pts ?? 0 : 0;
279
- const lastAudioTs = audioPackets.length > 0 ? audioPackets[audioPackets.length - 1].pts ?? 0 : 0;
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
- await output.finalize();
291
- try {
292
- await libav.av_packet_free?.(readPkt);
293
- } catch {
294
- }
475
+ }
476
+ async function loadBridge() {
295
477
  try {
296
- await libav.avformat_close_input_js(fmt_ctx);
297
- } catch {
298
- }
299
- if (!target.buffer) {
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 sanitizePacketTimestamp(pkt, nextUs, fallbackTimeBase) {
317
- const lo = pkt.pts ?? 0;
318
- const hi = pkt.ptshi ?? 0;
319
- const isInvalid = hi === -2147483648 && lo === 0 || !Number.isFinite(lo);
320
- if (isInvalid) {
321
- const us2 = nextUs();
322
- pkt.pts = us2;
323
- pkt.ptshi = 0;
324
- pkt.time_base_num = 1;
325
- pkt.time_base_den = 1e6;
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 createOutputFormat(mb, format) {
345
- switch (format) {
346
- case "mp4":
347
- return new mb.Mp4OutputFormat({ fastStart: "in-memory" });
348
- case "webm":
349
- return new mb.WebMOutputFormat();
350
- case "mkv":
351
- return new mb.MkvOutputFormat();
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 mimeForFormat(format) {
357
- switch (format) {
358
- case "mp4":
359
- return "video/mp4";
360
- case "webm":
361
- return "video/webm";
362
- case "mkv":
363
- return "video/x-matroska";
364
- default:
365
- return "application/octet-stream";
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 MEDIABUNNY_CONTAINERS2 = /* @__PURE__ */ new Set([
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 (!MEDIABUNNY_CONTAINERS2.has(ctx.container)) {
419
- throw new Error(
420
- `Cannot transcode "${ctx.container}" sources in v1. transcode() only supports inputs that mediabunny can read (MP4, MKV, WebM, OGG, MP3, FLAC, WAV, MOV). For AVI/ASF/FLV sources, use the player's playback strategies instead.`
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 ?? qualityToMediabunny(mb, quality),
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 ?? qualityToMediabunny(mb, quality),
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 qualityToMediabunny(mb, quality) {
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 { remux, transcode };
761
+ export { transcode };
634
762
  //# sourceMappingURL=index.js.map
635
763
  //# sourceMappingURL=index.js.map