avbridge 2.2.0 → 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 +45 -0
- package/README.md +98 -71
- package/dist/{chunk-C5VA5U5O.js → chunk-DMWARSEF.js} +76 -21
- package/dist/chunk-DMWARSEF.js.map +1 -0
- package/dist/{chunk-OE66B34H.cjs → chunk-UF2N5L63.cjs} +76 -21
- package/dist/chunk-UF2N5L63.cjs.map +1 -0
- package/dist/element-browser.js +74 -19
- package/dist/element-browser.js.map +1 -1
- package/dist/element.cjs +2 -2
- package/dist/element.d.cts +1 -1
- package/dist/element.d.ts +1 -1
- package/dist/element.js +1 -1
- package/dist/index.cjs +14 -14
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/{player-DUyvltvy.d.cts → player-U2NPmFvA.d.cts} +1 -0
- package/dist/{player-DUyvltvy.d.ts → player-U2NPmFvA.d.ts} +1 -0
- package/package.json +1 -1
- package/src/player.ts +24 -16
- package/src/strategies/fallback/index.ts +8 -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/dist/chunk-C5VA5U5O.js.map +0 -1
- package/dist/chunk-OE66B34H.cjs.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,51 @@ 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
|
+
|
|
7
52
|
## [2.2.0]
|
|
8
53
|
|
|
9
54
|
### Added
|
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
|
|
|
@@ -1070,6 +1070,10 @@ async function createRemuxPipeline(ctx, video) {
|
|
|
1070
1070
|
console.error("[avbridge] remux pipeline reseek failed:", err);
|
|
1071
1071
|
});
|
|
1072
1072
|
},
|
|
1073
|
+
setAutoPlay(autoPlay) {
|
|
1074
|
+
pendingAutoPlay = autoPlay;
|
|
1075
|
+
if (sink) sink.setPlayOnSeek(autoPlay);
|
|
1076
|
+
},
|
|
1073
1077
|
async destroy() {
|
|
1074
1078
|
destroyed = true;
|
|
1075
1079
|
pumpToken++;
|
|
@@ -1110,7 +1114,11 @@ async function createRemuxSession(context, video) {
|
|
|
1110
1114
|
await pipeline.start(video.currentTime || 0, true);
|
|
1111
1115
|
return;
|
|
1112
1116
|
}
|
|
1113
|
-
|
|
1117
|
+
pipeline.setAutoPlay(true);
|
|
1118
|
+
try {
|
|
1119
|
+
await video.play();
|
|
1120
|
+
} catch {
|
|
1121
|
+
}
|
|
1114
1122
|
},
|
|
1115
1123
|
pause() {
|
|
1116
1124
|
wantPlay = false;
|
|
@@ -1158,7 +1166,7 @@ var VideoRenderer = class {
|
|
|
1158
1166
|
this.resolveFirstFrame = resolve;
|
|
1159
1167
|
});
|
|
1160
1168
|
this.canvas = document.createElement("canvas");
|
|
1161
|
-
this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;";
|
|
1169
|
+
this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;object-fit:contain;";
|
|
1162
1170
|
const parent = target.parentElement ?? target.parentNode;
|
|
1163
1171
|
if (parent && parent instanceof HTMLElement) {
|
|
1164
1172
|
if (getComputedStyle(parent).position === "static") {
|
|
@@ -1400,9 +1408,13 @@ var AudioOutput = class {
|
|
|
1400
1408
|
const node = this.ctx.createBufferSource();
|
|
1401
1409
|
node.buffer = buffer;
|
|
1402
1410
|
node.connect(this.gain);
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1411
|
+
let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor);
|
|
1412
|
+
if (ctxStart < this.ctx.currentTime) {
|
|
1413
|
+
this.ctxTimeAtAnchor = this.ctx.currentTime;
|
|
1414
|
+
this.mediaTimeOfAnchor = this.mediaTimeOfNext;
|
|
1415
|
+
ctxStart = this.ctx.currentTime;
|
|
1416
|
+
}
|
|
1417
|
+
node.start(ctxStart);
|
|
1406
1418
|
this.mediaTimeOfNext += frameCount / sampleRate;
|
|
1407
1419
|
this.framesScheduled++;
|
|
1408
1420
|
}
|
|
@@ -1970,6 +1982,10 @@ async function createHybridSession(ctx, target) {
|
|
|
1970
1982
|
void doSeek(v);
|
|
1971
1983
|
}
|
|
1972
1984
|
});
|
|
1985
|
+
Object.defineProperty(target, "paused", {
|
|
1986
|
+
configurable: true,
|
|
1987
|
+
get: () => !audio.isPlaying()
|
|
1988
|
+
});
|
|
1973
1989
|
if (ctx.duration && Number.isFinite(ctx.duration)) {
|
|
1974
1990
|
Object.defineProperty(target, "duration", {
|
|
1975
1991
|
configurable: true,
|
|
@@ -2034,6 +2050,7 @@ async function createHybridSession(ctx, target) {
|
|
|
2034
2050
|
try {
|
|
2035
2051
|
delete target.currentTime;
|
|
2036
2052
|
delete target.duration;
|
|
2053
|
+
delete target.paused;
|
|
2037
2054
|
} catch {
|
|
2038
2055
|
}
|
|
2039
2056
|
},
|
|
@@ -2113,7 +2130,8 @@ async function startDecoder(opts) {
|
|
|
2113
2130
|
let audioFramesDecoded = 0;
|
|
2114
2131
|
let watchdogFirstFrameMs = 0;
|
|
2115
2132
|
let watchdogSlowSinceMs = 0;
|
|
2116
|
-
let
|
|
2133
|
+
let watchdogSlowWarned = false;
|
|
2134
|
+
let watchdogOverflowWarned = false;
|
|
2117
2135
|
let syntheticVideoUs = 0;
|
|
2118
2136
|
let syntheticAudioUs = 0;
|
|
2119
2137
|
const videoTrackInfo = opts.context.videoTracks.find((t) => t.id === videoStream?.index);
|
|
@@ -2125,7 +2143,7 @@ async function startDecoder(opts) {
|
|
|
2125
2143
|
let packets;
|
|
2126
2144
|
try {
|
|
2127
2145
|
[readErr, packets] = await libav.ff_read_frame_multi(fmt_ctx, readPkt, {
|
|
2128
|
-
limit:
|
|
2146
|
+
limit: 16 * 1024
|
|
2129
2147
|
});
|
|
2130
2148
|
} catch (err) {
|
|
2131
2149
|
console.error("[avbridge] ff_read_frame_multi failed:", err);
|
|
@@ -2134,26 +2152,26 @@ async function startDecoder(opts) {
|
|
|
2134
2152
|
if (myToken !== pumpToken || destroyed) return;
|
|
2135
2153
|
const videoPackets = videoStream ? packets[videoStream.index] : void 0;
|
|
2136
2154
|
const audioPackets = audioStream ? packets[audioStream.index] : void 0;
|
|
2137
|
-
if (videoDec && videoPackets && videoPackets.length > 0) {
|
|
2138
|
-
await decodeVideoBatch(videoPackets, myToken);
|
|
2139
|
-
}
|
|
2140
|
-
if (myToken !== pumpToken || destroyed) return;
|
|
2141
2155
|
if (audioDec && audioPackets && audioPackets.length > 0) {
|
|
2142
2156
|
await decodeAudioBatch(audioPackets, myToken);
|
|
2143
2157
|
}
|
|
2158
|
+
if (myToken !== pumpToken || destroyed) return;
|
|
2159
|
+
if (videoDec && videoPackets && videoPackets.length > 0) {
|
|
2160
|
+
await decodeVideoBatch(videoPackets, myToken);
|
|
2161
|
+
}
|
|
2144
2162
|
packetsRead += (videoPackets?.length ?? 0) + (audioPackets?.length ?? 0);
|
|
2145
2163
|
if (videoFramesDecoded > 0) {
|
|
2146
2164
|
if (watchdogFirstFrameMs === 0) {
|
|
2147
2165
|
watchdogFirstFrameMs = performance.now();
|
|
2148
2166
|
}
|
|
2149
2167
|
const elapsedSinceFirst = (performance.now() - watchdogFirstFrameMs) / 1e3;
|
|
2150
|
-
if (elapsedSinceFirst > 1 && !
|
|
2168
|
+
if (elapsedSinceFirst > 1 && !watchdogSlowWarned) {
|
|
2151
2169
|
const expectedFrames = elapsedSinceFirst * videoFps;
|
|
2152
2170
|
const ratio = videoFramesDecoded / expectedFrames;
|
|
2153
2171
|
if (ratio < 0.6) {
|
|
2154
2172
|
if (watchdogSlowSinceMs === 0) watchdogSlowSinceMs = performance.now();
|
|
2155
2173
|
if ((performance.now() - watchdogSlowSinceMs) / 1e3 > 5) {
|
|
2156
|
-
|
|
2174
|
+
watchdogSlowWarned = true;
|
|
2157
2175
|
console.warn(
|
|
2158
2176
|
"[avbridge:decode-rate]",
|
|
2159
2177
|
`decoder is running slower than realtime: ${videoFramesDecoded} frames in ${elapsedSinceFirst.toFixed(1)}s (${(videoFramesDecoded / elapsedSinceFirst).toFixed(1)} fps vs ${videoFps} fps source \u2014 ${(ratio * 100).toFixed(0)}% of realtime). Playback will stutter. Typical causes: software decode of a codec with no WebCodecs support (rv40, mpeg4 @ 720p+, wmv3), or a resolution too large for single-threaded WASM to keep up with.`
|
|
@@ -2163,6 +2181,17 @@ async function startDecoder(opts) {
|
|
|
2163
2181
|
watchdogSlowSinceMs = 0;
|
|
2164
2182
|
}
|
|
2165
2183
|
}
|
|
2184
|
+
if (!watchdogOverflowWarned && videoFramesDecoded > 100) {
|
|
2185
|
+
const rendererStats = opts.renderer.stats();
|
|
2186
|
+
const overflow = rendererStats.framesDroppedOverflow ?? 0;
|
|
2187
|
+
if (overflow / videoFramesDecoded > 0.1) {
|
|
2188
|
+
watchdogOverflowWarned = true;
|
|
2189
|
+
console.warn(
|
|
2190
|
+
"[avbridge:overflow-drop]",
|
|
2191
|
+
`renderer is dropping ${overflow}/${videoFramesDecoded} frames (${(overflow / videoFramesDecoded * 100).toFixed(0)}%) because the decoder is producing bursts faster than the canvas can drain. Symptom: choppy playback despite decoder keeping up on average. Fix would be smaller read batches in the pump loop or a lower queueHighWater cap \u2014 see src/strategies/fallback/decoder.ts.`
|
|
2192
|
+
);
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2166
2195
|
}
|
|
2167
2196
|
while (!destroyed && myToken === pumpToken && (opts.audio.bufferAhead() > 2 || opts.renderer.queueDepth() >= opts.renderer.queueHighWater)) {
|
|
2168
2197
|
await new Promise((r) => setTimeout(r, 50));
|
|
@@ -2519,6 +2548,10 @@ async function createFallbackSession(ctx, target) {
|
|
|
2519
2548
|
void doSeek(v);
|
|
2520
2549
|
}
|
|
2521
2550
|
});
|
|
2551
|
+
Object.defineProperty(target, "paused", {
|
|
2552
|
+
configurable: true,
|
|
2553
|
+
get: () => !audio.isPlaying()
|
|
2554
|
+
});
|
|
2522
2555
|
if (ctx.duration && Number.isFinite(ctx.duration)) {
|
|
2523
2556
|
Object.defineProperty(target, "duration", {
|
|
2524
2557
|
configurable: true,
|
|
@@ -2527,25 +2560,35 @@ async function createFallbackSession(ctx, target) {
|
|
|
2527
2560
|
}
|
|
2528
2561
|
async function waitForBuffer() {
|
|
2529
2562
|
const start = performance.now();
|
|
2563
|
+
let firstFrameAtMs = 0;
|
|
2530
2564
|
dbg.info(
|
|
2531
2565
|
"cold-start",
|
|
2532
|
-
`gate entry:
|
|
2566
|
+
`gate entry: want audio \u2265 ${READY_AUDIO_BUFFER_SECONDS2 * 1e3}ms + 1 frame`
|
|
2533
2567
|
);
|
|
2534
2568
|
while (true) {
|
|
2535
2569
|
const audioAhead = audio.isNoAudio() ? Infinity : audio.bufferAhead();
|
|
2536
2570
|
const audioReady = audio.isNoAudio() || audioAhead >= READY_AUDIO_BUFFER_SECONDS2;
|
|
2537
2571
|
const hasFrames = renderer.hasFrames();
|
|
2572
|
+
const nowMs = performance.now();
|
|
2573
|
+
if (hasFrames && firstFrameAtMs === 0) firstFrameAtMs = nowMs;
|
|
2538
2574
|
if (audioReady && hasFrames) {
|
|
2539
2575
|
dbg.info(
|
|
2540
2576
|
"cold-start",
|
|
2541
|
-
`gate satisfied in ${(
|
|
2577
|
+
`gate satisfied in ${(nowMs - start).toFixed(0)}ms (audio=${(audioAhead * 1e3).toFixed(0)}ms, frames=${renderer.queueDepth()})`
|
|
2542
2578
|
);
|
|
2543
2579
|
return;
|
|
2544
2580
|
}
|
|
2545
|
-
if (
|
|
2581
|
+
if (hasFrames && firstFrameAtMs > 0 && nowMs - firstFrameAtMs >= 500) {
|
|
2582
|
+
dbg.info(
|
|
2583
|
+
"cold-start",
|
|
2584
|
+
`gate released on video-only grace at ${(nowMs - start).toFixed(0)}ms (frames=${renderer.queueDepth()}, audio=${(audioAhead * 1e3).toFixed(0)}ms \u2014 demuxer hasn't delivered audio packets yet, starting anyway and letting the audio scheduler catch up at its media-time anchor)`
|
|
2585
|
+
);
|
|
2586
|
+
return;
|
|
2587
|
+
}
|
|
2588
|
+
if ((nowMs - start) / 1e3 > READY_TIMEOUT_SECONDS2) {
|
|
2546
2589
|
dbg.diag(
|
|
2547
2590
|
"cold-start",
|
|
2548
|
-
`gate TIMEOUT after ${READY_TIMEOUT_SECONDS2}s \u2014 audio=${(audioAhead * 1e3).toFixed(0)}ms (needed ${READY_AUDIO_BUFFER_SECONDS2 * 1e3}ms), frames=${renderer.queueDepth()} (needed \u22651).
|
|
2591
|
+
`gate TIMEOUT after ${READY_TIMEOUT_SECONDS2}s \u2014 audio=${(audioAhead * 1e3).toFixed(0)}ms (needed ${READY_AUDIO_BUFFER_SECONDS2 * 1e3}ms), frames=${renderer.queueDepth()} (needed \u22651). Decoder produced nothing in ${READY_TIMEOUT_SECONDS2}s \u2014 either a corrupt source, a missing codec, or WASM is catastrophically slow on this file. Check getDiagnostics().runtime for decode counters.`
|
|
2549
2592
|
);
|
|
2550
2593
|
return;
|
|
2551
2594
|
}
|
|
@@ -2594,6 +2637,7 @@ async function createFallbackSession(ctx, target) {
|
|
|
2594
2637
|
try {
|
|
2595
2638
|
delete target.currentTime;
|
|
2596
2639
|
delete target.duration;
|
|
2640
|
+
delete target.paused;
|
|
2597
2641
|
} catch {
|
|
2598
2642
|
}
|
|
2599
2643
|
},
|
|
@@ -2736,6 +2780,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2736
2780
|
lastProgressTime = 0;
|
|
2737
2781
|
lastProgressPosition = -1;
|
|
2738
2782
|
errorListener = null;
|
|
2783
|
+
// Bound so we can removeEventListener in destroy(); without this the
|
|
2784
|
+
// listener outlives the player and accumulates on elements that swap
|
|
2785
|
+
// source (e.g. <avbridge-video>).
|
|
2786
|
+
endedListener = null;
|
|
2739
2787
|
// Serializes escalation / setStrategy calls
|
|
2740
2788
|
switchingPromise = Promise.resolve();
|
|
2741
2789
|
// Owns blob URLs created during sidecar discovery + SRT->VTT conversion.
|
|
@@ -2821,7 +2869,8 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
2821
2869
|
subtitle: ctx.subtitleTracks
|
|
2822
2870
|
});
|
|
2823
2871
|
this.startTimeupdateLoop();
|
|
2824
|
-
this.
|
|
2872
|
+
this.endedListener = () => this.emitter.emit("ended", void 0);
|
|
2873
|
+
this.options.target.addEventListener("ended", this.endedListener);
|
|
2825
2874
|
this.emitter.emitSticky("ready", void 0);
|
|
2826
2875
|
const bootstrapElapsed = performance.now() - bootstrapStart;
|
|
2827
2876
|
dbg.info("bootstrap", `ready in ${bootstrapElapsed.toFixed(0)}ms`);
|
|
@@ -3107,6 +3156,10 @@ var UnifiedPlayer = class _UnifiedPlayer {
|
|
|
3107
3156
|
this.timeupdateInterval = null;
|
|
3108
3157
|
}
|
|
3109
3158
|
this.clearSupervisor();
|
|
3159
|
+
if (this.endedListener) {
|
|
3160
|
+
this.options.target.removeEventListener("ended", this.endedListener);
|
|
3161
|
+
this.endedListener = null;
|
|
3162
|
+
}
|
|
3110
3163
|
if (this.session) {
|
|
3111
3164
|
await this.session.destroy();
|
|
3112
3165
|
this.session = null;
|
|
@@ -3121,11 +3174,13 @@ async function createPlayer(options) {
|
|
|
3121
3174
|
function buildInitialDecision(initial, ctx) {
|
|
3122
3175
|
const natural = classifyContext(ctx);
|
|
3123
3176
|
const cls = strategyToClass(initial, natural);
|
|
3177
|
+
const inherited = natural.fallbackChain ?? defaultFallbackChain(initial);
|
|
3178
|
+
const fallbackChain = inherited.filter((s) => s !== initial);
|
|
3124
3179
|
return {
|
|
3125
3180
|
class: cls,
|
|
3126
3181
|
strategy: initial,
|
|
3127
3182
|
reason: `initial strategy "${initial}" requested via options.initialStrategy`,
|
|
3128
|
-
fallbackChain
|
|
3183
|
+
fallbackChain
|
|
3129
3184
|
};
|
|
3130
3185
|
}
|
|
3131
3186
|
function strategyToClass(strategy, natural) {
|
|
@@ -3155,5 +3210,5 @@ function defaultFallbackChain(strategy) {
|
|
|
3155
3210
|
}
|
|
3156
3211
|
|
|
3157
3212
|
export { UnifiedPlayer, avbridgeAudioToMediabunny, avbridgeVideoToMediabunny, buildMediabunnySourceFromInput, classifyContext, createPlayer, probe, srtToVtt };
|
|
3158
|
-
//# sourceMappingURL=chunk-
|
|
3159
|
-
//# sourceMappingURL=chunk-
|
|
3213
|
+
//# sourceMappingURL=chunk-DMWARSEF.js.map
|
|
3214
|
+
//# sourceMappingURL=chunk-DMWARSEF.js.map
|