fix(playback): re-sync embedded MPV bounds on position-only layout shifts (#1476)

An ancestor re-layout can translate the player host without resizing it
(sidebar content settling, EPG panel loading below the player).
ResizeObserver reports size changes only and no DOM event observes
"position changed", so the native child window silently kept its stale
coordinates and rendered offset from the DOM stage.

The session controller now polls the host bounds every 500 ms while a
session is active, compares them against the last synced bounds with a
half-pixel tolerance, and re-syncs only on drift — idle cost is one
getBoundingClientRect per tick with no IPC. The interval is registered
via NgZone.runOutsideAngular and never re-enters the zone, so zone change
detection does not run every tick for the whole stream. Frame-copy
sessions skip the measurement entirely: the canvas is laid out by the DOM
and moves with the layout, so only the native-view child window can go
stale on a position-only shift.

Fixes #1428

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 authored and GitHub committed 2026-08-27 08:35:34 +02:00
1 parent 069b8b3cc9
commit 7a3d5eae56
5 files changed
+346 -5

No files matched your search

+16
View File
@@ -667,6 +667,22 @@ re-armed `matchMedia('(resolution: …dppx)')` query and re-syncs bounds when
it changes; page zoom changes are covered by the same watch plus the ordinary
resize-driven syncs.
An ancestor re-layout can also translate the host **without resizing it** —
the channel sidebar's content settling, panels loading below the player.
`ResizeObserver` reports size changes only and no DOM event observes
"position changed", so before #1428 the native child window silently kept its
stale coordinates and rendered offset from the DOM stage. The session
controller therefore polls the host bounds every 500 ms while a session is
active, compares them against the last synced bounds with a half-pixel
tolerance, and schedules a re-sync only on drift — idle cost is one
`getBoundingClientRect` per tick with no IPC. The interval is registered
outside Angular's zone (a zone timer would run app-wide change detection
every tick for the whole stream) and never re-enters it, because the drift
path is rAF → `setEmbeddedMpvBounds` IPC and touches no Angular state.
Frame-copy skips the measurement entirely: its canvas is laid out by the
DOM and moves with the layout, so only the native-view child window can go
stale on a position-only shift.
### Controls ownership by engine
`EmbeddedMpvPlayerComponent` selects one control owner from