You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit f649751
Browse filesBrowse the repository at this point in the historyBrowse files
fix(vlc): converge VLC video placement below the ticker strip (v0.87.2) (#200)
v0.87.1's single-shot wmctrl correction mis-landed on the live karaoke
window (y=160 instead of 80): VLC nudges/resizes its own window while a 4K
file loads, so the one 0.4s-later measurement read a stale position and
the lone correction was wrong. Replace it with a short closed loop —
request, settle, measure, adjust the next request by the observed error —
that converges to (0, margin) within a 2px tolerance, and soft-warns
(never falsely claims success) if it can't settle. Validated end-to-end on
NomadPC: VLC video settles at 1920x1000+0+80, matching mpv.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/ARCHITECTURE.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -298,7 +298,7 @@ The overlay system uses a three-component architecture: (1) the KJ Controller we
298
298
299
299
**Layers:** `desktop/rotation_source.py` (pure stdlib) parses `/tmp/rotation_cache.json` into structured data; `desktop/overlay_painters.py` (pure pycairo, no GTK — headless-testable) holds one painter per overlay type; `desktop/overlay_engine.py` (the only `gi`/GTK module) owns the window + render loop and a gi-free `--render-png` mode for headless/on-device visual checks. Communication with the Flask backend is via `data/overlays.json` polled by mtime every ~1 second. Six overlay types are supported: `rotation_list` (the between-songs home screen — heading, stats, singer list with status badges/paid hearts, page cycling), `ticker` (scrolling bar; `source='rotation'` composes the "up next" text directly from the rotation cache; loops seamlessly — a configurable `loop_separator` glyph, default `♪`, is appended to form a repeating unit that tiles back-to-back so there's no blank gap after the last singer), `static_text`, `image`, `countdown`, and `qr_code`. Each overlay has an independent `show_over_video` flag — when false it is hidden during karaoke playback (e.g. the rotation list) and shown when playback stops. The `karaoke_playing` state is set by the play/control routes and a `MpvManager.on_karaoke_end` callback.
300
300
301
-
**Partial redraw + reserved top strip (4K frame-drop fix, 2026-07):** the render loop invalidates only each **animated overlay's own bounding box** (`queue_draw_area(*painter.bbox())`), not the whole window. Previously every ticker frame called `queue_draw()`, forcing the compositor to re-blend the entire screen — including the 4K video region — 30×/s, which cost measurable frame drops on the N97 iGPU. Complementing this, the karaoke video is rendered **below a reserved top strip** (`video_top_margin_px`, default 80): `mpv_manager` launches mpv borderless at `--geometry=<W>x<H-margin>+0+<margin>` (was `--fs`; margin 0 restores fullscreen for rollback), so a top-strip ticker's damage rect never overlaps the video window and the compositor stops re-blending video pixels for the ticker entirely. This is a **persisted cross-process contract, not runtime IPC**: kj-controller writes `video_top_margin_px` into `overlays.json` (`OverlayManager.set_video_top_margin`, at startup) and sizes the video from the same `config.py` value; the overlay engine reads that strip height back out (`load_config` injects it into each ticker's config as `_strip_h`) and a `position:'top'` ticker sizes its bar to **fill** the strip — so there's no wallpaper gap between the ticker and the video, and the ticker's damage rect is exactly the reserved strip. Both sides independently read one persisted setting; `video_top_margin_px` is the single source of truth for both the video geometry and the ticker height, so they can't drift. VLC honours the same strip (device-validated 2026-07): it launches windowed and — because it maps its video window only once a song plays and ignores its own geometry CLI flags — `VlcKaraokePlayer._position_window` places the window **per-play** with `wmctrl`, matched by the unambiguous `VLC media player` title (the filler VLC is audio-only with no window; VLC leaves `_NET_WM_PID` unset, so the title is the only key). It corrects xfwm4's fixed frame-extent offset by measuring where the window lands after the first move and re-requesting `2×target − actual`. `margin_px <= 0` restores fullscreen for both engines (clean rollback). mpv remains the default and the only engine that can *hardware-decode* 4K on this box — VLC 3.0's VAAPI decoder never engages, so VLC always software-decodes (fine at 1080p, glitchy at 4K).
301
+
**Partial redraw + reserved top strip (4K frame-drop fix, 2026-07):** the render loop invalidates only each **animated overlay's own bounding box** (`queue_draw_area(*painter.bbox())`), not the whole window. Previously every ticker frame called `queue_draw()`, forcing the compositor to re-blend the entire screen — including the 4K video region — 30×/s, which cost measurable frame drops on the N97 iGPU. Complementing this, the karaoke video is rendered **below a reserved top strip** (`video_top_margin_px`, default 80): `mpv_manager` launches mpv borderless at `--geometry=<W>x<H-margin>+0+<margin>` (was `--fs`; margin 0 restores fullscreen for rollback), so a top-strip ticker's damage rect never overlaps the video window and the compositor stops re-blending video pixels for the ticker entirely. This is a **persisted cross-process contract, not runtime IPC**: kj-controller writes `video_top_margin_px` into `overlays.json` (`OverlayManager.set_video_top_margin`, at startup) and sizes the video from the same `config.py` value; the overlay engine reads that strip height back out (`load_config` injects it into each ticker's config as `_strip_h`) and a `position:'top'` ticker sizes its bar to **fill** the strip — so there's no wallpaper gap between the ticker and the video, and the ticker's damage rect is exactly the reserved strip. Both sides independently read one persisted setting; `video_top_margin_px` is the single source of truth for both the video geometry and the ticker height, so they can't drift. VLC honours the same strip (device-validated 2026-07): it launches windowed and — because it maps its video window only once a song plays and ignores its own geometry CLI flags — `VlcKaraokePlayer._position_window` places the window **per-play** with `wmctrl`, matched by the unambiguous `VLC media player` title (the filler VLC is audio-only with no window; VLC leaves `_NET_WM_PID` unset, so the title is the only key). It drives a short **closed loop** (request → settle → measure → adjust the next request by the observed error) until the window lands within 2px of target — needed because xfwm4 offsets wmctrl moves by a fixed frame-extent amount *and* VLC nudges/resizes its own window while a 4K file loads, so a single measure-and-correct can act on a stale position. `margin_px <= 0` restores fullscreen for both engines (clean rollback). mpv remains the default and the only engine that can *hardware-decode* 4K on this box — VLC 3.0's VAAPI decoder never engages, so VLC always software-decodes (fine at 1080p, glitchy at 4K).
Copy file name to clipboardExpand all lines: docs/CHANGELOG.md
+8-4Lines changed: 8 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,17 +2,21 @@
2
2
3
3
Device configuration changes. For Pi details, see [archive/NOMADPI-DETAILS.md](archive/NOMADPI-DETAILS.md). For mini PC setup, see [MINIPC-SETUP.md](MINIPC-SETUP.md).
4
4
5
-
## 2026-07-17 - Fix: VLC video no longer covered by the rotation ticker (v0.87.1)
5
+
## 2026-07-17 - Fix: VLC video no longer covered by the rotation ticker (v0.87.2)
6
6
7
7
The VLC karaoke renderer now honours the reserved top ticker strip
8
8
(`video_top_margin_px`, default 80) just like mpv — previously VLC launched
9
9
fullscreen and the ticker composited over the top of its video, while mpv
10
10
rendered below the strip. VLC ignores its own geometry CLI flags and maps its
11
11
video window only when a song starts, so `VlcKaraokePlayer._position_window`
12
12
now places the window **per-play** with `wmctrl` (matched by the `VLC media
13
-
player` window title), correcting xfwm4's fixed frame-extent offset by
14
-
re-requesting `2×target − actual`. Placement validated on NomadPC by
15
-
screenshot: VLC video sits at `1920×1000+0+80`, matching mpv.
13
+
player` window title). Because xfwm4 offsets wmctrl moves by a fixed
14
+
frame-extent amount *and* VLC nudges its own window while a 4K file loads, it
15
+
drives a short closed loop (request → settle → measure → adjust by the observed
16
+
error) until the window lands within 2px of target. Validated end-to-end on
17
+
NomadPC: VLC video settles at `1920×1000+0+80`, matching mpv. (v0.87.1 shipped
18
+
the per-play placement but a single-shot correction mis-landed on the live 4K
19
+
window at `y=160`; v0.87.2 replaced it with the convergence loop.)
16
20
17
21
Investigation context (no code change): confirmed **VLC 3.0.20 hardware-decodes
18
22
nothing via VAAPI** on NomadPC — its decoder module declines every codec, so
0 commit comments