avbridge 2.12.1 → 2.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,107 @@ 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.13.0]
8
+
9
+ Post-seek fast-forward fix on the fallback path, plus seekbar UX
10
+ improvements and a small new API for hosts embedding the player in
11
+ swipe-driven UIs.
12
+
13
+ ### Fixed
14
+
15
+ - **Post-seek "fast-forward" on MPEG-4 ASP + MP3 AVIs** (the headline
16
+ fix). After seeking deep into a long AVI, ~1–2 s of visibly
17
+ fast-forwarded video preceded normal playback. Root cause: the
18
+ fallback decoder's synthetic-PTS counter was reset to the user's
19
+ requested seek time, so the first NOPTS frame post-seek (typically
20
+ the keyframe libav landed on, ~4–8 s before target) got stamped
21
+ with the seek target's PTS. Every libav-pts frame after that was
22
+ REGRESSED-DROP'd because the synthetic anchor was ahead of real
23
+ content. The renderer painted the ~1-in-3 mis-labeled frames at
24
+ audio rate, producing 2.4–3× content rate on screen until the
25
+ synthetic counter caught up. Rewrote the content clock as
26
+ **sync-on-every-valid-pts / step-on-NOPTS** with no fallback to
27
+ seekTarget — labels now match real content within one frame at all
28
+ times. See `docs/dev/POSTMORTEMS.md` (2026-06-01) for the full
29
+ diagnostic arc, including the two epistemic traps that delayed
30
+ triage by hours.
31
+ - **Cold-start opening loses 1–2 frames.** First emitted keyframe at
32
+ `t=0` was being discarded as pre-anchor NOPTS, so the first paint
33
+ landed at content ~80 ms instead of 0. Special-cased
34
+ `seekTarget === 0`: anchor `lastContentUs = 0` on the
35
+ `f.key_frame === 1` frame directly. Strictly branched so it can't
36
+ touch the seek path.
37
+ - **Seekbar pointer events bubble to host gesture recognizers.**
38
+ Touches that start on the player chrome (seek bar, buttons,
39
+ settings menu, overlay play button) now stop propagation on
40
+ `pointerdown` / `pointermove` / `pointerup` / `pointercancel`, so
41
+ a host page that wraps the player in a TikTok-style vertical
42
+ pager won't latch its swipe handler on seek interactions.
43
+ - **Hybrid strategy audio uses packet PTS** instead of the synthetic
44
+ counter, matching the fallback strategy's audio path (see v2.12.2
45
+ refactor). `decodeAudioBatch` now captures `packetPtsSec` before
46
+ decode and routes per-frame; eliminates a class of post-seek
47
+ audio-scheduling drift that affected hybrid playback the same way
48
+ it affected fallback.
49
+ - **Remux pipeline drops stale writes after seek.** Each output's
50
+ `write` callback now captures the pump token at creation time and
51
+ drops any in-flight chunk whose token has been bumped by a
52
+ subsequent seek. Without this, fragments from the pre-seek output
53
+ could append to the SourceBuffer at their original timestamps,
54
+ the deferred seek would apply against the wrong buffered range,
55
+ and playback would snap to the end of the stale range.
56
+
57
+ ### Added
58
+
59
+ - **`AvbridgePlayerElement.isPlayerChromeEvent(event)`** static
60
+ helper. Returns `true` if a DOM event originated from the player's
61
+ interactive chrome (works across the shadow boundary via
62
+ `composedPath()`). Use this in **capture-phase** gesture
63
+ recognizers — capture-phase listeners run before the player's own
64
+ handlers can stop propagation, so they need to check the path
65
+ themselves. See README → `<avbridge-player>` → "Embedding inside a
66
+ swipe gesture".
67
+
68
+ ### Changed
69
+
70
+ - **Seekbar drag model** switches on `(pointer: coarse)` instead of
71
+ bar width. Touch devices (coarse) now get **relative-drag
72
+ scrubbing** — initial tap doesn't move the thumb, finger Δx maps to
73
+ Δt added to the time at pointerdown. Mouse/trackpad (fine) keep
74
+ **absolute-position** seeking — thumb jumps under the cursor on
75
+ tap, follows pointer on drag. Both modes commit live during drag
76
+ (throttled to ~4 Hz so the decoder pump isn't overwhelmed) and
77
+ once more on pointerup. Removes the 400 px `SCRUB_WIDTH_THRESHOLD`
78
+ — width was the wrong signal for distinguishing
79
+ precise-pointer-input vs imprecise-finger-input.
80
+ - **Diagnostic logging gated behind `globalThis.AVBRIDGE_DEBUG`.**
81
+ Per-packet (`[DIAG-PKT]`), per-frame (`[DIAG-FRAME]`), per-paint
82
+ (`[TRACE] PAINT`), and per-audio-chunk (`[TRACE-AUD]`) trace lines
83
+ are silent in normal use. Degraded-state warnings
84
+ (`first valid raw pts ≥ seek target`, `LEGACY (no PTS)`,
85
+ `REBASE anchor`) stay unconditional — those signal real problems.
86
+ - **Pre-roll re-enabled** with a structural safety guarantee. The
87
+ renderer paints at most ONE frame (the head of the queue, held
88
+ static) during the post-flush gate-wait window. Because the
89
+ decoder fix discards pre-target frames before they reach the
90
+ renderer queue, the pre-roll frame is now guaranteed to be a
91
+ near-target frame — the previous regression artifact (painting the
92
+ keyframe-to-target preroll stream) is structurally impossible.
93
+
94
+ ### Internal
95
+
96
+ - Replaced the fallback decoder's `sanitizeFrameTimestamp` call-site
97
+ with an inline anchor-and-step block that never lies about content
98
+ position. Synthetic counter no longer initialized to anything
99
+ derived from the user's click — unanchored on seek, established at
100
+ the first valid libav pts, extended by `frameStep` on NOPTS,
101
+ re-synced to truth on every valid pts.
102
+ - Removed dead debug scaffolding from `video-renderer.ts`:
103
+ `BREAK_AT_PAINT_AFTER_FLUSH` constant, `paintsSinceFlush` counter,
104
+ unused `paintHistory` ring-buffer field, the `debugger` statement
105
+ that would trap users on paint #10 post-seek when
106
+ `AVBRIDGE_DEBUG=true`.
107
+
7
108
  ## [2.12.1]
8
109
 
9
110
  DivX/Xvid AVI playback reliability — four latent bugs fixed. Content
package/README.md CHANGED
@@ -431,6 +431,39 @@ attribute mirroring the controls auto-hide state — useful if slotted
431
431
  buttons need to drive JS behavior (focus, announcements) in sync with
432
432
  the fade, not just CSS opacity.
433
433
 
434
+ #### Embedding inside a swipe gesture (TikTok-style pagers, etc.)
435
+
436
+ When `<avbridge-player>` is nested inside a host UI that recognizes
437
+ swipe gestures (vertical pager, drawer, carousel), pointer events
438
+ that start on the player chrome — seek bar, buttons, settings menu,
439
+ overlay play button — should NOT also latch the host's gesture
440
+ recognizer. The player handles this in two layers:
441
+
442
+ **Bubble-phase listeners** (the default) need no action on your
443
+ side. The player calls `stopPropagation()` on `pointerdown`,
444
+ `pointermove`, `pointerup`, and `pointercancel` for chrome
445
+ interactions, so they never bubble out to the host.
446
+
447
+ **Capture-phase listeners** (`{ capture: true }`) run *before* the
448
+ player's handlers, so `stopPropagation()` can't help. Check the
449
+ event against the static helper instead:
450
+
451
+ ```ts
452
+ import { AvbridgePlayerElement } from "avbridge/player-element";
453
+
454
+ document.addEventListener("pointerdown", (e) => {
455
+ if (AvbridgePlayerElement.isPlayerChromeEvent(e)) return;
456
+ startSwipeGesture(e);
457
+ }, { capture: true });
458
+ ```
459
+
460
+ `isPlayerChromeEvent(event)` returns `true` only for events whose
461
+ `composedPath()` includes the player's interactive chrome (works
462
+ across the shadow boundary). Events on the bare video surface return
463
+ `false` — the host page remains free to claim those for its own
464
+ gestures (e.g. swipe-to-next-video). Events that didn't hit a player
465
+ at all return `false`.
466
+
434
467
  This is a second tsup entry (`dist/element-browser.js`) that inlines
435
468
  mediabunny + libavjs-webcodecs-bridge into a single ~1.3 MB file with
436
469
  zero bare specifiers at runtime. Perfect for self-hosted tools or static