avbridge 1.0.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 (103) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/LICENSE +21 -0
  3. package/README.md +415 -0
  4. package/dist/avi-M5B4SHRM.cjs +164 -0
  5. package/dist/avi-M5B4SHRM.cjs.map +1 -0
  6. package/dist/avi-POCGZ4JX.js +162 -0
  7. package/dist/avi-POCGZ4JX.js.map +1 -0
  8. package/dist/chunk-5ISVAODK.js +80 -0
  9. package/dist/chunk-5ISVAODK.js.map +1 -0
  10. package/dist/chunk-F7YS2XOA.cjs +2966 -0
  11. package/dist/chunk-F7YS2XOA.cjs.map +1 -0
  12. package/dist/chunk-FKM7QBZU.js +2957 -0
  13. package/dist/chunk-FKM7QBZU.js.map +1 -0
  14. package/dist/chunk-J5MCMN3S.js +27 -0
  15. package/dist/chunk-J5MCMN3S.js.map +1 -0
  16. package/dist/chunk-L4NPOJ36.cjs +180 -0
  17. package/dist/chunk-L4NPOJ36.cjs.map +1 -0
  18. package/dist/chunk-NZU7W256.cjs +29 -0
  19. package/dist/chunk-NZU7W256.cjs.map +1 -0
  20. package/dist/chunk-PQTZS7OA.js +147 -0
  21. package/dist/chunk-PQTZS7OA.js.map +1 -0
  22. package/dist/chunk-WD2ZNQA7.js +177 -0
  23. package/dist/chunk-WD2ZNQA7.js.map +1 -0
  24. package/dist/chunk-Y5FYF5KG.cjs +153 -0
  25. package/dist/chunk-Y5FYF5KG.cjs.map +1 -0
  26. package/dist/chunk-Z2FJ5TJC.cjs +82 -0
  27. package/dist/chunk-Z2FJ5TJC.cjs.map +1 -0
  28. package/dist/element.cjs +433 -0
  29. package/dist/element.cjs.map +1 -0
  30. package/dist/element.d.cts +158 -0
  31. package/dist/element.d.ts +158 -0
  32. package/dist/element.js +431 -0
  33. package/dist/element.js.map +1 -0
  34. package/dist/index.cjs +576 -0
  35. package/dist/index.cjs.map +1 -0
  36. package/dist/index.d.cts +80 -0
  37. package/dist/index.d.ts +80 -0
  38. package/dist/index.js +554 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/libav-http-reader-FPYDBMYK.cjs +16 -0
  41. package/dist/libav-http-reader-FPYDBMYK.cjs.map +1 -0
  42. package/dist/libav-http-reader-NQJVY273.js +3 -0
  43. package/dist/libav-http-reader-NQJVY273.js.map +1 -0
  44. package/dist/libav-import-2JURFHEW.js +8 -0
  45. package/dist/libav-import-2JURFHEW.js.map +1 -0
  46. package/dist/libav-import-GST2AMPL.cjs +30 -0
  47. package/dist/libav-import-GST2AMPL.cjs.map +1 -0
  48. package/dist/libav-loader-KA2MAWLM.js +3 -0
  49. package/dist/libav-loader-KA2MAWLM.js.map +1 -0
  50. package/dist/libav-loader-ZHOERPHW.cjs +12 -0
  51. package/dist/libav-loader-ZHOERPHW.cjs.map +1 -0
  52. package/dist/player-BBwbCkdL.d.cts +365 -0
  53. package/dist/player-BBwbCkdL.d.ts +365 -0
  54. package/dist/source-SC6ZEQYR.cjs +28 -0
  55. package/dist/source-SC6ZEQYR.cjs.map +1 -0
  56. package/dist/source-ZFS4H7J3.js +3 -0
  57. package/dist/source-ZFS4H7J3.js.map +1 -0
  58. package/dist/variant-routing-GOHB2RZN.cjs +12 -0
  59. package/dist/variant-routing-GOHB2RZN.cjs.map +1 -0
  60. package/dist/variant-routing-JOBWXYKD.js +3 -0
  61. package/dist/variant-routing-JOBWXYKD.js.map +1 -0
  62. package/package.json +95 -0
  63. package/src/classify/index.ts +1 -0
  64. package/src/classify/rules.ts +214 -0
  65. package/src/convert/index.ts +2 -0
  66. package/src/convert/remux.ts +522 -0
  67. package/src/convert/transcode.ts +329 -0
  68. package/src/diagnostics.ts +99 -0
  69. package/src/element/avbridge-player.ts +576 -0
  70. package/src/element.ts +19 -0
  71. package/src/events.ts +71 -0
  72. package/src/index.ts +42 -0
  73. package/src/libav-stubs.d.ts +24 -0
  74. package/src/player.ts +455 -0
  75. package/src/plugins/builtin.ts +37 -0
  76. package/src/plugins/registry.ts +32 -0
  77. package/src/probe/avi.ts +242 -0
  78. package/src/probe/index.ts +59 -0
  79. package/src/probe/mediabunny.ts +194 -0
  80. package/src/strategies/fallback/audio-output.ts +293 -0
  81. package/src/strategies/fallback/clock.ts +7 -0
  82. package/src/strategies/fallback/decoder.ts +660 -0
  83. package/src/strategies/fallback/index.ts +170 -0
  84. package/src/strategies/fallback/libav-import.ts +27 -0
  85. package/src/strategies/fallback/libav-loader.ts +190 -0
  86. package/src/strategies/fallback/variant-routing.ts +43 -0
  87. package/src/strategies/fallback/video-renderer.ts +216 -0
  88. package/src/strategies/hybrid/decoder.ts +641 -0
  89. package/src/strategies/hybrid/index.ts +139 -0
  90. package/src/strategies/native.ts +107 -0
  91. package/src/strategies/remux/annexb.ts +112 -0
  92. package/src/strategies/remux/index.ts +79 -0
  93. package/src/strategies/remux/mse.ts +234 -0
  94. package/src/strategies/remux/pipeline.ts +254 -0
  95. package/src/subtitles/index.ts +91 -0
  96. package/src/subtitles/render.ts +62 -0
  97. package/src/subtitles/srt.ts +62 -0
  98. package/src/subtitles/vtt.ts +5 -0
  99. package/src/types-shim.d.ts +3 -0
  100. package/src/types.ts +360 -0
  101. package/src/util/codec-strings.ts +86 -0
  102. package/src/util/libav-http-reader.ts +315 -0
  103. package/src/util/source.ts +274 -0
