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
package/CHANGELOG.md ADDED
@@ -0,0 +1,120 @@
1
+ # Changelog
2
+
3
+ All notable changes to **avbridge** are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project
5
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Added
10
+
11
+ - **`createPlayer()`** — universal browser media player with automatic strategy
12
+ selection (native → remux → hybrid → fallback), runtime fallback escalation,
13
+ manual `setStrategy()`, typed events, diagnostics, and subtitle support.
14
+ - **`probe()` / `classify()`** — standalone analysis functions. Probe sniffs
15
+ the container via magic bytes, then routes to mediabunny (modern containers)
16
+ or libav.js (AVI/ASF/FLV). Classify decides the best playback strategy with
17
+ a fallback chain.
18
+ - **`remux()`** — standalone repackage from any avbridge-readable container
19
+ into a finalized downloadable MP4, WebM, or MKV. Built on mediabunny's
20
+ `Conversion` for modern containers and libav.js demux for AVI/ASF/FLV.
21
+ Lossless. Supports `signal`, `onProgress`, and `strict` mode.
22
+ - **`transcode()`** — standalone re-encode via WebCodecs encoders. Configurable
23
+ output container (mp4 / webm / mkv), video codec (h264 / h265 / vp9 / av1),
24
+ audio codec (aac / opus / flac), quality preset, explicit bitrate override,
25
+ resize, frame rate, drop-tracks, and `hardwareAcceleration` hint.
26
+ - **Native strategy** — direct `<video src>` playback for files browsers play
27
+ out of the box.
28
+ - **HTTP Range streaming for URL sources across all strategies.** Local files
29
+ (`File` / `Blob`) and remote URLs use the same API. URL inputs are read via
30
+ HTTP Range requests by every strategy:
31
+ - **Native**: passes the URL straight to `<video src>`; the browser drives
32
+ its own progressive download.
33
+ - **Remux**: uses mediabunny's `UrlSource` (Range requests + prefetch + cache).
34
+ - **Hybrid / fallback** (libav.js): uses a new HTTP block reader that wires
35
+ `libav.mkblockreaderdev` + `onblockread` to issue Range requests on demand.
36
+ Servers without Range support fail fast with a clear error rather than
37
+ silently downloading the whole file. The initial sniff is a single
38
+ `Range: bytes=0-32767` request — no full GET, ever.
39
+ - **Remux strategy** — mediabunny demux → fragmented MP4 → MSE for files whose
40
+ codecs are browser-supported but whose container isn't. Backpressure on the
41
+ SourceBuffer queue, deferred-seek across discontinuous ranges, automatic
42
+ re-creation of the muxer on seek to satisfy mediabunny's monotonic-timestamp
43
+ requirement.
44
+ - **MPEG-TS support** in the remux strategy — mediabunny demuxes TS natively,
45
+ Annex B H.264 packets are passed through (mediabunny extracts the AVC
46
+ decoder config from in-band SPS/PPS), and the MseSink snaps `video.currentTime`
47
+ to the start of the first buffered range to handle TS sources whose PTS
48
+ doesn't start at 0.
49
+ - **Hybrid strategy** — libav.js demux + WebCodecs `VideoDecoder` (hardware) +
50
+ libav.js audio decode for AVI/ASF/FLV files with browser-supported codecs.
51
+ Falls back to wall-clock timing when no audio decoder is available.
52
+ - **Fallback strategy** — full WASM software decode via libav.js, canvas
53
+ rendering, Web Audio output, audio-driven master clock with wall-clock
54
+ fallback when audio decode fails.
55
+ - **Subtitles** — SRT → VTT conversion, sidecar discovery, native `<track>`
56
+ for video strategies, overlay renderer for the fallback strategy.
57
+ - **Plugin system** — strategy registry with `canHandle()` / `execute()`
58
+ interface for injecting custom playback strategies.
59
+ - **Custom libav.js variant** — build script (`scripts/build-libav.sh`) for
60
+ AVI / WMV3 / MPEG-4 Part 2 / DivX / VC-1 and 15+ legacy codecs.
61
+ - **`<avbridge-player>.buffered`** — `TimeRanges` getter and `progress` event
62
+ forwarded from the underlying `<video>` element. Native and remux strategies
63
+ expose real buffered ranges; hybrid and fallback (canvas-rendered) currently
64
+ return an empty `TimeRanges` (synthesizing from the decoder is on the v1.1
65
+ list).
66
+ - **Streaming diagnostics** — `DiagnosticsSnapshot` now includes `sourceType`
67
+ (`"blob" | "url"`), `transport` (`"memory" | "http-range"`), and
68
+ `rangeSupported`, so consumers can show what's actually happening.
69
+ - **Demos** — Player demo (`demo/index.html`) and HandBrake-like Converter
70
+ demo (`demo/convert.html`).
71
+
72
+ ### Public API
73
+
74
+ ```ts
75
+ createPlayer(options): Promise<UnifiedPlayer>
76
+ probe(source): Promise<MediaContext>
77
+ classify(context): Classification
78
+ remux(source, options?): Promise<ConvertResult>
79
+ transcode(source, options?): Promise<ConvertResult>
80
+ srtToVtt(srt): string
81
+ ```
82
+
83
+ Public types: `MediaInput`, `CreatePlayerOptions`, `MediaContext`,
84
+ `Classification`, `StrategyName`, `StrategyClass`, `PlaybackSession`, `Plugin`,
85
+ `DiagnosticsSnapshot`, `PlayerEventMap`, `PlayerEventName`, `VideoTrackInfo`,
86
+ `AudioTrackInfo`, `SubtitleTrackInfo`, `ContainerKind`, `VideoCodec`,
87
+ `AudioCodec`, `OutputFormat`, `ConvertOptions`, `ConvertResult`, `ProgressInfo`,
88
+ `TranscodeOptions`, `TranscodeQuality`, `OutputVideoCodec`, `OutputAudioCodec`,
89
+ `HardwareAccelerationHint`.
90
+
91
+ ### Package boundary
92
+
93
+ - `avbridge` core (probe + classify + native + remux + transcode) — ~110 KB
94
+ ESM, no WASM.
95
+ - Optional fallback / hybrid: `@libav.js/variant-webcodecs` +
96
+ `libavjs-webcodecs-bridge` (peer-installed by the consumer).
97
+ - Custom libav.js build for AVI / WMV3 / DivX: documented in
98
+ `vendor/libav/README.md`.
99
+
100
+ ### Reliability
101
+
102
+ - **`transcode()` automatically retries on encoder init failures** (up to 2
103
+ retries with backoff). This works around a known headless Chromium bug
104
+ where the H.264 WebCodecs encoder fails on its first call per page and
105
+ recovers on retry. When retries occur, the cause is recorded in
106
+ `ConvertResult.notes` so consumers can detect and report the issue.
107
+ - Real browsers (Chrome/Edge/Safari) typically don't hit this bug; the
108
+ retry path is silent in those environments.
109
+
110
+ ### Known limitations
111
+
112
+ - Fallback / hybrid require optional libav.js installs.
113
+ - `transcode()` v1 only accepts inputs in mediabunny-readable containers
114
+ (MP4 / MKV / WebM / OGG / MOV / MP3 / FLAC / WAV). AVI/ASF/FLV transcoding
115
+ is planned for v1.1.
116
+ - `transcode()` uses WebCodecs encoders only; codec availability depends on
117
+ the browser. AV1 encoding is not yet universal.
118
+ - libav.js threading is disabled due to bugs in v6.8.8; decode runs
119
+ single-threaded with WASM SIMD acceleration.
120
+ - Multi-audio track selection in the remux strategy is not yet implemented.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-2026 Keishi Hattori
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,415 @@
1
+ # avbridge
2
+
3
+ [![npm](https://img.shields.io/npm/v/avbridge.svg)](https://www.npmjs.com/package/avbridge)
4
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/avbridge?label=gzipped)](https://bundlephobia.com/package/avbridge)
5
+ [![license](https://img.shields.io/npm/l/avbridge.svg)](./LICENSE)
6
+ [![CI](https://img.shields.io/github/actions/workflow/status/keishi/avbridge/ci.yml?branch=main&label=CI)](https://github.com/keishi/avbridge/actions/workflows/ci.yml)
7
+
8
+ > **Play and convert arbitrary video files in the browser. Local files or remote URLs.**
9
+
10
+ A media compatibility layer for the web. Drop in any file — MP4, MKV, AVI,
11
+ WMV, FLV, MPEG-TS, DivX — and avbridge picks the best path: native `<video>`
12
+ playback, mediabunny remux to fragmented MP4, libav.js demux + WebCodecs
13
+ hardware decode, or full WASM software decode. Same API for all of them.
14
+
15
+ **Streaming-first.** Remote URLs are read via HTTP Range requests across all
16
+ strategies — even AVI/WMV/FLV — so a 4 GB file plays without buffering 4 GB
17
+ into RAM. Local files (`File` / `Blob`) work the same way through the same API.
18
+
19
+ Designed for personal media libraries, local file managers, and
20
+ "open anything" web apps — not streaming platforms.
21
+
22
+ ## When should I use avbridge?
23
+
24
+ - You need to **play arbitrary user-provided video files** in the browser
25
+ - You want to **convert media to a browser-friendly format** without a server
26
+ - You **don't control the input format** — users may drop AVI, MKV, WMV, anything
27
+ - You want **one API** that handles format detection, strategy selection, and fallback automatically
28
+
29
+ ## How it works
30
+
31
+ Browsers only support a narrow set of containers and codecs. avbridge bridges
32
+ that gap with a multi-strategy pipeline:
33
+
34
+ 1. **Native** — hand the file to `<video>` (zero overhead)
35
+ 2. **Remux** — repackage to fragmented MP4 via MSE (preserves hardware decode)
36
+ 3. **Hybrid** — libav.js demux + WebCodecs hardware decode (for legacy containers with modern codecs)
37
+ 4. **Fallback** — full WASM software decode via libav.js (universal, CPU-intensive)
38
+
39
+ avbridge **always prefers native**, **prefers remux over decode**, and uses WASM
40
+ decode only when there is no other option. If a strategy fails or stalls, it
41
+ automatically escalates to the next one.
42
+
43
+ ```
44
+ MP4 (H.264/AAC) → native → direct <video> playback
45
+ MKV (H.264/AAC) → remux → fragmented MP4 via MSE
46
+ MPEG-TS (H.264) → remux → fragmented MP4 via MSE
47
+ AVI (H.264) → hybrid → libav demux + hardware decode
48
+ AVI (DivX) → fallback → smooth software decode
49
+ ```
50
+
51
+ ## Quick start
52
+
53
+ ### Playback
54
+
55
+ ```ts
56
+ import { createPlayer } from "avbridge";
57
+
58
+ const video = document.querySelector("video")!;
59
+ const player = await createPlayer({
60
+ source: file, // File / Blob / URL / ArrayBuffer
61
+ target: video,
62
+ });
63
+
64
+ player.on("strategy", ({ strategy, reason }) => {
65
+ console.log(`Using ${strategy}: ${reason}`);
66
+ });
67
+
68
+ await player.play();
69
+ ```
70
+
71
+ ### Remux / export
72
+
73
+ Convert a file to a modern format without re-encoding:
74
+
75
+ ```ts
76
+ import { remux } from "avbridge";
77
+
78
+ const result = await remux(file, {
79
+ outputFormat: "mp4", // "mp4" | "webm" | "mkv"
80
+ onProgress: ({ percent }) => console.log(`${percent.toFixed(0)}%`),
81
+ });
82
+
83
+ // result.blob is a downloadable MP4
84
+ const url = URL.createObjectURL(result.blob);
85
+ const a = document.createElement("a");
86
+ a.href = url;
87
+ a.download = result.filename ?? "output.mp4";
88
+ a.click();
89
+ ```
90
+
91
+ ### Transcode / re-encode
92
+
93
+ When the source codecs are legacy (or you want a different modern codec like AV1):
94
+
95
+ ```ts
96
+ import { transcode } from "avbridge";
97
+
98
+ const result = await transcode(file, {
99
+ outputFormat: "mp4",
100
+ videoCodec: "av1", // h264 | h265 | vp9 | av1
101
+ audioCodec: "opus", // aac | opus | flac
102
+ quality: "high", // low | medium | high | very-high
103
+ // Or override quality with explicit bitrate (in bps):
104
+ // videoBitrate: 4_000_000,
105
+ // audioBitrate: 192_000,
106
+ width: 1280, // optional resize
107
+ height: 720,
108
+ hardwareAcceleration: "prefer-software", // for archival quality
109
+ onProgress: ({ percent }) => console.log(`${percent.toFixed(0)}%`),
110
+ });
111
+
112
+ const url = URL.createObjectURL(result.blob);
113
+ ```
114
+
115
+ ### Analysis (standalone)
116
+
117
+ ```ts
118
+ import { probe, classify } from "avbridge";
119
+
120
+ const context = await probe(file);
121
+ console.log(context.container, context.videoTracks, context.audioTracks);
122
+
123
+ const decision = classify(context);
124
+ console.log(decision.strategy, decision.reason);
125
+ ```
126
+
127
+ ## Playback API
128
+
129
+ ```ts
130
+ createPlayer(options: CreatePlayerOptions): Promise<UnifiedPlayer>
131
+
132
+ interface UnifiedPlayer {
133
+ play(): Promise<void>;
134
+ pause(): void;
135
+ seek(time: number): Promise<void>;
136
+ setStrategy(strategy): Promise<void>;
137
+ setAudioTrack(id: number): Promise<void>;
138
+ setSubtitleTrack(id: number | null): Promise<void>;
139
+ getDuration(): number;
140
+ getCurrentTime(): number;
141
+ on(event, listener): () => void;
142
+ getDiagnostics(): DiagnosticsSnapshot;
143
+ destroy(): Promise<void>;
144
+ }
145
+ ```
146
+
147
+ ## Conversion API
148
+
149
+ ```ts
150
+ remux(source, options?): Promise<ConvertResult>
151
+ transcode(source, options?): Promise<ConvertResult>
152
+
153
+ interface ConvertOptions {
154
+ outputFormat?: "mp4" | "webm" | "mkv"; // default: "mp4"
155
+ signal?: AbortSignal;
156
+ onProgress?: (info: { percent: number; bytesWritten: number }) => void;
157
+ strict?: boolean; // reject uncertain combos like H.264 + MP3
158
+ }
159
+
160
+ interface TranscodeOptions extends ConvertOptions {
161
+ videoCodec?: "h264" | "h265" | "vp9" | "av1";
162
+ audioCodec?: "aac" | "opus" | "flac";
163
+ quality?: "low" | "medium" | "high" | "very-high"; // default: "medium"
164
+ videoBitrate?: number; // bits per second; overrides quality
165
+ audioBitrate?: number; // bits per second; overrides quality
166
+ width?: number; // resize; height auto-deduced if not set
167
+ height?: number;
168
+ frameRate?: number; // override frame rate
169
+ dropVideo?: boolean; // audio-only output
170
+ dropAudio?: boolean; // silent output
171
+ hardwareAcceleration?: "no-preference" | "prefer-hardware" | "prefer-software";
172
+ }
173
+
174
+ interface ConvertResult {
175
+ blob: Blob; // downloadable file
176
+ mimeType: string; // "video/mp4", "video/webm", "video/x-matroska"
177
+ container: string;
178
+ videoCodec?: string;
179
+ audioCodec?: string;
180
+ duration?: number;
181
+ filename?: string; // suggested download name
182
+ }
183
+ ```
184
+
185
+ ### What `remux()` guarantees
186
+
187
+ - Outputs a **finalized downloadable file** — not fragmented-for-streaming
188
+ - Does **not** decode or re-encode — lossless repackaging only
189
+ - Rejects unsupported codecs with a clear error pointing to `transcode()`
190
+ - In `strict` mode, rejects uncertain combinations (e.g. H.264 + MP3)
191
+
192
+ ### What `transcode()` does
193
+
194
+ - Decodes the source and re-encodes via **WebCodecs encoders** (hardware-accelerated when available)
195
+ - Mux pipeline is provided by mediabunny — it handles encoder selection, sample sync, and finalization
196
+ - Output format is fully configurable: container × video codec × audio codec × quality
197
+ - **Automatic retry on encoder init failures** — works around a headless-Chromium-specific
198
+ WebCodecs H.264 first-call init bug. When a retry happens, it's recorded in `result.notes`.
199
+ - Use the `hardwareAcceleration` hint to trade speed vs quality:
200
+ - `"prefer-hardware"` — fastest, may produce slightly lower quality at low bitrates
201
+ - `"prefer-software"` — slower, higher quality (recommended for archival)
202
+ - `"no-preference"` — let the browser pick (default)
203
+
204
+ ### Transcode codec compatibility
205
+
206
+ Which video/audio codec combinations are valid for each output container:
207
+
208
+ | Container | Video codecs | Audio codecs |
209
+ |-----------|-------------------------|--------------------|
210
+ | **MP4** | H.264, H.265/HEVC, AV1 | AAC, FLAC |
211
+ | **WebM** | VP9, AV1 | Opus |
212
+ | **MKV** | H.264, H.265, VP9, AV1 | AAC, Opus, FLAC |
213
+
214
+ Picking an incompatible combo (e.g. WebM + H.264) throws an error before any encoding starts.
215
+
216
+ > **Browser support note:** transcode availability depends on what the browser's WebCodecs implementation supports. Chrome/Edge have the broadest encoder set; Safari is narrower; Firefox is the most limited. AV1 encoding in particular is not yet universally supported.
217
+
218
+ ## Conversion support
219
+
220
+ | Input | Best path | Notes |
221
+ |---|---|---|
222
+ | MP4 (H.264/AAC) | **Native playback** | No conversion needed |
223
+ | MKV (H.264/AAC) | **Safe remux** | Repackage to MP4/WebM/MKV losslessly |
224
+ | MKV (H.265/Opus) | **Safe remux** | Any modern codec combo |
225
+ | MPEG-TS (H.264/AAC) | **Safe remux** | TS demuxed by mediabunny; repackaged to fragmented MP4 |
226
+ | MP4 (H.264/AAC) → MP4 AV1 | **Transcode** | Re-encode via WebCodecs `VideoEncoder` |
227
+ | MP4 (H.264) → WebM (VP9) | **Transcode** | Container + video codec change requires re-encode |
228
+ | AVI (H.264/MP3) | **Best-effort remux** | Requires libav.js for demux; `strict` mode rejects |
229
+ | AVI (DivX/Xvid) | **Requires transcode** | Codec has no browser decoder (input not yet supported by `transcode()` in v1) |
230
+ | WMV (WMV3) | **Requires transcode** | Codec has no browser decoder (input not yet supported by `transcode()` in v1) |
231
+
232
+ > **Note:** `transcode()` v1 only accepts inputs in mediabunny-readable containers (MP4, MKV, WebM, OGG, MOV, MP3, FLAC, WAV). Transcoding from AVI/ASF/FLV is planned for v1.1.
233
+
234
+ ## Diagnostics
235
+
236
+ Every decision avbridge makes is inspectable:
237
+
238
+ ```ts
239
+ player.getDiagnostics();
240
+ // {
241
+ // container: "avi",
242
+ // videoCodec: "h264",
243
+ // audioCodec: "mp3",
244
+ // strategy: "hybrid",
245
+ // strategyClass: "HYBRID_CANDIDATE",
246
+ // reason: "avi container requires libav demux; codecs are hardware-decodable",
247
+ // width: 1920, height: 1080, duration: 5400,
248
+ // probedBy: "libav",
249
+ // strategyHistory: [{ strategy: "hybrid", reason: "...", at: 1712764800000 }]
250
+ // }
251
+ ```
252
+
253
+ ## Install
254
+
255
+ ```bash
256
+ npm install avbridge
257
+ ```
258
+
259
+ This gives you the **core package**: probe, classify, native playback, remux,
260
+ transcode, and subtitles. No WASM. The full library is ~17 KB gzipped, but
261
+ tree-shaking is aggressive — what you actually pay for depends on which
262
+ exports you import:
263
+
264
+ | Import | Eager (gzip) |
265
+ |---|---|
266
+ | `srtToVtt` | **0.5 KB** |
267
+ | `probe`, `classify` | **3 KB** |
268
+ | `transcode` | **3.3 KB** |
269
+ | `remux` | **4.1 KB** |
270
+ | `createPlayer` | **14 KB** |
271
+ | `*` (everything) | **17 KB** |
272
+
273
+ The libav-loader path is split into a lazy chunk (~5 KB extra) that only
274
+ loads when a consumer actually invokes the AVI/ASF/FLV remux path.
275
+
276
+ Run `npm run audit:bundle` to verify these numbers in your fork.
277
+
278
+ ### Optional: fallback / hybrid strategies
279
+
280
+ For files that need software decode or libav.js demux (AVI, WMV, FLV,
281
+ legacy codecs):
282
+
283
+ ```bash
284
+ npm install @libav.js/variant-webcodecs libavjs-webcodecs-bridge
285
+ ```
286
+
287
+ This handles MKV/WebM/MP4 containers via the hybrid/fallback strategies.
288
+
289
+ ### Optional: AVI, WMV3, DivX, and other legacy formats
290
+
291
+ For **AVI, WMV3, MPEG-4 Part 2, DivX**, and other legacy formats, you need
292
+ a custom libav.js build — see [`vendor/libav/README.md`](./vendor/libav/README.md)
293
+ for the build recipe.
294
+
295
+ ### Package boundary summary
296
+
297
+ | What you need | What to install |
298
+ |---|---|
299
+ | Playback of MP4/MKV/WebM/**MPEG-TS** + remux/transcode export | `avbridge` (core, no WASM) |
300
+ | Fallback/hybrid decode for modern codecs in legacy containers (AVI/ASF/FLV) | + `@libav.js/variant-webcodecs` + `libavjs-webcodecs-bridge` |
301
+ | AVI, WMV3, DivX, MPEG-4 Part 2, VC-1 | + custom libav build (`scripts/build-libav.sh`) |
302
+
303
+ ### Serving the libav.js binaries
304
+
305
+ The optional libav variants ship as `.wasm` + `.mjs` files that need to be
306
+ served by your app at a known URL. avbridge looks for them at
307
+ `/libav/<variant>/libav-<variant>.mjs` (where `<variant>` is `webcodecs` or
308
+ `avbridge`). You can override the base URL with
309
+ `globalThis.AVBRIDGE_LIBAV_BASE = "/my-static-path"` before any avbridge
310
+ code runs.
311
+
312
+ #### Vite
313
+
314
+ Copy the variant binaries into your `public/libav/` directory at build
315
+ time. The avbridge demo does this via `scripts/copy-libav.mjs`:
316
+
317
+ ```bash
318
+ # In your project, after npm install:
319
+ mkdir -p public/libav/webcodecs
320
+ cp node_modules/@libav.js/variant-webcodecs/dist/* public/libav/webcodecs/
321
+ ```
322
+
323
+ For the custom `avbridge` variant, after running `./scripts/build-libav.sh`
324
+ in the avbridge repo, copy `vendor/libav/*` into `public/libav/avbridge/`.
325
+
326
+ #### Webpack
327
+
328
+ Use `copy-webpack-plugin` to ship the binaries to your output directory at
329
+ the same `libav/<variant>/` path.
330
+
331
+ #### Plain `<script>` / no bundler
332
+
333
+ Drop the variant directory anywhere on your origin and set
334
+ `globalThis.AVBRIDGE_LIBAV_BASE` to the matching URL before importing
335
+ avbridge.
336
+
337
+ If a libav-backed strategy is selected and the binary isn't reachable,
338
+ avbridge throws a clear error mentioning the URL it tried to load. The
339
+ core (native + remux for modern containers) doesn't need any of this.
340
+
341
+ ## Known limitations
342
+
343
+ - The **fallback strategy** uses WASM software decoding and is CPU-intensive, especially for HD video on mobile devices.
344
+ - **Remux of AVI/ASF/FLV** requires libav.js — the core package cannot demux these containers.
345
+ - **Remote URL playback requires HTTP Range requests.** Servers that don't support `Range: bytes=...` will fail fast with a clear error rather than silently downloading the whole file. This applies to all strategies.
346
+ - **H.264 + MP3 in MP4** is a best-effort combination that may produce playback issues in some browsers. Use `strict: true` to reject it, or re-encode audio to AAC via `transcode()`.
347
+ - AVI files with **packed B-frames** (some DivX encodes) may have timing issues until the `mpeg4_unpack_bframes` BSF is wired in.
348
+ - libav.js **threading is disabled** due to bugs in v6.8.8 — decode runs single-threaded with SIMD acceleration.
349
+ - `transcode()` v1 only accepts mediabunny-readable inputs (MP4/MKV/WebM/OGG/MOV/MP3/FLAC/WAV). AVI/ASF/FLV transcoding is planned for v1.1.
350
+ - `transcode()` uses **WebCodecs encoders only** — codec availability depends on the browser. AV1 encoding is not yet universal.
351
+ - For the **hybrid and fallback strategies**, `<avbridge-player>.buffered` returns an empty `TimeRanges` because the canvas-based renderers don't track buffered ranges yet. Native and remux strategies expose the full `<video>.buffered` set as expected.
352
+
353
+ ## Demos
354
+
355
+ ```bash
356
+ npm install
357
+ npm run demo
358
+ ```
359
+
360
+ Two pages share the dev server:
361
+
362
+ - **Player** (`/`) — file picker, custom controls, strategy badge, manual
363
+ backend switcher, live diagnostics. Drop a media file and watch the
364
+ strategy chain pick the best path.
365
+ - **Converter** (`/convert.html`) — HandBrake-like UI with container/codec/
366
+ quality/bitrate/resize options. Picks remux when codecs already match
367
+ the target, transcode when they don't. Progress bar, cancel, download.
368
+
369
+ ## Build & test
370
+
371
+ ```bash
372
+ npm run build # tsup → dist/ (ESM + CJS + d.ts, code-split lazy chunks)
373
+ npm run typecheck # tsc --noEmit
374
+ npm test # vitest unit tests
375
+ npm run audit:bundle # verify tree-shaking — bundle each public export and check size
376
+ npm run fixtures # regenerate the test fixture corpus from BBB source via ffmpeg
377
+
378
+ # Browser smoke tests (require `npm run demo` running in another terminal)
379
+ npm run test:playback -- tests/fixtures/ # walk the corpus through the player
380
+ npm run test:convert # exercise the converter via puppeteer
381
+ ```
382
+
383
+ ## Architecture
384
+
385
+ ```
386
+ probe(source) → MediaContext (container, codecs, tracks, resolution, ...)
387
+ classify(MediaContext) → Classification (strategy, reason, fallbackChain)
388
+ strategy.start() → PlaybackSession (play/pause/seek/destroy)
389
+ ```
390
+
391
+ If the chosen strategy fails or stalls, the player walks the `fallbackChain`
392
+ automatically (unless `autoEscalate: false` is set). Users can also call
393
+ `player.setStrategy()` at any time to switch manually.
394
+
395
+ ## Third-party licenses
396
+
397
+ avbridge itself is [MIT licensed](./LICENSE). It depends on:
398
+
399
+ | Library | License | Role |
400
+ |---|---|---|
401
+ | [mediabunny](https://mediabunny.dev) | MPL-2.0 | Demux/mux for modern containers (remux strategy + conversion) |
402
+ | [libav.js](https://github.com/Yahweasel/libav.js) | LGPL-2.1 | Demux + decode for legacy codecs (fallback/hybrid strategies) |
403
+ | [libavjs-webcodecs-bridge](https://github.com/Yahweasel/libavjs-webcodecs-bridge) | ISC | AVFrame <-> VideoFrame/AudioData conversion |
404
+
405
+ **LGPL-2.1 compliance**: The libav.js WASM binary in `vendor/libav/` is built
406
+ from source via [`scripts/build-libav.sh`](./scripts/build-libav.sh). The build
407
+ script, source repository URL, and version tag are provided so users can rebuild
408
+ or modify the library. See [`vendor/libav/README.md`](./vendor/libav/README.md).
409
+
410
+ **MPL-2.0 compliance**: mediabunny is used as an unmodified npm dependency. Its
411
+ source is available at the npm registry.
412
+
413
+ ## License
414
+
415
+ [MIT](./LICENSE)