avbridge 2.1.2 → 2.2.1
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 +138 -0
- package/README.md +98 -71
- package/dist/{avi-GNTV5ZOH.cjs → avi-6SJLWIWW.cjs} +19 -4
- package/dist/avi-6SJLWIWW.cjs.map +1 -0
- package/dist/{avi-V6HYQVR2.js → avi-GCGM7OJI.js} +18 -3
- package/dist/avi-GCGM7OJI.js.map +1 -0
- package/dist/{chunk-EJH67FXG.js → chunk-5DMTJVIU.js} +99 -3
- package/dist/chunk-5DMTJVIU.js.map +1 -0
- package/dist/{chunk-3AUGRKPY.js → chunk-DMWARSEF.js} +160 -27
- package/dist/chunk-DMWARSEF.js.map +1 -0
- package/dist/{chunk-JQH6D4OE.cjs → chunk-G4APZMCP.cjs} +100 -3
- package/dist/chunk-G4APZMCP.cjs.map +1 -0
- package/dist/{chunk-Y5FYF5KG.cjs → chunk-HZLQNKFN.cjs} +5 -2
- package/dist/chunk-HZLQNKFN.cjs.map +1 -0
- package/dist/{chunk-PQTZS7OA.js → chunk-ILKDNBSE.js} +5 -2
- package/dist/chunk-ILKDNBSE.js.map +1 -0
- package/dist/{chunk-DPVIOYGC.cjs → chunk-UF2N5L63.cjs} +164 -31
- package/dist/chunk-UF2N5L63.cjs.map +1 -0
- package/dist/element-browser.js +276 -21
- package/dist/element-browser.js.map +1 -1
- package/dist/element.cjs +4 -4
- package/dist/element.d.cts +1 -1
- package/dist/element.d.ts +1 -1
- package/dist/element.js +3 -3
- package/dist/index.cjs +18 -18
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -5
- package/dist/libav-loader-27RDIN2I.js +3 -0
- package/dist/{libav-loader-XKH2TKUW.js.map → libav-loader-27RDIN2I.js.map} +1 -1
- package/dist/libav-loader-IV4AJ2HW.cjs +12 -0
- package/dist/{libav-loader-6APXVNIV.cjs.map → libav-loader-IV4AJ2HW.cjs.map} +1 -1
- package/dist/{player-BdtUG4rh.d.cts → player-U2NPmFvA.d.cts} +4 -3
- package/dist/{player-BdtUG4rh.d.ts → player-U2NPmFvA.d.ts} +4 -3
- package/dist/source-CN43EI7Z.cjs +28 -0
- package/dist/{source-SC6ZEQYR.cjs.map → source-CN43EI7Z.cjs.map} +1 -1
- package/dist/source-FFZ7TW2B.js +3 -0
- package/dist/{source-ZFS4H7J3.js.map → source-FFZ7TW2B.js.map} +1 -1
- package/package.json +1 -1
- package/src/classify/rules.ts +9 -2
- package/src/player.ts +46 -17
- package/src/probe/avi.ts +8 -1
- package/src/strategies/fallback/audio-output.ts +25 -3
- package/src/strategies/fallback/decoder.ts +96 -8
- package/src/strategies/fallback/index.ts +98 -6
- package/src/strategies/fallback/libav-loader.ts +12 -0
- package/src/strategies/fallback/video-renderer.ts +5 -1
- package/src/strategies/hybrid/index.ts +9 -1
- package/src/strategies/remux/index.ts +13 -1
- package/src/strategies/remux/pipeline.ts +6 -0
- package/src/types.ts +10 -1
- package/src/util/debug.ts +131 -0
- package/src/util/source.ts +4 -0
- package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.mjs +1 -1
- package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.wasm +0 -0
- package/dist/avi-GNTV5ZOH.cjs.map +0 -1
- package/dist/avi-V6HYQVR2.js.map +0 -1
- package/dist/chunk-3AUGRKPY.js.map +0 -1
- package/dist/chunk-DPVIOYGC.cjs.map +0 -1
- package/dist/chunk-EJH67FXG.js.map +0 -1
- package/dist/chunk-JQH6D4OE.cjs.map +0 -1
- package/dist/chunk-PQTZS7OA.js.map +0 -1
- package/dist/chunk-Y5FYF5KG.cjs.map +0 -1
- package/dist/libav-loader-6APXVNIV.cjs +0 -12
- package/dist/libav-loader-XKH2TKUW.js +0 -3
- package/dist/source-SC6ZEQYR.cjs +0 -28
- package/dist/source-ZFS4H7J3.js +0 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,144 @@ All notable changes to **avbridge** are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project
|
|
5
5
|
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [2.2.1]
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Canvas renderer no longer stretches non-stage-aspect video.** The
|
|
12
|
+
fallback + hybrid renderer's canvas sat at `width:100%;height:100%`
|
|
13
|
+
with no `object-fit`, so portrait or otherwise non-matching content
|
|
14
|
+
was stretched to fill the stage. Now uses `object-fit: contain` to
|
|
15
|
+
letterbox the bitmap inside the stage.
|
|
16
|
+
- **Strategy switch to `remux` while playing now resumes playback.**
|
|
17
|
+
`doSetStrategy` calls `session.seek()` before `session.play()`, so
|
|
18
|
+
the remux pipeline used to start with `pendingAutoPlay=false`; the
|
|
19
|
+
subsequent `video.play()` would then hit an element whose `src`
|
|
20
|
+
wasn't yet assigned (the MseSink constructs lazily on first write)
|
|
21
|
+
and silently reject. `RemuxPipeline` gained `setAutoPlay()` so
|
|
22
|
+
`session.play()` can flip `pendingAutoPlay=true` mid-flight; the
|
|
23
|
+
MseSink fires `video.play()` as soon as buffered data lands.
|
|
24
|
+
- **Strategy switch from `hybrid` / `fallback` to another backend now
|
|
25
|
+
preserves play state.** Those strategies hide the `<video>` and
|
|
26
|
+
drive playback from their own Web Audio clock, so the underlying
|
|
27
|
+
element's native `paused` was always `true`. `doSetStrategy` read
|
|
28
|
+
`!target.paused` and captured `wasPlaying=false`, skipping the
|
|
29
|
+
restore on the new session. Both strategies now patch a
|
|
30
|
+
configurable `paused` getter on the target that mirrors
|
|
31
|
+
`audio.isPlaying()`, and clean it up on `destroy()`.
|
|
32
|
+
- **`initialStrategy` no longer retries the same failing strategy.**
|
|
33
|
+
`buildInitialDecision` inherited `natural.fallbackChain` verbatim,
|
|
34
|
+
so for a `RISKY_NATIVE` file with `initialStrategy: "remux"` the
|
|
35
|
+
chain still contained `"remux"` — on failure, `startSession` would
|
|
36
|
+
shift it off and retry `remux` before escalating. The synthetic
|
|
37
|
+
decision now filters `initial` out of the inherited chain.
|
|
38
|
+
- **`UnifiedPlayer.destroy()` removes the `ended` listener it
|
|
39
|
+
attached during `bootstrap()`.** Previously the anonymous handler
|
|
40
|
+
leaked across player lifecycles on long-lived target elements
|
|
41
|
+
(e.g. `<avbridge-video>` swapping source), causing gradual
|
|
42
|
+
accumulation and duplicate `ended` events after source reloads.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- Bundle audit ceiling for the `element-only` scenario raised to
|
|
47
|
+
20 KB eager gzip. The budget's purpose is catching
|
|
48
|
+
order-of-magnitude regressions (e.g. libav accidentally eager-
|
|
49
|
+
imported), not policing ±200 bytes; realistic first-play cost is
|
|
50
|
+
dominated by the multi-megabyte lazy wasm load.
|
|
51
|
+
|
|
52
|
+
## [2.2.0]
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
|
|
56
|
+
- **RealMedia playback support** (`.rm`, `.rmvb`). The custom `avbridge`
|
|
57
|
+
libav variant now includes the `rm` demuxer and every RealVideo /
|
|
58
|
+
RealAudio decoder family:
|
|
59
|
+
- Video: `rv10`, `rv20`, `rv30`, `rv40`
|
|
60
|
+
- Audio: `cook`, `ra_144`, `ra_288`, `sipr`, `atrac3`
|
|
61
|
+
These codecs have no browser decoder, so classification routes them
|
|
62
|
+
to the fallback WASM strategy. The custom variant grows by a few
|
|
63
|
+
hundred KB of WASM; the webcodecs variant is unchanged.
|
|
64
|
+
- **Sniff layer recognizes `.RMF` magic bytes** and returns the new
|
|
65
|
+
`"rm"` `ContainerKind`. Probing for a `.rm` or `.rmvb` file goes
|
|
66
|
+
through libav directly (mediabunny doesn't handle RealMedia).
|
|
67
|
+
- **File-picker accept list** in `demo/index.html` and
|
|
68
|
+
`demo/convert.html` now includes `.rm` and `.rmvb`.
|
|
69
|
+
|
|
70
|
+
### Changed
|
|
71
|
+
|
|
72
|
+
- **`VideoCodec`** gained `rv10`, `rv20`, `rv30` (existing `rv40` kept).
|
|
73
|
+
- **`AudioCodec`** gained `cook`, `ra_144`, `ra_288`, `sipr`, `atrac3`.
|
|
74
|
+
- **`ContainerKind`** gained `rm`.
|
|
75
|
+
- **Classifier** (`src/classify/rules.ts`) — all new RV/RA codecs added
|
|
76
|
+
to `FALLBACK_VIDEO_CODECS` / `FALLBACK_AUDIO_CODECS`.
|
|
77
|
+
|
|
78
|
+
### Fallback strategy performance tuning
|
|
79
|
+
|
|
80
|
+
These are general-purpose improvements motivated by RealMedia testing
|
|
81
|
+
but also benefit MPEG-4 Part 2 (DivX/Xvid), WMV3, and other
|
|
82
|
+
software-decoded content:
|
|
83
|
+
|
|
84
|
+
- **Cold-start pre-roll gate lowered 300 ms → 40 ms, timeout 10 s → 3 s.**
|
|
85
|
+
The gate used to wait for 300 ms of buffered audio before starting
|
|
86
|
+
playback. On software-decode-bound content (rv40, mpeg4 @ 720p+),
|
|
87
|
+
the decoder produces output slower than realtime, so 300 ms is
|
|
88
|
+
unreachable — the gate would sit out its 10 s timeout before the
|
|
89
|
+
first frame appeared, which the user experienced as a silent 10-
|
|
90
|
+
second hang after clicking Play. The gate now starts on 40 ms
|
|
91
|
+
audio + first frame, and the safety timeout is 3 s. A diagnostic
|
|
92
|
+
warning fires loudly if the timeout is ever hit.
|
|
93
|
+
- **Decoder read batch size raised 16 KB → 64 KB.** Fewer JS↔WASM
|
|
94
|
+
`ff_read_frame_multi` / `ff_decode_multi` round trips per unit of
|
|
95
|
+
video, which measurably speeds up software decode on slow devices.
|
|
96
|
+
Queue burstiness is unchanged because the existing
|
|
97
|
+
`queueHighWater = 30` backpressure still applies.
|
|
98
|
+
|
|
99
|
+
### Debug + self-diagnosis layer
|
|
100
|
+
|
|
101
|
+
New: **`src/util/debug.ts`** — a runtime-toggleable verbose logging
|
|
102
|
+
channel, plus unconditional warnings for suspicious conditions. The
|
|
103
|
+
goal is that subtle issues self-identify in the console instead of
|
|
104
|
+
requiring 10 minutes of reading diagnostics JSON.
|
|
105
|
+
|
|
106
|
+
- Set `globalThis.AVBRIDGE_DEBUG = true` (or append `?avbridge_debug`
|
|
107
|
+
to a demo page URL) to enable verbose logging. Every log is
|
|
108
|
+
prefixed `[avbridge:<tag>]` so you can filter.
|
|
109
|
+
- When debug is **off**, the following conditions still emit an
|
|
110
|
+
unconditional `console.warn`:
|
|
111
|
+
- **`[avbridge:cold-start] gate TIMEOUT…`** — the fallback
|
|
112
|
+
strategy's `waitForBuffer` hit its 3 s timeout with a
|
|
113
|
+
specific underflow (e.g. "audio=0 ms, frames=0"). This used to
|
|
114
|
+
silently hang playback for 10 seconds.
|
|
115
|
+
- **`[avbridge:decode-rate] decoder is running slower than
|
|
116
|
+
realtime…`** — watchdog in the fallback pump loop; fires once
|
|
117
|
+
per stall when framesDecoded/s stays below 60% of source fps
|
|
118
|
+
for ≥5 s after the first frame. Tells you the exact fps
|
|
119
|
+
ratio and names the likely cause.
|
|
120
|
+
- **`[avbridge:bootstrap] total bootstrap time <N>ms — unusually
|
|
121
|
+
slow…`** — bootstrap took >5 s end-to-end.
|
|
122
|
+
- **`[avbridge:probe] probe took <N>ms (>3000ms expected)…`** —
|
|
123
|
+
slow probe (usually a slow Range request or libav cold-start).
|
|
124
|
+
- **`[avbridge:libav-load] load "<variant>" took <N>ms
|
|
125
|
+
(>5000ms expected)…`** — slow WASM download or wrong base
|
|
126
|
+
path.
|
|
127
|
+
|
|
128
|
+
### Known limitations
|
|
129
|
+
|
|
130
|
+
- **rv40 / rv30 at 720p+ may still stutter** on modest CPUs. Single-
|
|
131
|
+
threaded WASM software decode of RealVideo's motion compensation is
|
|
132
|
+
fundamentally slower than realtime on many files. libav.js pthreads
|
|
133
|
+
and a WebGL YUV→RGBA upload path are both plausible follow-ups but
|
|
134
|
+
not in 2.2.0. For reference: a 1024×768 rv40 file plays at roughly
|
|
135
|
+
0.5-2× realtime on an M-series Mac depending on the bitrate. The
|
|
136
|
+
new `[avbridge:decode-rate]` watchdog flags this condition in the
|
|
137
|
+
console so the symptom is never a silent stutter.
|
|
138
|
+
|
|
139
|
+
### Tests
|
|
140
|
+
|
|
141
|
+
- New sniff test for `.RMF` magic bytes (`tests/sniff.test.ts`).
|
|
142
|
+
- Two new classify tests for RealMedia routing (rv40+cook, rv30+ra_288).
|
|
143
|
+
- Test count: 115 → **118**.
|
|
144
|
+
|
|
7
145
|
## [2.1.2]
|
|
8
146
|
|
|
9
147
|
### Fixed
|
package/README.md
CHANGED
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
> **Play and convert arbitrary video files in the browser. Local files or remote URLs.**
|
|
9
9
|
|
|
10
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
|
|
12
|
-
playback, mediabunny remux to fragmented MP4, libav.js demux +
|
|
13
|
-
hardware decode, or full WASM software decode. Same API for all
|
|
11
|
+
WMV, FLV, MPEG-TS, DivX, RMVB — and avbridge picks the best path: native
|
|
12
|
+
`<video>` playback, mediabunny remux to fragmented MP4, libav.js demux +
|
|
13
|
+
WebCodecs hardware decode, or full WASM software decode. Same API for all
|
|
14
|
+
of them.
|
|
14
15
|
|
|
15
16
|
**Streaming-first.** Remote URLs are read via HTTP Range requests across all
|
|
16
17
|
strategies — even AVI/WMV/FLV — so a 4 GB file plays without buffering 4 GB
|
|
@@ -46,6 +47,7 @@ MKV (H.264/AAC) → remux → fragmented MP4 via MSE
|
|
|
46
47
|
MPEG-TS (H.264) → remux → fragmented MP4 via MSE
|
|
47
48
|
AVI (H.264) → hybrid → libav demux + hardware decode
|
|
48
49
|
AVI (DivX) → fallback → smooth software decode
|
|
50
|
+
RMVB (rv40/cook) → fallback → libav software decode
|
|
49
51
|
```
|
|
50
52
|
|
|
51
53
|
## Quick start
|
|
@@ -246,107 +248,132 @@ player.getDiagnostics();
|
|
|
246
248
|
// reason: "avi container requires libav demux; codecs are hardware-decodable",
|
|
247
249
|
// width: 1920, height: 1080, duration: 5400,
|
|
248
250
|
// probedBy: "libav",
|
|
251
|
+
// transport: "http-range",
|
|
252
|
+
// rangeSupported: true,
|
|
253
|
+
// runtime: { decoderType: "webcodecs-hybrid", videoFramesDecoded: 5432, ... },
|
|
249
254
|
// strategyHistory: [{ strategy: "hybrid", reason: "...", at: 1712764800000 }]
|
|
250
255
|
// }
|
|
251
256
|
```
|
|
252
257
|
|
|
258
|
+
### Debug logging
|
|
259
|
+
|
|
260
|
+
Enable verbose per-stage logging for hard-to-diagnose issues:
|
|
261
|
+
|
|
262
|
+
```js
|
|
263
|
+
// In the browser console, or before avbridge loads:
|
|
264
|
+
globalThis.AVBRIDGE_DEBUG = true;
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
The demo pages also accept `?avbridge_debug` in the URL. When enabled,
|
|
268
|
+
every decision point emits a `[avbridge:<tag>]` log covering probe,
|
|
269
|
+
classify, libav load, bootstrap, strategy execute, and cold-start gate
|
|
270
|
+
timings.
|
|
271
|
+
|
|
272
|
+
The following **unconditional diagnostics** also fire — even without the
|
|
273
|
+
flag — when something smells off:
|
|
274
|
+
|
|
275
|
+
- `[avbridge:bootstrap]` — bootstrap chain took >5 s end-to-end
|
|
276
|
+
- `[avbridge:probe]` — probe took >3 s
|
|
277
|
+
- `[avbridge:libav-load]` — libav variant load took >5 s (usually a
|
|
278
|
+
misconfigured base path or server MIME type)
|
|
279
|
+
- `[avbridge:cold-start]` — fallback cold-start gate timed out or
|
|
280
|
+
released on video-only grace after waiting for audio
|
|
281
|
+
- `[avbridge:decode-rate]` — fallback decoder is running under 60% of
|
|
282
|
+
realtime fps for more than 5 seconds (one-shot per session)
|
|
283
|
+
- `[avbridge:overflow-drop]` — renderer is dropping more than 10% of
|
|
284
|
+
decoded frames because the decoder is bursting faster than the
|
|
285
|
+
canvas can drain (one-shot per session)
|
|
286
|
+
|
|
287
|
+
These are designed so "it works on my machine but stutters on your
|
|
288
|
+
file" surfaces the specific reason in the console instead of requiring
|
|
289
|
+
a live debug session.
|
|
290
|
+
|
|
253
291
|
## Install
|
|
254
292
|
|
|
255
293
|
```bash
|
|
256
294
|
npm install avbridge
|
|
257
295
|
```
|
|
258
296
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
297
|
+
That's it. **No optional peers to install, no binaries to build, no static
|
|
298
|
+
file path to configure.** Both libav.js variants (the 5 MB webcodecs build
|
|
299
|
+
and the 6.5 MB custom avbridge build with AVI/WMV/DivX/rv40 decoders) ship
|
|
300
|
+
inside the tarball under `node_modules/avbridge/vendor/libav/` and are
|
|
301
|
+
lazy-loaded at runtime only if a file actually needs them.
|
|
263
302
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
| `transcode` | **3.3 KB** |
|
|
269
|
-
| `remux` | **4.1 KB** |
|
|
270
|
-
| `createPlayer` | **14 KB** |
|
|
271
|
-
| `*` (everything) | **17 KB** |
|
|
303
|
+
Packed tarball is **~4 MB**, unpacked **~15 MB** (mostly the two WASM
|
|
304
|
+
binaries). If you only ever play native MP4, you never download a single
|
|
305
|
+
byte of the libav WASM — the loader is behind a dynamic `import()` that
|
|
306
|
+
never fires.
|
|
272
307
|
|
|
273
|
-
|
|
274
|
-
loads when a consumer actually invokes the AVI/ASF/FLV remux path.
|
|
308
|
+
### Two ways to consume
|
|
275
309
|
|
|
276
|
-
|
|
310
|
+
**Bundler (Vite, webpack, Rollup, esbuild):**
|
|
277
311
|
|
|
278
|
-
|
|
312
|
+
```ts
|
|
313
|
+
import { createPlayer, remux, transcode, probe, classify } from "avbridge";
|
|
314
|
+
// or
|
|
315
|
+
import "avbridge/element"; // registers <avbridge-video> custom element
|
|
316
|
+
```
|
|
279
317
|
|
|
280
|
-
|
|
281
|
-
|
|
318
|
+
The tree-shaking budgets below apply to this path. Your bundler resolves
|
|
319
|
+
`mediabunny` and `libavjs-webcodecs-bridge` through normal dependency
|
|
320
|
+
resolution. libav.js binaries live at
|
|
321
|
+
`node_modules/avbridge/vendor/libav/` — the loader finds them
|
|
322
|
+
automatically via `import.meta.url` in the generated chunk.
|
|
282
323
|
|
|
283
|
-
|
|
284
|
-
npm install @libav.js/variant-webcodecs libavjs-webcodecs-bridge
|
|
285
|
-
```
|
|
324
|
+
**Plain `<script type="module">` (no bundler):**
|
|
286
325
|
|
|
287
|
-
|
|
326
|
+
```html
|
|
327
|
+
<script type="module"
|
|
328
|
+
src="/node_modules/avbridge/dist/element-browser.js"></script>
|
|
288
329
|
|
|
289
|
-
|
|
330
|
+
<avbridge-video src="/video.mkv" autoplay playsinline></avbridge-video>
|
|
331
|
+
```
|
|
290
332
|
|
|
291
|
-
|
|
292
|
-
a
|
|
293
|
-
for
|
|
333
|
+
This is a second tsup entry (`dist/element-browser.js`) that inlines
|
|
334
|
+
mediabunny + libavjs-webcodecs-bridge into a single ~1.3 MB file with
|
|
335
|
+
zero bare specifiers at runtime. Perfect for self-hosted tools or static
|
|
336
|
+
sites that don't want a build step. It loads libav.js from the same
|
|
337
|
+
co-located `vendor/libav/` tree.
|
|
294
338
|
|
|
295
|
-
###
|
|
339
|
+
### Bundle sizes (bundler path)
|
|
296
340
|
|
|
297
|
-
|
|
|
341
|
+
| Import | Eager (gzip) |
|
|
298
342
|
|---|---|
|
|
299
|
-
|
|
|
300
|
-
|
|
|
301
|
-
|
|
|
302
|
-
|
|
303
|
-
|
|
343
|
+
| `srtToVtt` | **0.5 KB** |
|
|
344
|
+
| `probe`, `classify` | **2.5 KB** |
|
|
345
|
+
| `transcode` | **3 KB** |
|
|
346
|
+
| `remux` | **3.7 KB** |
|
|
347
|
+
| `createPlayer` | **15 KB** |
|
|
348
|
+
| `*` (everything) | **17.5 KB** |
|
|
349
|
+
| `avbridge/element` | **17 KB** |
|
|
304
350
|
|
|
305
|
-
|
|
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.
|
|
351
|
+
Run `npm run audit:bundle` to verify in your fork.
|
|
311
352
|
|
|
312
|
-
|
|
353
|
+
### Overriding the libav path (advanced)
|
|
313
354
|
|
|
314
|
-
|
|
315
|
-
|
|
355
|
+
If you want to host the libav binaries somewhere other than
|
|
356
|
+
`node_modules/avbridge/vendor/libav/` — for example a CDN, a custom
|
|
357
|
+
libav build, or a patched version — set `AVBRIDGE_LIBAV_BASE` **before**
|
|
358
|
+
any avbridge code runs:
|
|
316
359
|
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
cp node_modules/@libav.js/variant-webcodecs/dist/* public/libav/webcodecs/
|
|
360
|
+
```html
|
|
361
|
+
<script>globalThis.AVBRIDGE_LIBAV_BASE = "https://cdn.example.com/libav";</script>
|
|
362
|
+
<script type="module" src="..."></script>
|
|
321
363
|
```
|
|
322
364
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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.
|
|
365
|
+
The loader will then fetch `<base>/<variant>/libav-<variant>.mjs` and its
|
|
366
|
+
sibling `.wasm` files. This is the documented replaceability hook for
|
|
367
|
+
LGPL compliance — see [`NOTICE.md`](./NOTICE.md) and
|
|
368
|
+
[`THIRD_PARTY_LICENSES.md`](./THIRD_PARTY_LICENSES.md).
|
|
340
369
|
|
|
341
370
|
## Known limitations
|
|
342
371
|
|
|
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.
|
|
372
|
+
- The **fallback strategy** uses WASM software decoding and is CPU-intensive, especially for HD video on mobile devices. The `[avbridge:decode-rate]` diagnostic fires if the decoder falls below 60% of realtime so you know that's what's happening. Codecs with no WebCodecs support (rv40, mpeg4 @ 720p+, wmv3, vc1 at high resolutions) are the usual suspects.
|
|
345
373
|
- **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
374
|
- **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
|
-
-
|
|
348
|
-
-
|
|
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.
|
|
375
|
+
- libav.js **threading is disabled** due to known runtime bugs in the v6.8.8 pthreads build — decode runs single-threaded with WASM SIMD acceleration.
|
|
376
|
+
- `transcode()` only accepts mediabunny-readable inputs (MP4/MKV/WebM/OGG/MOV/MP3/FLAC/WAV). AVI/ASF/FLV/RM transcoding means "play it first, record the output" — not yet plumbed.
|
|
350
377
|
- `transcode()` uses **WebCodecs encoders only** — codec availability depends on the browser. AV1 encoding is not yet universal.
|
|
351
378
|
- For the **hybrid and fallback strategies**, `<avbridge-video>.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
379
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var chunkL4NPOJ36_cjs = require('./chunk-L4NPOJ36.cjs');
|
|
4
|
-
var
|
|
4
|
+
var chunkG4APZMCP_cjs = require('./chunk-G4APZMCP.cjs');
|
|
5
5
|
|
|
6
6
|
// src/probe/avi.ts
|
|
7
7
|
async function probeWithLibav(source, sniffed) {
|
|
8
|
-
const libav = await
|
|
8
|
+
const libav = await chunkG4APZMCP_cjs.loadLibav("avbridge");
|
|
9
9
|
const filename = source.name ?? `input.${sniffed === "unknown" ? "bin" : sniffed}`;
|
|
10
10
|
const handle = await chunkL4NPOJ36_cjs.prepareLibavInput(libav, filename, source);
|
|
11
11
|
let fmt_ctx;
|
|
@@ -123,7 +123,12 @@ function ffmpegToAvbridgeVideo(name) {
|
|
|
123
123
|
return "mpeg1";
|
|
124
124
|
case "theora":
|
|
125
125
|
return "theora";
|
|
126
|
+
case "rv10":
|
|
127
|
+
return "rv10";
|
|
128
|
+
case "rv20":
|
|
129
|
+
return "rv20";
|
|
126
130
|
case "rv30":
|
|
131
|
+
return "rv30";
|
|
127
132
|
case "rv40":
|
|
128
133
|
return "rv40";
|
|
129
134
|
default:
|
|
@@ -154,11 +159,21 @@ function ffmpegToAvbridgeAudio(name) {
|
|
|
154
159
|
return "wmapro";
|
|
155
160
|
case "alac":
|
|
156
161
|
return "alac";
|
|
162
|
+
case "cook":
|
|
163
|
+
return "cook";
|
|
164
|
+
case "ra_144":
|
|
165
|
+
return "ra_144";
|
|
166
|
+
case "ra_288":
|
|
167
|
+
return "ra_288";
|
|
168
|
+
case "sipr":
|
|
169
|
+
return "sipr";
|
|
170
|
+
case "atrac3":
|
|
171
|
+
return "atrac3";
|
|
157
172
|
default:
|
|
158
173
|
return name;
|
|
159
174
|
}
|
|
160
175
|
}
|
|
161
176
|
|
|
162
177
|
exports.probeWithLibav = probeWithLibav;
|
|
163
|
-
//# sourceMappingURL=avi-
|
|
164
|
-
//# sourceMappingURL=avi-
|
|
178
|
+
//# sourceMappingURL=avi-6SJLWIWW.cjs.map
|
|
179
|
+
//# sourceMappingURL=avi-6SJLWIWW.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/probe/avi.ts"],"names":["loadLibav","prepareLibavInput"],"mappings":"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","file":"avi-6SJLWIWW.cjs","sourcesContent":["import type {\n AudioCodec,\n AudioTrackInfo,\n ContainerKind,\n MediaContext,\n VideoCodec,\n VideoTrackInfo,\n} from \"../types.js\";\nimport type { NormalizedSource } from \"../util/source.js\";\nimport { prepareLibavInput, type LibavInputHandle } from \"../util/libav-http-reader.js\";\nimport { loadLibav } from \"../strategies/fallback/libav-loader.js\";\n\n/**\n * Probe AVI/ASF/FLV (and any other format mediabunny doesn't speak) via\n * libav.js. This module is `import()`-ed only when sniffing identifies one of\n * those containers.\n *\n * Critical: codec identification goes through `libav.avcodec_get_name(id)`\n * which returns the FFmpeg codec name as a string (e.g. \"h264\", \"mpeg4\",\n * \"wmv3\"). The numeric AV_CODEC_ID_* enum is *not* exposed on the libav\n * instance (only AVMEDIA_TYPE_*, AV_PIX_FMT_*, AV_SAMPLE_FMT_* and a handful\n * of others are), so comparing codec_ids against constants does not work.\n */\nexport async function probeWithLibav(\n source: NormalizedSource,\n sniffed: ContainerKind,\n): Promise<MediaContext> {\n // AVI/ASF/FLV demuxers are not in any libav.js npm variant — they live in\n // the custom \"avbridge\" build produced by `scripts/build-libav.sh`. The loader\n // emits an actionable error if the build hasn't been run yet. Threading\n // is OFF by default in `loadLibav` (see the comment there for why).\n const libav = (await loadLibav(\"avbridge\")) as unknown as LibavInstance;\n\n const filename = source.name ?? `input.${sniffed === \"unknown\" ? \"bin\" : sniffed}`;\n // For Blob/File sources we use libav's in-memory readahead file. For URL\n // sources we attach an HTTP block reader so libav demuxes via Range\n // requests instead of buffering the whole file.\n const handle: LibavInputHandle = await prepareLibavInput(libav as unknown as Parameters<typeof prepareLibavInput>[0], filename, source);\n\n let fmt_ctx: number | undefined;\n let streams: LibavStream[] = [];\n try {\n const result = await libav.ff_init_demuxer_file(filename);\n fmt_ctx = result[0];\n streams = result[1];\n } catch (err) {\n await handle.detach().catch(() => {});\n // Errors thrown across the libav.js worker/pthread boundary aren't\n // always Error instances — they can be plain objects, numbers (errno\n // codes), or strings. Stringify defensively so the user-facing message\n // never has `(undefined)` in it.\n const inner =\n err instanceof Error\n ? err.message\n : typeof err === \"object\" && err !== null\n ? JSON.stringify(err)\n : String(err);\n // eslint-disable-next-line no-console\n console.error(\"[avbridge] ff_init_demuxer_file raw error:\", err);\n throw new Error(\n `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\"})`,\n );\n }\n\n const videoTracks: VideoTrackInfo[] = [];\n const audioTracks: AudioTrackInfo[] = [];\n\n for (const stream of streams) {\n const codecName = (await safe(() => libav.avcodec_get_name(stream.codec_id))) ?? `unknown(${stream.codec_id})`;\n // codecpar holds width/height/channels/sample_rate/profile/level/extradata\n // for the actual stream. We have to copy it out of WASM memory.\n const codecpar = await safe(() => libav.ff_copyout_codecpar(stream.codecpar));\n\n if (stream.codec_type === libav.AVMEDIA_TYPE_VIDEO) {\n videoTracks.push({\n id: stream.index,\n codec: ffmpegToAvbridgeVideo(codecName),\n width: codecpar?.width ?? 0,\n height: codecpar?.height ?? 0,\n fps: framerate(stream),\n });\n } else if (stream.codec_type === libav.AVMEDIA_TYPE_AUDIO) {\n audioTracks.push({\n id: stream.index,\n codec: ffmpegToAvbridgeAudio(codecName),\n channels: codecpar?.channels ?? codecpar?.ch_layout_nb_channels ?? 0,\n sampleRate: codecpar?.sample_rate ?? 0,\n });\n }\n }\n\n // We need this duration but cannot reliably get it from the streams alone\n // for AVI; libav.js exposes it via the AVFormatContext duration helper.\n const duration = await safeDuration(libav, fmt_ctx!);\n\n // Close the demuxer; the strategy will reopen it later if it ends up being\n // chosen. Probing should not pin native resources.\n await libav.avformat_close_input_js(fmt_ctx!).catch(() => {});\n await handle.detach().catch(() => {});\n\n return {\n source: source.original,\n name: source.name,\n byteLength: source.byteLength,\n container: sniffed === \"unknown\" ? \"unknown\" : sniffed,\n videoTracks,\n audioTracks,\n subtitleTracks: [],\n probedBy: \"libav\",\n duration,\n };\n}\n\nfunction framerate(stream: LibavStream): number | undefined {\n if (typeof stream.avg_frame_rate_num === \"number\" && stream.avg_frame_rate_den) {\n return stream.avg_frame_rate_num / stream.avg_frame_rate_den;\n }\n if (stream.avg_frame_rate && typeof stream.avg_frame_rate === \"object\") {\n if (stream.avg_frame_rate.den === 0) return undefined;\n return stream.avg_frame_rate.num / stream.avg_frame_rate.den;\n }\n return undefined;\n}\n\nasync function safeDuration(libav: LibavInstance, fmt_ctx: number): Promise<number | undefined> {\n try {\n // `AVFormatContext.duration` is an int64 in microseconds (AV_TIME_BASE).\n // libav.js exposes it as a split lo/hi pair the same way it does for\n // packet pts — `AVFormatContext_duration(ctx)` returns the low 32 bits,\n // `AVFormatContext_durationhi(ctx)` returns the high 32 bits. Reading\n // only the low half (the previous bug) gave garbage for any file whose\n // duration > ~35 minutes, and zero for shorter files where the value\n // happened to live in the high half.\n const lo = await libav.AVFormatContext_duration?.(fmt_ctx);\n const hi = await libav.AVFormatContext_durationhi?.(fmt_ctx);\n if (typeof lo !== \"number\" || typeof hi !== \"number\") return undefined;\n\n // AV_NOPTS_VALUE = -2^63 → ptshi = -2147483648, pts = 0. Means \"unknown\".\n if (hi === -2147483648 && lo === 0) return undefined;\n\n // Reconstruct the 64-bit value. Prefer libav's helper when available\n // because it correctly handles signed 32-bit two's complement.\n const us =\n typeof libav.i64tof64 === \"function\"\n ? libav.i64tof64(lo, hi)\n : hi * 0x100000000 + lo + (lo < 0 ? 0x100000000 : 0);\n\n if (!Number.isFinite(us) || us <= 0) return undefined;\n return us / 1_000_000;\n } catch {\n return undefined;\n }\n}\n\nasync function safe<T>(fn: () => Promise<T> | T): Promise<T | undefined> {\n try { return await fn(); } catch { return undefined; }\n}\n\n/** Map FFmpeg codec names to avbridge video codec identifiers. */\nfunction ffmpegToAvbridgeVideo(name: string): VideoCodec {\n switch (name) {\n case \"h264\": return \"h264\";\n case \"hevc\": return \"h265\";\n case \"vp8\": return \"vp8\";\n case \"vp9\": return \"vp9\";\n case \"av1\": return \"av1\";\n case \"mpeg4\": return \"mpeg4\"; // MPEG-4 Part 2 / DivX / Xvid\n case \"msmpeg4v1\":\n case \"msmpeg4v2\":\n case \"msmpeg4v3\": // a.k.a. DIV3\n return \"mpeg4\";\n case \"wmv1\":\n case \"wmv2\":\n case \"wmv3\":\n return \"wmv3\";\n case \"vc1\": return \"vc1\";\n case \"mpeg2video\": return \"mpeg2\";\n case \"mpeg1video\": return \"mpeg1\";\n case \"theora\": return \"theora\";\n case \"rv10\": return \"rv10\";\n case \"rv20\": return \"rv20\";\n case \"rv30\": return \"rv30\";\n case \"rv40\": return \"rv40\";\n default: return name as VideoCodec;\n }\n}\n\nfunction ffmpegToAvbridgeAudio(name: string): AudioCodec {\n switch (name) {\n case \"aac\": return \"aac\";\n case \"mp3\":\n case \"mp3float\":\n return \"mp3\";\n case \"opus\": return \"opus\";\n case \"vorbis\": return \"vorbis\";\n case \"flac\": return \"flac\";\n case \"ac3\": return \"ac3\";\n case \"eac3\": return \"eac3\";\n case \"wmav1\":\n case \"wmav2\": return \"wmav2\";\n case \"wmapro\": return \"wmapro\";\n case \"alac\": return \"alac\";\n case \"cook\": return \"cook\";\n case \"ra_144\": return \"ra_144\";\n case \"ra_288\": return \"ra_288\";\n case \"sipr\": return \"sipr\";\n case \"atrac3\": return \"atrac3\";\n default: return name as AudioCodec;\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Minimal structural types for the slice of libav.js we touch.\n// ─────────────────────────────────────────────────────────────────────────────\n\ninterface LibavStream {\n index: number;\n codec_type: number;\n codec_id: number;\n codecpar: number;\n avg_frame_rate?: { num: number; den: number };\n avg_frame_rate_num?: number;\n avg_frame_rate_den?: number;\n}\n\ninterface LibavCodecpar {\n width?: number;\n height?: number;\n channels?: number;\n ch_layout_nb_channels?: number;\n sample_rate?: number;\n profile?: number;\n level?: number;\n}\n\ninterface LibavInstance {\n mkreadaheadfile(name: string, blob: Blob): Promise<void>;\n unlinkreadaheadfile(name: string): Promise<void>;\n ff_init_demuxer_file(name: string): Promise<[number, LibavStream[]]>;\n ff_copyout_codecpar(codecpar: number): Promise<LibavCodecpar>;\n avcodec_get_name(codec_id: number): Promise<string>;\n avformat_close_input_js(ctx: number): Promise<void>;\n AVFormatContext_duration?(ctx: number): Promise<number>;\n AVFormatContext_durationhi?(ctx: number): Promise<number>;\n i64tof64?(lo: number, hi: number): number;\n\n AVMEDIA_TYPE_VIDEO: number;\n AVMEDIA_TYPE_AUDIO: number;\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { prepareLibavInput } from './chunk-WD2ZNQA7.js';
|
|
2
|
-
import { loadLibav } from './chunk-
|
|
2
|
+
import { loadLibav } from './chunk-5DMTJVIU.js';
|
|
3
3
|
|
|
4
4
|
// src/probe/avi.ts
|
|
5
5
|
async function probeWithLibav(source, sniffed) {
|
|
@@ -121,7 +121,12 @@ function ffmpegToAvbridgeVideo(name) {
|
|
|
121
121
|
return "mpeg1";
|
|
122
122
|
case "theora":
|
|
123
123
|
return "theora";
|
|
124
|
+
case "rv10":
|
|
125
|
+
return "rv10";
|
|
126
|
+
case "rv20":
|
|
127
|
+
return "rv20";
|
|
124
128
|
case "rv30":
|
|
129
|
+
return "rv30";
|
|
125
130
|
case "rv40":
|
|
126
131
|
return "rv40";
|
|
127
132
|
default:
|
|
@@ -152,11 +157,21 @@ function ffmpegToAvbridgeAudio(name) {
|
|
|
152
157
|
return "wmapro";
|
|
153
158
|
case "alac":
|
|
154
159
|
return "alac";
|
|
160
|
+
case "cook":
|
|
161
|
+
return "cook";
|
|
162
|
+
case "ra_144":
|
|
163
|
+
return "ra_144";
|
|
164
|
+
case "ra_288":
|
|
165
|
+
return "ra_288";
|
|
166
|
+
case "sipr":
|
|
167
|
+
return "sipr";
|
|
168
|
+
case "atrac3":
|
|
169
|
+
return "atrac3";
|
|
155
170
|
default:
|
|
156
171
|
return name;
|
|
157
172
|
}
|
|
158
173
|
}
|
|
159
174
|
|
|
160
175
|
export { probeWithLibav };
|
|
161
|
-
//# sourceMappingURL=avi-
|
|
162
|
-
//# sourceMappingURL=avi-
|
|
176
|
+
//# sourceMappingURL=avi-GCGM7OJI.js.map
|
|
177
|
+
//# sourceMappingURL=avi-GCGM7OJI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/probe/avi.ts"],"names":[],"mappings":"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","file":"avi-GCGM7OJI.js","sourcesContent":["import type {\n AudioCodec,\n AudioTrackInfo,\n ContainerKind,\n MediaContext,\n VideoCodec,\n VideoTrackInfo,\n} from \"../types.js\";\nimport type { NormalizedSource } from \"../util/source.js\";\nimport { prepareLibavInput, type LibavInputHandle } from \"../util/libav-http-reader.js\";\nimport { loadLibav } from \"../strategies/fallback/libav-loader.js\";\n\n/**\n * Probe AVI/ASF/FLV (and any other format mediabunny doesn't speak) via\n * libav.js. This module is `import()`-ed only when sniffing identifies one of\n * those containers.\n *\n * Critical: codec identification goes through `libav.avcodec_get_name(id)`\n * which returns the FFmpeg codec name as a string (e.g. \"h264\", \"mpeg4\",\n * \"wmv3\"). The numeric AV_CODEC_ID_* enum is *not* exposed on the libav\n * instance (only AVMEDIA_TYPE_*, AV_PIX_FMT_*, AV_SAMPLE_FMT_* and a handful\n * of others are), so comparing codec_ids against constants does not work.\n */\nexport async function probeWithLibav(\n source: NormalizedSource,\n sniffed: ContainerKind,\n): Promise<MediaContext> {\n // AVI/ASF/FLV demuxers are not in any libav.js npm variant — they live in\n // the custom \"avbridge\" build produced by `scripts/build-libav.sh`. The loader\n // emits an actionable error if the build hasn't been run yet. Threading\n // is OFF by default in `loadLibav` (see the comment there for why).\n const libav = (await loadLibav(\"avbridge\")) as unknown as LibavInstance;\n\n const filename = source.name ?? `input.${sniffed === \"unknown\" ? \"bin\" : sniffed}`;\n // For Blob/File sources we use libav's in-memory readahead file. For URL\n // sources we attach an HTTP block reader so libav demuxes via Range\n // requests instead of buffering the whole file.\n const handle: LibavInputHandle = await prepareLibavInput(libav as unknown as Parameters<typeof prepareLibavInput>[0], filename, source);\n\n let fmt_ctx: number | undefined;\n let streams: LibavStream[] = [];\n try {\n const result = await libav.ff_init_demuxer_file(filename);\n fmt_ctx = result[0];\n streams = result[1];\n } catch (err) {\n await handle.detach().catch(() => {});\n // Errors thrown across the libav.js worker/pthread boundary aren't\n // always Error instances — they can be plain objects, numbers (errno\n // codes), or strings. Stringify defensively so the user-facing message\n // never has `(undefined)` in it.\n const inner =\n err instanceof Error\n ? err.message\n : typeof err === \"object\" && err !== null\n ? JSON.stringify(err)\n : String(err);\n // eslint-disable-next-line no-console\n console.error(\"[avbridge] ff_init_demuxer_file raw error:\", err);\n throw new Error(\n `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\"})`,\n );\n }\n\n const videoTracks: VideoTrackInfo[] = [];\n const audioTracks: AudioTrackInfo[] = [];\n\n for (const stream of streams) {\n const codecName = (await safe(() => libav.avcodec_get_name(stream.codec_id))) ?? `unknown(${stream.codec_id})`;\n // codecpar holds width/height/channels/sample_rate/profile/level/extradata\n // for the actual stream. We have to copy it out of WASM memory.\n const codecpar = await safe(() => libav.ff_copyout_codecpar(stream.codecpar));\n\n if (stream.codec_type === libav.AVMEDIA_TYPE_VIDEO) {\n videoTracks.push({\n id: stream.index,\n codec: ffmpegToAvbridgeVideo(codecName),\n width: codecpar?.width ?? 0,\n height: codecpar?.height ?? 0,\n fps: framerate(stream),\n });\n } else if (stream.codec_type === libav.AVMEDIA_TYPE_AUDIO) {\n audioTracks.push({\n id: stream.index,\n codec: ffmpegToAvbridgeAudio(codecName),\n channels: codecpar?.channels ?? codecpar?.ch_layout_nb_channels ?? 0,\n sampleRate: codecpar?.sample_rate ?? 0,\n });\n }\n }\n\n // We need this duration but cannot reliably get it from the streams alone\n // for AVI; libav.js exposes it via the AVFormatContext duration helper.\n const duration = await safeDuration(libav, fmt_ctx!);\n\n // Close the demuxer; the strategy will reopen it later if it ends up being\n // chosen. Probing should not pin native resources.\n await libav.avformat_close_input_js(fmt_ctx!).catch(() => {});\n await handle.detach().catch(() => {});\n\n return {\n source: source.original,\n name: source.name,\n byteLength: source.byteLength,\n container: sniffed === \"unknown\" ? \"unknown\" : sniffed,\n videoTracks,\n audioTracks,\n subtitleTracks: [],\n probedBy: \"libav\",\n duration,\n };\n}\n\nfunction framerate(stream: LibavStream): number | undefined {\n if (typeof stream.avg_frame_rate_num === \"number\" && stream.avg_frame_rate_den) {\n return stream.avg_frame_rate_num / stream.avg_frame_rate_den;\n }\n if (stream.avg_frame_rate && typeof stream.avg_frame_rate === \"object\") {\n if (stream.avg_frame_rate.den === 0) return undefined;\n return stream.avg_frame_rate.num / stream.avg_frame_rate.den;\n }\n return undefined;\n}\n\nasync function safeDuration(libav: LibavInstance, fmt_ctx: number): Promise<number | undefined> {\n try {\n // `AVFormatContext.duration` is an int64 in microseconds (AV_TIME_BASE).\n // libav.js exposes it as a split lo/hi pair the same way it does for\n // packet pts — `AVFormatContext_duration(ctx)` returns the low 32 bits,\n // `AVFormatContext_durationhi(ctx)` returns the high 32 bits. Reading\n // only the low half (the previous bug) gave garbage for any file whose\n // duration > ~35 minutes, and zero for shorter files where the value\n // happened to live in the high half.\n const lo = await libav.AVFormatContext_duration?.(fmt_ctx);\n const hi = await libav.AVFormatContext_durationhi?.(fmt_ctx);\n if (typeof lo !== \"number\" || typeof hi !== \"number\") return undefined;\n\n // AV_NOPTS_VALUE = -2^63 → ptshi = -2147483648, pts = 0. Means \"unknown\".\n if (hi === -2147483648 && lo === 0) return undefined;\n\n // Reconstruct the 64-bit value. Prefer libav's helper when available\n // because it correctly handles signed 32-bit two's complement.\n const us =\n typeof libav.i64tof64 === \"function\"\n ? libav.i64tof64(lo, hi)\n : hi * 0x100000000 + lo + (lo < 0 ? 0x100000000 : 0);\n\n if (!Number.isFinite(us) || us <= 0) return undefined;\n return us / 1_000_000;\n } catch {\n return undefined;\n }\n}\n\nasync function safe<T>(fn: () => Promise<T> | T): Promise<T | undefined> {\n try { return await fn(); } catch { return undefined; }\n}\n\n/** Map FFmpeg codec names to avbridge video codec identifiers. */\nfunction ffmpegToAvbridgeVideo(name: string): VideoCodec {\n switch (name) {\n case \"h264\": return \"h264\";\n case \"hevc\": return \"h265\";\n case \"vp8\": return \"vp8\";\n case \"vp9\": return \"vp9\";\n case \"av1\": return \"av1\";\n case \"mpeg4\": return \"mpeg4\"; // MPEG-4 Part 2 / DivX / Xvid\n case \"msmpeg4v1\":\n case \"msmpeg4v2\":\n case \"msmpeg4v3\": // a.k.a. DIV3\n return \"mpeg4\";\n case \"wmv1\":\n case \"wmv2\":\n case \"wmv3\":\n return \"wmv3\";\n case \"vc1\": return \"vc1\";\n case \"mpeg2video\": return \"mpeg2\";\n case \"mpeg1video\": return \"mpeg1\";\n case \"theora\": return \"theora\";\n case \"rv10\": return \"rv10\";\n case \"rv20\": return \"rv20\";\n case \"rv30\": return \"rv30\";\n case \"rv40\": return \"rv40\";\n default: return name as VideoCodec;\n }\n}\n\nfunction ffmpegToAvbridgeAudio(name: string): AudioCodec {\n switch (name) {\n case \"aac\": return \"aac\";\n case \"mp3\":\n case \"mp3float\":\n return \"mp3\";\n case \"opus\": return \"opus\";\n case \"vorbis\": return \"vorbis\";\n case \"flac\": return \"flac\";\n case \"ac3\": return \"ac3\";\n case \"eac3\": return \"eac3\";\n case \"wmav1\":\n case \"wmav2\": return \"wmav2\";\n case \"wmapro\": return \"wmapro\";\n case \"alac\": return \"alac\";\n case \"cook\": return \"cook\";\n case \"ra_144\": return \"ra_144\";\n case \"ra_288\": return \"ra_288\";\n case \"sipr\": return \"sipr\";\n case \"atrac3\": return \"atrac3\";\n default: return name as AudioCodec;\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Minimal structural types for the slice of libav.js we touch.\n// ─────────────────────────────────────────────────────────────────────────────\n\ninterface LibavStream {\n index: number;\n codec_type: number;\n codec_id: number;\n codecpar: number;\n avg_frame_rate?: { num: number; den: number };\n avg_frame_rate_num?: number;\n avg_frame_rate_den?: number;\n}\n\ninterface LibavCodecpar {\n width?: number;\n height?: number;\n channels?: number;\n ch_layout_nb_channels?: number;\n sample_rate?: number;\n profile?: number;\n level?: number;\n}\n\ninterface LibavInstance {\n mkreadaheadfile(name: string, blob: Blob): Promise<void>;\n unlinkreadaheadfile(name: string): Promise<void>;\n ff_init_demuxer_file(name: string): Promise<[number, LibavStream[]]>;\n ff_copyout_codecpar(codecpar: number): Promise<LibavCodecpar>;\n avcodec_get_name(codec_id: number): Promise<string>;\n avformat_close_input_js(ctx: number): Promise<void>;\n AVFormatContext_duration?(ctx: number): Promise<number>;\n AVFormatContext_durationhi?(ctx: number): Promise<number>;\n i64tof64?(lo: number, hi: number): number;\n\n AVMEDIA_TYPE_VIDEO: number;\n AVMEDIA_TYPE_AUDIO: number;\n}\n"]}
|