avbridge 2.7.0 → 2.8.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 CHANGED
@@ -4,6 +4,93 @@ All notable changes to **avbridge.js** 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.8.1]
8
+
9
+ Cross-browser playback validation — second slice of the v2.7.0 Playwright
10
+ tier. Bootstrap → play → destroy lifecycle tested across Chromium,
11
+ Firefox, and WebKit. Surfaced and fixed three real bugs along the way:
12
+
13
+ ### Fixed
14
+
15
+ - **`<avbridge-player>` constructor violated the Custom Elements spec**
16
+ by setting attributes (`tabindex`, `data-toolbar-empty`) on `this`.
17
+ Browsers allow this when the parser constructs the element, but
18
+ `document.createElement("avbridge-player")` fails with "The result
19
+ must not have attributes" — so programmatic creation, including all
20
+ Playwright tests, was broken. Moved attribute writes to
21
+ `connectedCallback`. Caught by the new browser matrix.
22
+ - **classify() didn't feature-detect MSE for remuxable non-native
23
+ containers.** On open-source Chromium (no proprietary codecs),
24
+ MKV+HEVC was classified as `remux` even though MSE rejected the
25
+ target mime — playback stalled silently. classify() now calls
26
+ `MediaSource.isTypeSupported()` for the remux target and gracefully
27
+ degrades to hybrid/fallback when it says no.
28
+ - **Fallback strategy was loading the wrong libav variant for some
29
+ codecs.** `pickLibavVariant` would choose the "webcodecs" companion
30
+ variant (smaller, assumes the browser decodes natively) for codecs
31
+ like HEVC — but fallback does *full* software decode, and that
32
+ variant lacks HEVC's software decoder. Fallback now unconditionally
33
+ loads the "avbridge" variant, which is correct since we're there
34
+ specifically because the browser can't decode.
35
+
36
+ ### Added
37
+
38
+ - **`tests/browser/playback.spec.ts`** — bootstrap → play → destroy per
39
+ fixture per browser. Asserts playback actually advances (either via
40
+ audio-clock `currentTime` for native/remux, or `framesPainted` for
41
+ canvas strategies) and that the runtime strategy after escalation
42
+ matches the per-browser matrix. 29 passed, 1 skipped (documented).
43
+ - **`playbackStrategy` field on `_expectations.ts`** for codifying
44
+ per-browser runtime expectations distinct from initial classify
45
+ output. Firefox HEVC is the one skip, pending a decode-stall
46
+ detection follow-up.
47
+
48
+ ### Known deferred
49
+
50
+ - **Firefox HEVC**: MSE optimistically reports `hev1.*` supported but
51
+ the decoder can't decode it. Audio plays, video is black. No signal
52
+ currently reaches escalation. Runtime decode-stall detection
53
+ (buffered but `currentTime` not advancing) is the right fix; tracked
54
+ as a follow-up, skipped in the matrix for now.
55
+
56
+ ## [2.8.0]
57
+
58
+ Element feature release — fit mode, consumer-slotted toolbar chrome, and
59
+ orientation-aware fullscreen.
60
+
61
+ ### Added
62
+
63
+ - **`fit` attribute on `<avbridge-video>`.** `fit="contain|cover|fill"`
64
+ (also reflected as the `fit` property, with a `fitchange` event) maps to
65
+ `object-fit` on the inner `<video>` and the fallback canvas via a new
66
+ `--avbridge-fit` CSS custom property on the stage wrapper. Default
67
+ `contain`; invalid values fall back to `contain`. Proxied through
68
+ `<avbridge-player>`.
69
+ - **Top toolbar slots on `<avbridge-player>`.** `<slot name="top-left">`
70
+ and `<slot name="top-right">` inside a new `part="toolbar-top"`
71
+ wrapper let consumers place back / title / translate buttons inside the
72
+ auto-hide chrome so they fade together with the bottom controls. A
73
+ `slotchange` listener toggles a `data-toolbar-empty` host attribute so
74
+ the gradient band disappears when no content is slotted. Click,
75
+ double-click, and tap handlers ignore events originating from slotted
76
+ content via `composedPath()` — so consumer buttons don't trigger
77
+ play/pause or seek.
78
+ - **Orientation-aware fullscreen on `<avbridge-video>`.** On fullscreen
79
+ entry (including fullscreen applied to an ancestor like
80
+ `<avbridge-player>` whose shadow DOM hosts the video), the element
81
+ derives the target orientation from the video's intrinsic
82
+ `videoWidth`/`videoHeight` (already SAR-corrected by the browser) and
83
+ calls `screen.orientation.lock('landscape'|'portrait')`. Releases the
84
+ lock on exit. iOS Safari rejections are swallowed (iOS handles rotation
85
+ via `webkitEnterFullscreen` on `<video>` itself). Opt out per element
86
+ with the `no-orientation-lock` attribute / `noOrientationLock` property.
87
+
88
+ ### Notes
89
+
90
+ - `<avbridge-player>` is no longer a "reserved" name — it's the
91
+ shipping chrome-bearing player. `<avbridge-video>` remains the bare
92
+ HTMLMediaElement-compatible primitive.
93
+
7
94
  ## [2.7.0]
8
95
 
9
96
  Cross-browser confidence release. A Playwright-based test tier now
package/README.md CHANGED
@@ -329,6 +329,29 @@ automatically via `import.meta.url` in the generated chunk.
329
329
  <avbridge-video src="/video.mkv" autoplay playsinline></avbridge-video>
