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.
- package/CHANGELOG.md +120 -0
- package/LICENSE +21 -0
- package/README.md +415 -0
- package/dist/avi-M5B4SHRM.cjs +164 -0
- package/dist/avi-M5B4SHRM.cjs.map +1 -0
- package/dist/avi-POCGZ4JX.js +162 -0
- package/dist/avi-POCGZ4JX.js.map +1 -0
- package/dist/chunk-5ISVAODK.js +80 -0
- package/dist/chunk-5ISVAODK.js.map +1 -0
- package/dist/chunk-F7YS2XOA.cjs +2966 -0
- package/dist/chunk-F7YS2XOA.cjs.map +1 -0
- package/dist/chunk-FKM7QBZU.js +2957 -0
- package/dist/chunk-FKM7QBZU.js.map +1 -0
- package/dist/chunk-J5MCMN3S.js +27 -0
- package/dist/chunk-J5MCMN3S.js.map +1 -0
- package/dist/chunk-L4NPOJ36.cjs +180 -0
- package/dist/chunk-L4NPOJ36.cjs.map +1 -0
- package/dist/chunk-NZU7W256.cjs +29 -0
- package/dist/chunk-NZU7W256.cjs.map +1 -0
- package/dist/chunk-PQTZS7OA.js +147 -0
- package/dist/chunk-PQTZS7OA.js.map +1 -0
- package/dist/chunk-WD2ZNQA7.js +177 -0
- package/dist/chunk-WD2ZNQA7.js.map +1 -0
- package/dist/chunk-Y5FYF5KG.cjs +153 -0
- package/dist/chunk-Y5FYF5KG.cjs.map +1 -0
- package/dist/chunk-Z2FJ5TJC.cjs +82 -0
- package/dist/chunk-Z2FJ5TJC.cjs.map +1 -0
- package/dist/element.cjs +433 -0
- package/dist/element.cjs.map +1 -0
- package/dist/element.d.cts +158 -0
- package/dist/element.d.ts +158 -0
- package/dist/element.js +431 -0
- package/dist/element.js.map +1 -0
- package/dist/index.cjs +576 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +80 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.js +554 -0
- package/dist/index.js.map +1 -0
- package/dist/libav-http-reader-FPYDBMYK.cjs +16 -0
- package/dist/libav-http-reader-FPYDBMYK.cjs.map +1 -0
- package/dist/libav-http-reader-NQJVY273.js +3 -0
- package/dist/libav-http-reader-NQJVY273.js.map +1 -0
- package/dist/libav-import-2JURFHEW.js +8 -0
- package/dist/libav-import-2JURFHEW.js.map +1 -0
- package/dist/libav-import-GST2AMPL.cjs +30 -0
- package/dist/libav-import-GST2AMPL.cjs.map +1 -0
- package/dist/libav-loader-KA2MAWLM.js +3 -0
- package/dist/libav-loader-KA2MAWLM.js.map +1 -0
- package/dist/libav-loader-ZHOERPHW.cjs +12 -0
- package/dist/libav-loader-ZHOERPHW.cjs.map +1 -0
- package/dist/player-BBwbCkdL.d.cts +365 -0
- package/dist/player-BBwbCkdL.d.ts +365 -0
- package/dist/source-SC6ZEQYR.cjs +28 -0
- package/dist/source-SC6ZEQYR.cjs.map +1 -0
- package/dist/source-ZFS4H7J3.js +3 -0
- package/dist/source-ZFS4H7J3.js.map +1 -0
- package/dist/variant-routing-GOHB2RZN.cjs +12 -0
- package/dist/variant-routing-GOHB2RZN.cjs.map +1 -0
- package/dist/variant-routing-JOBWXYKD.js +3 -0
- package/dist/variant-routing-JOBWXYKD.js.map +1 -0
- package/package.json +95 -0
- package/src/classify/index.ts +1 -0
- package/src/classify/rules.ts +214 -0
- package/src/convert/index.ts +2 -0
- package/src/convert/remux.ts +522 -0
- package/src/convert/transcode.ts +329 -0
- package/src/diagnostics.ts +99 -0
- package/src/element/avbridge-player.ts +576 -0
- package/src/element.ts +19 -0
- package/src/events.ts +71 -0
- package/src/index.ts +42 -0
- package/src/libav-stubs.d.ts +24 -0
- package/src/player.ts +455 -0
- package/src/plugins/builtin.ts +37 -0
- package/src/plugins/registry.ts +32 -0
- package/src/probe/avi.ts +242 -0
- package/src/probe/index.ts +59 -0
- package/src/probe/mediabunny.ts +194 -0
- package/src/strategies/fallback/audio-output.ts +293 -0
- package/src/strategies/fallback/clock.ts +7 -0
- package/src/strategies/fallback/decoder.ts +660 -0
- package/src/strategies/fallback/index.ts +170 -0
- package/src/strategies/fallback/libav-import.ts +27 -0
- package/src/strategies/fallback/libav-loader.ts +190 -0
- package/src/strategies/fallback/variant-routing.ts +43 -0
- package/src/strategies/fallback/video-renderer.ts +216 -0
- package/src/strategies/hybrid/decoder.ts +641 -0
- package/src/strategies/hybrid/index.ts +139 -0
- package/src/strategies/native.ts +107 -0
- package/src/strategies/remux/annexb.ts +112 -0
- package/src/strategies/remux/index.ts +79 -0
- package/src/strategies/remux/mse.ts +234 -0
- package/src/strategies/remux/pipeline.ts +254 -0
- package/src/subtitles/index.ts +91 -0
- package/src/subtitles/render.ts +62 -0
- package/src/subtitles/srt.ts +62 -0
- package/src/subtitles/vtt.ts +5 -0
- package/src/types-shim.d.ts +3 -0
- package/src/types.ts +360 -0
- package/src/util/codec-strings.ts +86 -0
- package/src/util/libav-http-reader.ts +315 -0
- package/src/util/source.ts +274 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core types shared across avbridge modules.
|
|
3
|
+
*
|
|
4
|
+
* The four main concepts:
|
|
5
|
+
* - {@link MediaInput} — what the user gives us (File / Blob / URL / bytes).
|
|
6
|
+
* - {@link MediaContext} — what we learned about it from probing.
|
|
7
|
+
* - {@link Classification} — which playback strategy we picked.
|
|
8
|
+
* - {@link PlaybackSession} — the running playback, returned by a strategy.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Anything we accept as a media source. We do not accept arbitrary
|
|
13
|
+
* `ReadableStream`s in v1 because we need random access for seeking.
|
|
14
|
+
*/
|
|
15
|
+
export type MediaInput = File | Blob | string | URL | ArrayBuffer | Uint8Array;
|
|
16
|
+
|
|
17
|
+
/** Container format families we know about. */
|
|
18
|
+
export type ContainerKind =
|
|
19
|
+
| "mp4"
|
|
20
|
+
| "mov"
|
|
21
|
+
| "mkv"
|
|
22
|
+
| "webm"
|
|
23
|
+
| "avi"
|
|
24
|
+
| "asf"
|
|
25
|
+
| "flv"
|
|
26
|
+
| "ogg"
|
|
27
|
+
| "wav"
|
|
28
|
+
| "mp3"
|
|
29
|
+
| "flac"
|
|
30
|
+
| "adts"
|
|
31
|
+
| "mpegts"
|
|
32
|
+
| "unknown";
|
|
33
|
+
|
|
34
|
+
/** Video codec families. Strings, not enums, so plugins can extend. */
|
|
35
|
+
export type VideoCodec =
|
|
36
|
+
| "h264"
|
|
37
|
+
| "h265"
|
|
38
|
+
| "vp8"
|
|
39
|
+
| "vp9"
|
|
40
|
+
| "av1"
|
|
41
|
+
| "mpeg4" // MPEG-4 Part 2 (DivX/Xvid)
|
|
42
|
+
| "wmv3"
|
|
43
|
+
| "vc1"
|
|
44
|
+
| "rv40"
|
|
45
|
+
| "mpeg2"
|
|
46
|
+
| "mpeg1"
|
|
47
|
+
| "theora"
|
|
48
|
+
| (string & {});
|
|
49
|
+
|
|
50
|
+
/** Audio codec families. */
|
|
51
|
+
export type AudioCodec =
|
|
52
|
+
| "aac"
|
|
53
|
+
| "mp3"
|
|
54
|
+
| "opus"
|
|
55
|
+
| "vorbis"
|
|
56
|
+
| "flac"
|
|
57
|
+
| "pcm"
|
|
58
|
+
| "ac3"
|
|
59
|
+
| "eac3"
|
|
60
|
+
| "wmav2"
|
|
61
|
+
| "wmapro"
|
|
62
|
+
| "alac"
|
|
63
|
+
| (string & {});
|
|
64
|
+
|
|
65
|
+
export interface VideoTrackInfo {
|
|
66
|
+
id: number;
|
|
67
|
+
codec: VideoCodec;
|
|
68
|
+
/** Codec-private profile string when known (e.g. "High", "Main 10"). */
|
|
69
|
+
profile?: string;
|
|
70
|
+
level?: number;
|
|
71
|
+
width: number;
|
|
72
|
+
height: number;
|
|
73
|
+
/** Pixel format string in ffmpeg style (e.g. "yuv420p", "yuv420p10le"). */
|
|
74
|
+
pixelFormat?: string;
|
|
75
|
+
/** Frames per second, when known. */
|
|
76
|
+
fps?: number;
|
|
77
|
+
bitDepth?: number;
|
|
78
|
+
/** RFC 6381 codec string for `MediaSource.isTypeSupported`, when computable. */
|
|
79
|
+
codecString?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface AudioTrackInfo {
|
|
83
|
+
id: number;
|
|
84
|
+
codec: AudioCodec;
|
|
85
|
+
channels: number;
|
|
86
|
+
sampleRate: number;
|
|
87
|
+
language?: string;
|
|
88
|
+
codecString?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface SubtitleTrackInfo {
|
|
92
|
+
id: number;
|
|
93
|
+
/** "vtt" | "srt" | "ass" | "pgs" | "embedded" */
|
|
94
|
+
format: string;
|
|
95
|
+
language?: string;
|
|
96
|
+
/** Set if this is a sidecar file rather than embedded in the container. */
|
|
97
|
+
sidecarUrl?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Everything the probe layer learned about a source.
|
|
102
|
+
* This is the input to the classification engine.
|
|
103
|
+
*/
|
|
104
|
+
export interface MediaContext {
|
|
105
|
+
source: MediaInput;
|
|
106
|
+
/** Stable display name for diagnostics, if we have one. */
|
|
107
|
+
name?: string;
|
|
108
|
+
byteLength?: number;
|
|
109
|
+
container: ContainerKind;
|
|
110
|
+
videoTracks: VideoTrackInfo[];
|
|
111
|
+
audioTracks: AudioTrackInfo[];
|
|
112
|
+
subtitleTracks: SubtitleTrackInfo[];
|
|
113
|
+
/** Which probe backend produced this context, for diagnostics. */
|
|
114
|
+
probedBy: "mediabunny" | "libav" | "sniff";
|
|
115
|
+
/** Total duration in seconds, if known. */
|
|
116
|
+
duration?: number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The four playback strategies, ordered from lightest to heaviest:
|
|
121
|
+
* - `"native"` — direct `<video>` playback (zero overhead)
|
|
122
|
+
* - `"remux"` — repackage to fragmented MP4 via MSE (preserves hardware decode)
|
|
123
|
+
* - `"hybrid"` — libav.js demux + WebCodecs hardware decode (for AVI/ASF/FLV with modern codecs)
|
|
124
|
+
* - `"fallback"` — full WASM software decode via libav.js (universal, CPU-intensive)
|
|
125
|
+
*/
|
|
126
|
+
export type StrategyName = "native" | "remux" | "hybrid" | "fallback";
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Classification outcome from the rules engine. Determines which strategy to use:
|
|
130
|
+
* - `NATIVE` — browser plays this directly
|
|
131
|
+
* - `REMUX_CANDIDATE` — codecs are native, container needs repackaging
|
|
132
|
+
* - `HYBRID_CANDIDATE` — container needs libav demux, codecs are hardware-decodable
|
|
133
|
+
* - `FALLBACK_REQUIRED` — codec has no browser decoder, WASM decode needed
|
|
134
|
+
* - `RISKY_NATIVE` — might work natively but may stall (e.g. Hi10P, 4K120)
|
|
135
|
+
*/
|
|
136
|
+
export type StrategyClass =
|
|
137
|
+
| "NATIVE"
|
|
138
|
+
| "REMUX_CANDIDATE"
|
|
139
|
+
| "HYBRID_CANDIDATE"
|
|
140
|
+
| "FALLBACK_REQUIRED"
|
|
141
|
+
| "RISKY_NATIVE";
|
|
142
|
+
|
|
143
|
+
export interface Classification {
|
|
144
|
+
class: StrategyClass;
|
|
145
|
+
strategy: StrategyName;
|
|
146
|
+
reason: string;
|
|
147
|
+
/**
|
|
148
|
+
* Ordered list of strategies to try if the primary fails or stalls.
|
|
149
|
+
* The player pops from the front on each escalation.
|
|
150
|
+
*/
|
|
151
|
+
fallbackChain?: StrategyName[];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* A live playback session created by a strategy. The {@link UnifiedPlayer}
|
|
156
|
+
* delegates user-facing controls to whichever session is currently active.
|
|
157
|
+
*/
|
|
158
|
+
export interface PlaybackSession {
|
|
159
|
+
readonly strategy: StrategyName;
|
|
160
|
+
play(): Promise<void>;
|
|
161
|
+
pause(): void;
|
|
162
|
+
seek(time: number): Promise<void>;
|
|
163
|
+
setAudioTrack(id: number): Promise<void>;
|
|
164
|
+
setSubtitleTrack(id: number | null): Promise<void>;
|
|
165
|
+
/** Tear down everything: revoke object URLs, close decoders, etc. */
|
|
166
|
+
destroy(): Promise<void>;
|
|
167
|
+
/** Strategy-specific runtime stats merged into Diagnostics. */
|
|
168
|
+
getRuntimeStats(): Record<string, unknown>;
|
|
169
|
+
/** Current playback position in seconds. Used to capture position before strategy switch. */
|
|
170
|
+
getCurrentTime(): number;
|
|
171
|
+
/** Register a callback for unrecoverable errors that should trigger escalation. */
|
|
172
|
+
onFatalError?(handler: (reason: string) => void): void;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface DiagnosticsSnapshot {
|
|
176
|
+
container: ContainerKind | "unknown";
|
|
177
|
+
videoCodec?: VideoCodec;
|
|
178
|
+
audioCodec?: AudioCodec;
|
|
179
|
+
width?: number;
|
|
180
|
+
height?: number;
|
|
181
|
+
fps?: number;
|
|
182
|
+
duration?: number;
|
|
183
|
+
strategy: StrategyName | "pending";
|
|
184
|
+
strategyClass: StrategyClass | "pending";
|
|
185
|
+
reason: string;
|
|
186
|
+
probedBy?: "mediabunny" | "libav" | "sniff";
|
|
187
|
+
/**
|
|
188
|
+
* Where the source is coming from. `"blob"` means File / Blob /
|
|
189
|
+
* ArrayBuffer / Uint8Array (in-memory). `"url"` means an HTTP/HTTPS URL
|
|
190
|
+
* being streamed via Range requests.
|
|
191
|
+
*/
|
|
192
|
+
sourceType?: "blob" | "url";
|
|
193
|
+
/**
|
|
194
|
+
* Transport used to read the source. `"memory"` for in-memory blobs;
|
|
195
|
+
* `"http-range"` for URL sources streamed via HTTP Range requests.
|
|
196
|
+
*/
|
|
197
|
+
transport?: "memory" | "http-range";
|
|
198
|
+
/**
|
|
199
|
+
* For URL sources, true if the server supports HTTP Range requests
|
|
200
|
+
* (the only mode we accept — see `attachLibavHttpReader`). Always true
|
|
201
|
+
* when `transport === "http-range"` because we fail fast otherwise.
|
|
202
|
+
*/
|
|
203
|
+
rangeSupported?: boolean;
|
|
204
|
+
runtime?: Record<string, unknown>;
|
|
205
|
+
strategyHistory?: Array<{ strategy: StrategyName; reason: string; at: number }>;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** §8.2 plugin interface, kept structurally identical to the design doc. */
|
|
209
|
+
export interface Plugin {
|
|
210
|
+
name: string;
|
|
211
|
+
canHandle(context: MediaContext): boolean;
|
|
212
|
+
/** Returns a session if it claims the context, otherwise throws. */
|
|
213
|
+
execute(context: MediaContext, target: HTMLVideoElement): Promise<PlaybackSession>;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Player creation options. */
|
|
217
|
+
export interface CreatePlayerOptions {
|
|
218
|
+
source: MediaInput;
|
|
219
|
+
target: HTMLVideoElement;
|
|
220
|
+
/**
|
|
221
|
+
* Optional explicit subtitle list. The player otherwise tries to discover
|
|
222
|
+
* sidecar files via the FileSystemDirectoryHandle (when supplied), or pulls
|
|
223
|
+
* embedded subtitle tracks if the container exposes them.
|
|
224
|
+
*/
|
|
225
|
+
subtitles?: Array<{ url: string; language?: string; format?: "vtt" | "srt" }>;
|
|
226
|
+
/**
|
|
227
|
+
* Optional directory handle for sidecar discovery. When the source is a
|
|
228
|
+
* `File` selected from this directory, sibling `*.srt`/`*.vtt` files are
|
|
229
|
+
* picked up automatically.
|
|
230
|
+
*/
|
|
231
|
+
directory?: FileSystemDirectoryHandle;
|
|
232
|
+
/**
|
|
233
|
+
* Override the strategy decision. Useful for diagnostics and tests.
|
|
234
|
+
*/
|
|
235
|
+
forceStrategy?: StrategyName;
|
|
236
|
+
/** Inject extra plugins; they take priority over built-ins. */
|
|
237
|
+
plugins?: Plugin[];
|
|
238
|
+
/**
|
|
239
|
+
* When true (default), the player automatically escalates to the next
|
|
240
|
+
* strategy in the fallback chain on failure or stall.
|
|
241
|
+
*/
|
|
242
|
+
autoEscalate?: boolean;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Events emitted by {@link UnifiedPlayer}. Strongly typed. */
|
|
246
|
+
export interface PlayerEventMap {
|
|
247
|
+
strategy: { strategy: StrategyName; reason: string };
|
|
248
|
+
strategychange: { from: StrategyName; to: StrategyName; reason: string; currentTime: number };
|
|
249
|
+
tracks: {
|
|
250
|
+
video: VideoTrackInfo[];
|
|
251
|
+
audio: AudioTrackInfo[];
|
|
252
|
+
subtitle: SubtitleTrackInfo[];
|
|
253
|
+
};
|
|
254
|
+
error: Error;
|
|
255
|
+
timeupdate: { currentTime: number };
|
|
256
|
+
ended: void;
|
|
257
|
+
ready: void;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export type PlayerEventName = keyof PlayerEventMap;
|
|
261
|
+
|
|
262
|
+
/** Generic listener type re-exported for player.on overloads. */
|
|
263
|
+
export type Listener<T> = (payload: T) => void;
|
|
264
|
+
|
|
265
|
+
// ── Conversion types ────────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
/** Target output format for conversion functions. */
|
|
268
|
+
export type OutputFormat = "mp4" | "webm" | "mkv";
|
|
269
|
+
|
|
270
|
+
/** Options for standalone conversion functions ({@link remux}, transcode). */
|
|
271
|
+
export interface ConvertOptions {
|
|
272
|
+
/** Target container format. Default: `"mp4"`. */
|
|
273
|
+
outputFormat?: OutputFormat;
|
|
274
|
+
/** AbortSignal to cancel the operation. */
|
|
275
|
+
signal?: AbortSignal;
|
|
276
|
+
/** Called periodically with progress information. */
|
|
277
|
+
onProgress?: (info: ProgressInfo) => void;
|
|
278
|
+
/** When true, reject on any uncertain codec/container combo. Default: `false` (best-effort). */
|
|
279
|
+
strict?: boolean;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Progress information passed to {@link ConvertOptions.onProgress}. */
|
|
283
|
+
export interface ProgressInfo {
|
|
284
|
+
/** Estimated completion percentage, 0–100. */
|
|
285
|
+
percent: number;
|
|
286
|
+
/** Total bytes written to the output so far. */
|
|
287
|
+
bytesWritten: number;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Quality preset for transcode. */
|
|
291
|
+
export type TranscodeQuality = "low" | "medium" | "high" | "very-high";
|
|
292
|
+
|
|
293
|
+
/** Modern video codecs supported as transcode targets. */
|
|
294
|
+
export type OutputVideoCodec = "h264" | "h265" | "vp9" | "av1";
|
|
295
|
+
|
|
296
|
+
/** Modern audio codecs supported as transcode targets. */
|
|
297
|
+
export type OutputAudioCodec = "aac" | "opus" | "flac";
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Hardware acceleration hint for WebCodecs encoders.
|
|
301
|
+
* - `"no-preference"` (default) — let the browser pick
|
|
302
|
+
* - `"prefer-hardware"` — faster, may produce slightly lower quality at low bitrates
|
|
303
|
+
* - `"prefer-software"` — better quality at low bitrates, slower; recommended for archival
|
|
304
|
+
*/
|
|
305
|
+
export type HardwareAccelerationHint = "no-preference" | "prefer-hardware" | "prefer-software";
|
|
306
|
+
|
|
307
|
+
/** Options for {@link transcode}. Extends {@link ConvertOptions} with codec/quality. */
|
|
308
|
+
export interface TranscodeOptions extends ConvertOptions {
|
|
309
|
+
/** Target video codec. Default: `"h264"` for mp4/mkv, `"vp9"` for webm. */
|
|
310
|
+
videoCodec?: OutputVideoCodec;
|
|
311
|
+
/** Target audio codec. Default: `"aac"` for mp4/mkv, `"opus"` for webm. */
|
|
312
|
+
audioCodec?: OutputAudioCodec;
|
|
313
|
+
/** Quality preset. Default: `"medium"`. Maps to mediabunny `Quality` levels. */
|
|
314
|
+
quality?: TranscodeQuality;
|
|
315
|
+
/** Explicit video bitrate in bits per second. Overrides `quality`. */
|
|
316
|
+
videoBitrate?: number;
|
|
317
|
+
/** Explicit audio bitrate in bits per second. Overrides `quality`. */
|
|
318
|
+
audioBitrate?: number;
|
|
319
|
+
/** Target output width in pixels. Height is auto-deduced if not set. */
|
|
320
|
+
width?: number;
|
|
321
|
+
/** Target output height in pixels. Width is auto-deduced if not set. */
|
|
322
|
+
height?: number;
|
|
323
|
+
/** Target output frame rate. */
|
|
324
|
+
frameRate?: number;
|
|
325
|
+
/** Drop the video track entirely (audio-only output). */
|
|
326
|
+
dropVideo?: boolean;
|
|
327
|
+
/** Drop the audio track entirely (silent output). */
|
|
328
|
+
dropAudio?: boolean;
|
|
329
|
+
/**
|
|
330
|
+
* Hardware acceleration hint for the WebCodecs video encoder. Default: `"no-preference"`.
|
|
331
|
+
* Set to `"prefer-software"` for archival-quality encodes at low bitrates;
|
|
332
|
+
* `"prefer-hardware"` for fast batch transcoding where speed matters more than the last
|
|
333
|
+
* few percent of quality.
|
|
334
|
+
*/
|
|
335
|
+
hardwareAcceleration?: HardwareAccelerationHint;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Result of a standalone conversion ({@link remux} or transcode). */
|
|
339
|
+
export interface ConvertResult {
|
|
340
|
+
/** The converted file as a Blob, ready for download or further processing. */
|
|
341
|
+
blob: Blob;
|
|
342
|
+
/** Full MIME type string, e.g. `"video/mp4"`. */
|
|
343
|
+
mimeType: string;
|
|
344
|
+
/** Container format name: `"mp4"`, `"webm"`, or `"mkv"`. */
|
|
345
|
+
container: OutputFormat;
|
|
346
|
+
/** Video codec in the output, if present. */
|
|
347
|
+
videoCodec?: string;
|
|
348
|
+
/** Audio codec in the output, if present. */
|
|
349
|
+
audioCodec?: string;
|
|
350
|
+
/** Duration in seconds, if known. */
|
|
351
|
+
duration?: number;
|
|
352
|
+
/** Suggested filename for download. */
|
|
353
|
+
filename?: string;
|
|
354
|
+
/**
|
|
355
|
+
* Diagnostic notes about how the conversion ran. Currently records
|
|
356
|
+
* automatic retry of WebCodecs encoder failures (a known headless
|
|
357
|
+
* Chromium first-call init bug for the H.264 encoder).
|
|
358
|
+
*/
|
|
359
|
+
notes?: string[];
|
|
360
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { AudioTrackInfo, VideoTrackInfo } from "../types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build an RFC 6381 codec string for use with `MediaSource.isTypeSupported`
|
|
5
|
+
* and `<source type=...>`. Returns null when we don't have enough info to
|
|
6
|
+
* compose one — callers should treat that as "ask the browser at runtime via
|
|
7
|
+
* a different channel" rather than guessing.
|
|
8
|
+
*/
|
|
9
|
+
export function videoCodecString(track: VideoTrackInfo): string | null {
|
|
10
|
+
if (track.codecString) return track.codecString;
|
|
11
|
+
switch (track.codec) {
|
|
12
|
+
case "h264": {
|
|
13
|
+
// avc1.PPCCLL — profile (1B), constraint (1B), level (1B). Default to
|
|
14
|
+
// High Profile @ 4.0 if we don't know — common on real-world content.
|
|
15
|
+
const profileHex = profileToHex(track.profile) ?? "64"; // 0x64 = High
|
|
16
|
+
const constraint = "00";
|
|
17
|
+
const level = ((track.level ?? 40) & 0xff).toString(16).padStart(2, "0");
|
|
18
|
+
return `avc1.${profileHex}${constraint}${level}`;
|
|
19
|
+
}
|
|
20
|
+
case "h265":
|
|
21
|
+
// Default Main Profile @ Level 4.1 (0x5d = 93) — `hvc1.1.6.L93.B0`.
|
|
22
|
+
return "hvc1.1.6.L93.B0";
|
|
23
|
+
case "vp8":
|
|
24
|
+
return "vp8";
|
|
25
|
+
case "vp9":
|
|
26
|
+
return "vp09.00.10.08";
|
|
27
|
+
case "av1":
|
|
28
|
+
return "av01.0.04M.08";
|
|
29
|
+
default:
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function profileToHex(profile?: string): string | null {
|
|
35
|
+
if (!profile) return null;
|
|
36
|
+
const p = profile.toLowerCase();
|
|
37
|
+
if (p.includes("baseline")) return "42";
|
|
38
|
+
if (p.includes("main")) return "4d";
|
|
39
|
+
if (p.includes("high 10")) return "6e";
|
|
40
|
+
if (p.includes("high 4:2:2")) return "7a";
|
|
41
|
+
if (p.includes("high 4:4:4")) return "f4";
|
|
42
|
+
if (p.includes("high")) return "64";
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function audioCodecString(track: AudioTrackInfo): string | null {
|
|
47
|
+
if (track.codecString) return track.codecString;
|
|
48
|
+
switch (track.codec) {
|
|
49
|
+
case "aac":
|
|
50
|
+
return "mp4a.40.2"; // AAC-LC
|
|
51
|
+
case "mp3":
|
|
52
|
+
return "mp4a.40.34";
|
|
53
|
+
case "opus":
|
|
54
|
+
return "opus";
|
|
55
|
+
case "vorbis":
|
|
56
|
+
return "vorbis";
|
|
57
|
+
case "flac":
|
|
58
|
+
return "flac";
|
|
59
|
+
default:
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Compose a `video/mp4; codecs="..."` MIME for MSE. Returns null if either
|
|
66
|
+
* codec string can't be produced — caller should fall back to remux refusal.
|
|
67
|
+
*/
|
|
68
|
+
export function mp4MimeFor(video: VideoTrackInfo, audio?: AudioTrackInfo): string | null {
|
|
69
|
+
const v = videoCodecString(video);
|
|
70
|
+
if (!v) return null;
|
|
71
|
+
const codecs = audio ? `${v},${audioCodecString(audio) ?? ""}`.replace(/,$/, "") : v;
|
|
72
|
+
return `video/mp4; codecs="${codecs}"`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Wrap `MediaSource.isTypeSupported` so it returns false (instead of throwing)
|
|
77
|
+
* in environments without MSE — e.g. jsdom under vitest.
|
|
78
|
+
*/
|
|
79
|
+
export function mseSupports(mime: string): boolean {
|
|
80
|
+
if (typeof MediaSource === "undefined") return false;
|
|
81
|
+
try {
|
|
82
|
+
return MediaSource.isTypeSupported(mime);
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|