@@ -0,0 +1,242 @@
1
+ import type {
2
+ AudioCodec,
3
+ AudioTrackInfo,
4
+ ContainerKind,
5
+ MediaContext,
6
+ VideoCodec,
7
+ VideoTrackInfo,
8
+ } from "../types.js";
9
+ import type { NormalizedSource } from "../util/source.js";
10
+ import { prepareLibavInput, type LibavInputHandle } from "../util/libav-http-reader.js";
11
+ import { loadLibav } from "../strategies/fallback/libav-loader.js";
12
+
13
+ /**
14
+ * Probe AVI/ASF/FLV (and any other format mediabunny doesn't speak) via
15
+ * libav.js. This module is `import()`-ed only when sniffing identifies one of
16
+ * those containers.
17
+ *
18
+ * Critical: codec identification goes through `libav.avcodec_get_name(id)`
19
+ * which returns the FFmpeg codec name as a string (e.g. "h264", "mpeg4",
20
+ * "wmv3"). The numeric AV_CODEC_ID_* enum is *not* exposed on the libav
21
+ * instance (only AVMEDIA_TYPE_*, AV_PIX_FMT_*, AV_SAMPLE_FMT_* and a handful
22
+ * of others are), so comparing codec_ids against constants does not work.
23
+ */
24
+ export async function probeWithLibav(
25
+ source: NormalizedSource,
26
+ sniffed: ContainerKind,
27
+ ): Promise<MediaContext> {
28
+ // AVI/ASF/FLV demuxers are not in any libav.js npm variant — they live in
29
+ // the custom "avbridge" build produced by `scripts/build-libav.sh`. The loader
30
+ // emits an actionable error if the build hasn't been run yet. Threading
31
+ // is OFF by default in `loadLibav` (see the comment there for why).
32
+ const libav = (await loadLibav("avbridge")) as unknown as LibavInstance;
33
+
34
+ const filename = source.name ?? `input.${sniffed === "unknown" ? "bin" : sniffed}`;
35
+ // For Blob/File sources we use libav's in-memory readahead file. For URL
36
+ // sources we attach an HTTP block reader so libav demuxes via Range
37
+ // requests instead of buffering the whole file.
38
+ const handle: LibavInputHandle = await prepareLibavInput(libav as unknown as Parameters<typeof prepareLibavInput>[0], filename, source);
39
+
40
+ let fmt_ctx: number | undefined;
41
+ let streams: LibavStream[] = [];
42
+ try {
43
+ const result = await libav.ff_init_demuxer_file(filename);
44
+ fmt_ctx = result[0];
45
+ streams = result[1];
46
+ } catch (err) {
47
+ await handle.detach().catch(() => {});
48
+ // Errors thrown across the libav.js worker/pthread boundary aren't
49
+ // always Error instances — they can be plain objects, numbers (errno
50
+ // codes), or strings. Stringify defensively so the user-facing message
51
+ // never has `(undefined)` in it.
52
+ const inner =
53
+ err instanceof Error
54
+ ? err.message
55
+ : typeof err === "object" && err !== null
56
+ ? JSON.stringify(err)
57
+ : String(err);
58
+ // eslint-disable-next-line no-console
59
+ console.error("[avbridge] ff_init_demuxer_file raw error:", err);
60
+ throw new Error(
61
+ `libav.js could not demux ${filename}. The current libav variant likely lacks the required demuxer (e.g. AVI). See vendor/libav/README.md for build instructions. (${inner || "no message — see console for raw error"})`,
62
+ );
63
+ }
64
+
65
+ const videoTracks: VideoTrackInfo[] = [];
66
+ const audioTracks: AudioTrackInfo[] = [];
67
+
68
+ for (const stream of streams) {
69
+ const codecName = (await safe(() => libav.avcodec_get_name(stream.codec_id))) ?? `unknown(${stream.codec_id})`;
70
+ // codecpar holds width/height/channels/sample_rate/profile/level/extradata
71
+ // for the actual stream. We have to copy it out of WASM memory.
72
+ const codecpar = await safe(() => libav.ff_copyout_codecpar(stream.codecpar));
73
+
74
+ if (stream.codec_type === libav.AVMEDIA_TYPE_VIDEO) {
75
+ videoTracks.push({
76
+ id: stream.index,
77
+ codec: ffmpegToAvbridgeVideo(codecName),
78
+ width: codecpar?.width ?? 0,
79
+ height: codecpar?.height ?? 0,
80
+ fps: framerate(stream),
81
+ });
82
+ } else if (stream.codec_type === libav.AVMEDIA_TYPE_AUDIO) {
83
+ audioTracks.push({
84
+ id: stream.index,
85
+ codec: ffmpegToAvbridgeAudio(codecName),
86
+ channels: codecpar?.channels ?? codecpar?.ch_layout_nb_channels ?? 0,
87
+ sampleRate: codecpar?.sample_rate ?? 0,
88
+ });
89
+ }
90
+ }
91
+
92
+ // We need this duration but cannot reliably get it from the streams alone
93
+ // for AVI; libav.js exposes it via the AVFormatContext duration helper.
94
+ const duration = await safeDuration(libav, fmt_ctx!);
95
+
96
+ // Close the demuxer; the strategy will reopen it later if it ends up being
97
+ // chosen. Probing should not pin native resources.
98
+ await libav.avformat_close_input_js(fmt_ctx!).catch(() => {});
99
+ await handle.detach().catch(() => {});
100
+
101
+ return {
102
+ source: source.original,
103
+ name: source.name,
104
+ byteLength: source.byteLength,
105
+ container: sniffed === "unknown" ? "unknown" : sniffed,
106
+ videoTracks,
107
+ audioTracks,
108
+ subtitleTracks: [],
109
+ probedBy: "libav",
110
+ duration,
111
+ };
112
+ }
113
+
114
+ function framerate(stream: LibavStream): number | undefined {
115
+ if (typeof stream.avg_frame_rate_num === "number" && stream.avg_frame_rate_den) {
116
+ return stream.avg_frame_rate_num / stream.avg_frame_rate_den;
117
+ }
118
+ if (stream.avg_frame_rate && typeof stream.avg_frame_rate === "object") {
119
+ if (stream.avg_frame_rate.den === 0) return undefined;
120
+ return stream.avg_frame_rate.num / stream.avg_frame_rate.den;
121
+ }
122
+ return undefined;
123
+ }
124
+
125
+ async function safeDuration(libav: LibavInstance, fmt_ctx: number): Promise<number | undefined> {
126
+ try {
127
+ // `AVFormatContext.duration` is an int64 in microseconds (AV_TIME_BASE).
128
+ // libav.js exposes it as a split lo/hi pair the same way it does for
129
+ // packet pts — `AVFormatContext_duration(ctx)` returns the low 32 bits,
130
+ // `AVFormatContext_durationhi(ctx)` returns the high 32 bits. Reading
131
+ // only the low half (the previous bug) gave garbage for any file whose
132
+ // duration > ~35 minutes, and zero for shorter files where the value
133
+ // happened to live in the high half.
134
+ const lo = await libav.AVFormatContext_duration?.(fmt_ctx);
135
+ const hi = await libav.AVFormatContext_durationhi?.(fmt_ctx);
136
+ if (typeof lo !== "number" || typeof hi !== "number") return undefined;
137
+
138
+ // AV_NOPTS_VALUE = -2^63 → ptshi = -2147483648, pts = 0. Means "unknown".
139
+ if (hi === -2147483648 && lo === 0) return undefined;
140
+
141
+ // Reconstruct the 64-bit value. Prefer libav's helper when available
142
+ // because it correctly handles signed 32-bit two's complement.
143
+ const us =
144
+ typeof libav.i64tof64 === "function"
145
+ ? libav.i64tof64(lo, hi)
146
+ : hi * 0x100000000 + lo + (lo < 0 ? 0x100000000 : 0);
147
+
148
+ if (!Number.isFinite(us) || us <= 0) return undefined;
149
+ return us / 1_000_000;
150
+ } catch {
151
+ return undefined;
152
+ }
153
+ }
154
+
155
+ async function safe<T>(fn: () => Promise<T> | T): Promise<T | undefined> {
156
+ try { return await fn(); } catch { return undefined; }
157
+ }
158
+
159
+ /** Map FFmpeg codec names to avbridge video codec identifiers. */
160
+ function ffmpegToAvbridgeVideo(name: string): VideoCodec {
161
+ switch (name) {
162
+ case "h264": return "h264";
163
+ case "hevc": return "h265";
164
+ case "vp8": return "vp8";
165
+ case "vp9": return "vp9";
166
+ case "av1": return "av1";
167
+ case "mpeg4": return "mpeg4"; // MPEG-4 Part 2 / DivX / Xvid
168
+ case "msmpeg4v1":
169
+ case "msmpeg4v2":
170
+ case "msmpeg4v3": // a.k.a. DIV3
171
+ return "mpeg4";
172
+ case "wmv1":
173
+ case "wmv2":
174
+ case "wmv3":
175
+ return "wmv3";
176
+ case "vc1": return "vc1";
177
+ case "mpeg2video": return "mpeg2";
178
+ case "mpeg1video": return "mpeg1";
179
+ case "theora": return "theora";
180
+ case "rv30":
181
+ case "rv40": return "rv40";
182
+ default: return name as VideoCodec;
183
+ }
184
+ }
185
+
186
+ function ffmpegToAvbridgeAudio(name: string): AudioCodec {
187
+ switch (name) {
188
+ case "aac": return "aac";
189
+ case "mp3":
190
+ case "mp3float":
191
+ return "mp3";
192
+ case "opus": return "opus";
193
+ case "vorbis": return "vorbis";
194
+ case "flac": return "flac";
195
+ case "ac3": return "ac3";
196
+ case "eac3": return "eac3";
197
+ case "wmav1":
198
+ case "wmav2": return "wmav2";
199
+ case "wmapro": return "wmapro";
200
+ case "alac": return "alac";
201
+ default: return name as AudioCodec;
202
+ }
203
+ }
204
+
205
+ // ─────────────────────────────────────────────────────────────────────────────
206
+ // Minimal structural types for the slice of libav.js we touch.
207
+ // ─────────────────────────────────────────────────────────────────────────────
208
+
209
+ interface LibavStream {
210
+ index: number;
211
+ codec_type: number;
212
+ codec_id: number;
213
+ codecpar: number;
214
+ avg_frame_rate?: { num: number; den: number };
215
+ avg_frame_rate_num?: number;
216
+ avg_frame_rate_den?: number;
217
+ }
218
+
219
+ interface LibavCodecpar {
220
+ width?: number;
221
+ height?: number;
222
+ channels?: number;
223
+ ch_layout_nb_channels?: number;
224
+ sample_rate?: number;
225
+ profile?: number;
226
+ level?: number;
227
+ }
228
+
229
+ interface LibavInstance {
230
+ mkreadaheadfile(name: string, blob: Blob): Promise<void>;
231
+ unlinkreadaheadfile(name: string): Promise<void>;
232
+ ff_init_demuxer_file(name: string): Promise<[number, LibavStream[]]>;
233
+ ff_copyout_codecpar(codecpar: number): Promise<LibavCodecpar>;
234
+ avcodec_get_name(codec_id: number): Promise<string>;
235
+ avformat_close_input_js(ctx: number): Promise<void>;
236
+ AVFormatContext_duration?(ctx: number): Promise<number>;
237
+ AVFormatContext_durationhi?(ctx: number): Promise<number>;
238
+ i64tof64?(lo: number, hi: number): number;
239
+
240
+ AVMEDIA_TYPE_VIDEO: number;
241
+ AVMEDIA_TYPE_AUDIO: number;
242
+ }
@@ -0,0 +1,59 @@
1
+ import type { ContainerKind, MediaContext, MediaInput } from "../types.js";
2
+ import { normalizeSource, sniffNormalizedSource } from "../util/source.js";
3
+ import { probeWithMediabunny } from "./mediabunny.js";
4
+
5
+ /** Containers mediabunny can demux. Sniff results outside this set go straight to libav. */
6
+ const MEDIABUNNY_CONTAINERS = new Set<ContainerKind>([
7
+ "mp4",
8
+ "mov",
9
+ "mkv",
10
+ "webm",
11
+ "ogg",
12
+ "wav",
13
+ "mp3",
14
+ "flac",
15
+ "adts",
16
+ "mpegts",
17
+ ]);
18
+
19
+ /**
20
+ * Probe a source and produce a {@link MediaContext}.
21
+ *
22
+ * Routing:
23
+ * 1. Sniff the magic header. Cheap, deterministic, ignores file extensions.
24
+ * 2. If the container is one mediabunny supports → mediabunny. If mediabunny
25
+ * rejects, surface the real error rather than blindly falling through to
26
+ * libav (which would mask the real failure with a confusing libav error).
27
+ * 3. If sniffing identifies AVI/ASF/FLV (or `unknown`) → libav.js, lazy-loaded.
28
+ * `unknown` is included so genuinely unfamiliar files at least get a shot
29
+ * at the broader libav demuxer set.
30
+ */
31
+ export async function probe(source: MediaInput): Promise<MediaContext> {
32
+ const normalized = await normalizeSource(source);
33
+ const sniffed = await sniffNormalizedSource(normalized);
34
+
35
+ if (MEDIABUNNY_CONTAINERS.has(sniffed)) {
36
+ try {
37
+ return await probeWithMediabunny(normalized, sniffed);
38
+ } catch (err) {
39
+ throw new Error(
40
+ `mediabunny failed to probe a ${sniffed} file: ${(err as Error).message}`,
41
+ );
42
+ }
43
+ }
44
+
45
+ // sniffed === avi | asf | flv | unknown — try libav.
46
+ try {
47
+ const { probeWithLibav } = await import("./avi.js");
48
+ return await probeWithLibav(normalized, sniffed);
49
+ } catch (err) {
50
+ const inner = err instanceof Error ? err.message : String(err);
51
+ // eslint-disable-next-line no-console
52
+ console.error("[avbridge] libav probe failed for", sniffed, "file:", err);
53
+ throw new Error(
54
+ sniffed === "unknown"
55
+ ? `unable to probe source: container could not be identified, and the libav.js fallback also failed: ${inner || "(no message — see browser console for the original error)"}`
56
+ : `${sniffed.toUpperCase()} files require libav.js, which failed to load: ${inner || "(no message — see browser console for the original error)"}`,
57
+ );
58
+ }
59
+ }
@@ -0,0 +1,194 @@
1
+ import type {
2
+ AudioCodec,
3
+ AudioTrackInfo,
4
+ ContainerKind,
5
+ MediaContext,
6
+ VideoCodec,
7
+ VideoTrackInfo,
8
+ } from "../types.js";
9
+ import type { NormalizedSource } from "../util/source.js";
10
+
11
+ /**
12
+ * Probe via mediabunny. Built against the real (typed) API exported by
13
+ * `mediabunny.d.ts`:
14
+ *
15
+ * - `Input.getTracks()` returns `InputTrack[]`; each track has `isVideoTrack()`
16
+ * / `isAudioTrack()` type guards plus a `codec` getter that returns one of
17
+ * the enum strings (`"avc"|"hevc"|"vp9"|"vp8"|"av1"` for video,
18
+ * `"aac"|"opus"|...` for audio).
19
+ * - For decoder metadata + codec parameter strings we call
20
+ * `getDecoderConfig()` and `getCodecParameterString()` on the typed track.
21
+ *
22
+ * The bridging back to avbridge's own codec naming (`h264` instead of mediabunny's
23
+ * `avc`) happens here so the rest of the codebase keeps a single vocabulary.
24
+ */
25
+ export async function probeWithMediabunny(
26
+ source: NormalizedSource,
27
+ sniffedContainer: ContainerKind,
28
+ ): Promise<MediaContext> {
29
+ const mb = await import("mediabunny");
30
+ const input = new mb.Input({
31
+ source: await buildMediabunnySource(mb, source),
32
+ formats: mb.ALL_FORMATS,
33
+ });
34
+
35
+ const allTracks = await input.getTracks();
36
+ const duration = await safeNumber(() => input.computeDuration());
37
+
38
+ const videoTracks: VideoTrackInfo[] = [];
39
+ const audioTracks: AudioTrackInfo[] = [];
40
+
41
+ for (const track of allTracks) {
42
+ if (track.isVideoTrack()) {
43
+ const codecParam = await safe(() => track.getCodecParameterString());
44
+ videoTracks.push({
45
+ id: track.id,
46
+ codec: mediabunnyVideoToAvbridge(track.codec),
47
+ width: track.displayWidth ?? track.codedWidth ?? 0,
48
+ height: track.displayHeight ?? track.codedHeight ?? 0,
49
+ codecString: codecParam ?? undefined,
50
+ });
51
+ } else if (track.isAudioTrack()) {
52
+ const codecParam = await safe(() => track.getCodecParameterString());
53
+ audioTracks.push({
54
+ id: track.id,
55
+ codec: mediabunnyAudioToAvbridge(track.codec),
56
+ channels: track.numberOfChannels ?? 0,
57
+ sampleRate: track.sampleRate ?? 0,
58
+ language: track.languageCode,
59
+ codecString: codecParam ?? undefined,
60
+ });
61
+ }
62
+ }
63
+
64
+ const format = await safe(() => input.getFormat());
65
+ const container = resolveContainer(format?.name, sniffedContainer);
66
+
67
+ return {
68
+ source: source.original,
69
+ name: source.name,
70
+ byteLength: source.byteLength,
71
+ container,
72
+ videoTracks,
73
+ audioTracks,
74
+ subtitleTracks: [],
75
+ probedBy: "mediabunny",
76
+ duration,
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Build the right mediabunny `Source` for a normalized input. URL sources
82
+ * use `UrlSource` (Range requests, prefetch, parallelism) so we don't
83
+ * buffer the whole file into memory. Blob/File sources use `BlobSource`.
84
+ *
85
+ * Exported so the remux strategy can use the same routing logic.
86
+ */
87
+ export async function buildMediabunnySource(
88
+ mb: typeof import("mediabunny"),
89
+ source: NormalizedSource,
90
+ ): Promise<InstanceType<typeof mb.BlobSource> | InstanceType<typeof mb.UrlSource>> {
91
+ if (source.kind === "url") {
92
+ return new mb.UrlSource(source.url);
93
+ }
94
+ return new mb.BlobSource(source.blob);
95
+ }
96
+
97
+ /**
98
+ * Build a mediabunny `Source` directly from a raw `MediaInput`, bypassing
99
+ * `normalizeSource`. Used by strategies that already have the original
100
+ * input on hand (via `MediaContext.source`) and don't need a sniff window.
101
+ *
102
+ * This is the routing point that decides "stream from URL via Range
103
+ * requests" vs "wrap in-memory bytes as BlobSource". Always prefer
104
+ * `UrlSource` for URL inputs so we don't accidentally buffer the file.
105
+ */
106
+ export async function buildMediabunnySourceFromInput(
107
+ mb: typeof import("mediabunny"),
108
+ source: import("../types.js").MediaInput,
109
+ ): Promise<InstanceType<typeof mb.BlobSource> | InstanceType<typeof mb.UrlSource>> {
110
+ if (typeof source === "string") return new mb.UrlSource(source);
111
+ if (source instanceof URL) return new mb.UrlSource(source.toString());
112
+ if (source instanceof Blob) return new mb.BlobSource(source);
113
+ if (source instanceof ArrayBuffer) return new mb.BlobSource(new Blob([source]));
114
+ if (source instanceof Uint8Array) return new mb.BlobSource(new Blob([source as BlobPart]));
115
+ throw new TypeError("unsupported source type for mediabunny");
116
+ }
117
+
118
+ function resolveContainer(formatName: string | undefined, sniffed: ContainerKind): ContainerKind {
119
+ const name = (formatName ?? "").toLowerCase();
120
+ if (name.includes("matroska") || name.includes("mkv")) return "mkv";
121
+ if (name.includes("webm")) return "webm";
122
+ if (name.includes("mp4") || name.includes("isom")) return "mp4";
123
+ if (name.includes("mov") || name.includes("quicktime")) return "mov";
124
+ if (name.includes("ogg")) return "ogg";
125
+ if (name.includes("wav")) return "wav";
126
+ if (name.includes("flac")) return "flac";
127
+ if (name.includes("mp3")) return "mp3";
128
+ if (name.includes("adts") || name.includes("aac")) return "adts";
129
+ if (name.includes("mpegts") || name.includes("mpeg-ts") || name.includes("transport")) return "mpegts";
130
+ return sniffed;
131
+ }
132
+
133
+ /** Mediabunny video codec → avbridge video codec. */
134
+ export function mediabunnyVideoToAvbridge(c: string | null | undefined): VideoCodec {
135
+ switch (c) {
136
+ case "avc": return "h264";
137
+ case "hevc": return "h265";
138
+ case "vp8": return "vp8";
139
+ case "vp9": return "vp9";
140
+ case "av1": return "av1";
141
+ default: return "h264";
142
+ }
143
+ }
144
+
145
+ /** avbridge video codec → mediabunny video codec (for output sources). */
146
+ export function avbridgeVideoToMediabunny(c: VideoCodec): "avc" | "hevc" | "vp9" | "vp8" | "av1" | null {
147
+ switch (c) {
148
+ case "h264": return "avc";
149
+ case "h265": return "hevc";
150
+ case "vp8": return "vp8";
151
+ case "vp9": return "vp9";
152
+ case "av1": return "av1";
153
+ default: return null;
154
+ }
155
+ }
156
+
157
+ export function mediabunnyAudioToAvbridge(c: string | null | undefined): AudioCodec {
158
+ switch (c) {
159
+ case "aac": return "aac";
160
+ case "mp3": return "mp3";
161
+ case "opus": return "opus";
162
+ case "vorbis": return "vorbis";
163
+ case "flac": return "flac";
164
+ case "ac3": return "ac3";
165
+ case "eac3": return "eac3";
166
+ default: return (c as AudioCodec) ?? "aac";
167
+ }
168
+ }
169
+
170
+ export function avbridgeAudioToMediabunny(c: AudioCodec): string | null {
171
+ switch (c) {
172
+ case "aac": return "aac";
173
+ case "mp3": return "mp3";
174
+ case "opus": return "opus";
175
+ case "vorbis": return "vorbis";
176
+ case "flac": return "flac";
177
+ case "ac3": return "ac3";
178
+ case "eac3": return "eac3";
179
+ default: return null;
180
+ }
181
+ }
182
+
183
+ async function safeNumber(fn: () => Promise<number> | number): Promise<number | undefined> {
184
+ try {
185
+ const v = await fn();
186
+ return typeof v === "number" && Number.isFinite(v) ? v : undefined;
187
+ } catch {
188
+ return undefined;
189
+ }
190
+ }
191
+
192
+ async function safe<T>(fn: () => Promise<T> | T): Promise<T | undefined> {
193
+ try { return await fn(); } catch { return undefined; }
194
+ }