330
330
  ```
331
331
 
332
+ **Two elements ship.** `<avbridge-video>` is the bare
333
+ `HTMLMediaElement`-compatible primitive with zero UI; `<avbridge-player>`
334
+ (from `avbridge/player-element`) wraps it with YouTube-style chrome.
335
+ Both support:
336
+
337
+ - `fit="contain|cover|fill"` — how the video fills the element's box
338
+ (maps to `object-fit`; default `contain`). Fires a `fitchange` event.
339
+ - `no-orientation-lock` — opt out of the default behavior that locks
340
+ `screen.orientation` to the video's intrinsic aspect on fullscreen
341
+ entry (landscape video → landscape, portrait video → portrait). Safe
342
+ on iOS / desktop — the lock call is swallowed where unsupported.
343
+
344
+ `<avbridge-player>` also exposes `top-left` and `top-right` slots
345
+ inside its auto-hiding top chrome for consumer buttons (back, title,
346
+ translate, etc.):
347
+
348
+ ```html
349
+ <avbridge-player src="/video.mkv" fit="cover">
350
+ <button slot="top-left">← Back</button>
351
+ <button slot="top-right">Translate</button>
352
+ </avbridge-player>
353
+ ```
354
+
332
355
  This is a second tsup entry (`dist/element-browser.js`) that inlines
333
356
  mediabunny + libavjs-webcodecs-bridge into a single ~1.3 MB file with
334
357
  zero bare specifiers at runtime. Perfect for self-hosted tools or static
@@ -217,6 +217,22 @@ function classifyContext(ctx) {
217
217
  };
218
218
  }
219
219
  if (REMUXABLE_CONTAINERS.has(ctx.container)) {
220
+ const mime = mp4MimeFor(video, audio);
221
+ if (mime && typeof MediaSource !== "undefined" && !mseSupports(mime)) {
222
+ if (webCodecsAvailable()) {
223
+ return {
224
+ class: "HYBRID_CANDIDATE",
225
+ strategy: "hybrid",
226
+ reason: `${ctx.container} container with ${video.codec}${audio ? "/" + audio.codec : ""}; MSE rejects the remux target mime \u2014 routing to WebCodecs hardware decode`,
227
+ fallbackChain: ["fallback"]
228
+ };
229
+ }
230
+ return {
231
+ class: "FALLBACK_REQUIRED",
232
+ strategy: "fallback",
233
+ reason: `${ctx.container} container with ${video.codec}${audio ? "/" + audio.codec : ""}; MSE rejects the remux target mime and WebCodecs is unavailable \u2014 falling back to WASM decode`
234
+ };
235
+ }
220
236
  return {
221
237
  class: "REMUX_CANDIDATE",
222
238
  strategy: "remux",
@@ -1008,7 +1024,7 @@ var VideoRenderer = class {
1008
1024
  this.resolveFirstFrame = resolve;
1009
1025
  });
1010
1026
  this.canvas = document.createElement("canvas");
1011
- this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;object-fit:contain;";
1027
+ this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;object-fit:var(--avbridge-fit, contain);";
1012
1028
  const parent = target.parentElement ?? target.parentNode;
1013
1029
  if (parent && parent instanceof HTMLElement) {
1014
1030
  if (getComputedStyle(parent).position === "static") {
@@ -2182,7 +2198,7 @@ async function createHybridSession(ctx, target, transport) {
2182
2198
 
2183
2199
  // src/strategies/fallback/decoder.ts
2184
2200
  async function startDecoder(opts) {
2185
- const variant = chunkF3LQJKXK_cjs.pickLibavVariant(opts.context);
2201
+ const variant = "avbridge";
2186
2202
  const libav = await chunkG4APZMCP_cjs.loadLibav(variant);
2187
2203
  const bridge = await loadBridge2();
2188
2204
  const { prepareLibavInput } = await import('./libav-http-reader-AZLE7YFS.cjs');
@@ -2238,9 +2254,8 @@ async function startDecoder(opts) {
2238
2254
  videoStream ? `video: ${opts.context.videoTracks[0]?.codec ?? "unknown"}` : null,
2239
2255
  audioStream ? `audio: ${opts.context.audioTracks[0]?.codec ?? "unknown"}` : null
2240
2256
  ].filter(Boolean).join(", ");
2241
- const hint = variant === "webcodecs" ? ` The "${variant}" libav variant does not include software decoders for these codecs. Try the custom "avbridge" variant (scripts/build-libav.sh) for broader codec support, or use a lighter strategy (native, remux, hybrid) instead.` : "";
2242
2257
  throw new Error(
2243
- `fallback decoder: could not initialize any libav decoders (${codecs}).${hint}`
2258
+ `fallback decoder: could not initialize any libav decoders (${codecs}). The "${variant}" libav variant lacks software decoders for these codecs \u2014 rebuild with scripts/build-libav.sh including the missing decoder, or use a lighter strategy (native, remux, hybrid) instead.`
2244
2259
  );
2245
2260
  }
2246
2261
  let bsfCtx = null;
@@ -3366,5 +3381,5 @@ exports.NATIVE_VIDEO_CODECS = NATIVE_VIDEO_CODECS;
3366
3381
  exports.UnifiedPlayer = UnifiedPlayer;
3367
3382
  exports.classifyContext = classifyContext;
3368
3383
  exports.createPlayer = createPlayer;
3369
- //# sourceMappingURL=chunk-6SOFJV44.cjs.map
3370
- //# sourceMappingURL=chunk-6SOFJV44.cjs.map
3384
+ //# sourceMappingURL=chunk-IUSFLVLJ.cjs.map
3385
+ //# sourceMappingURL=chunk-IUSFLVLJ.cjs.map