fix: hold the DVD cover until the disc menu is actually showing

The readiness condition was playback state alone, and on a real disc
VLC reports playing well before its window has taken the screen. The
cover came off onto VLC's ordinary tiled startup window — black, sized
to everything except the dock's reserved strip — and the disc menu only
arrived afterwards. Exactly the state the cover exists to hide.

Ask sway instead. VLC only takes the display when its window goes
fullscreen, and the tree says when that happens. The cover now comes off
when VLC reports playing or paused, its window is fullscreen, and 800ms
have passed for the menu to draw its first frames. The playback check
runs first because it is local and cheap; the tree is only walked once
VLC claims to be playing something.

The twenty second deadline is unchanged: a cover with nothing behind it
is still the one outcome DVD mode must never produce.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
ekklesia
2026-09-04 23:17:37 -05:00
co-authored by Claude Opus 5
parent 719b856040
commit 37f8166401
3 changed files with 157 additions and 24 deletions
+46 -20
View File
@@ -46,8 +46,9 @@ sys.path.insert(
import classroom_optical as optical # noqa: E402
import classroom_style as style # noqa: E402
import classroom_sway as sway # noqa: E402
import classroom_vlc as vlc # noqa: E402
from classroom_config import read_device_config, scaled, ui_scale # noqa: E402
from classroom_config import SHELL_SCALE, read_device_config, scaled # noqa: E402
# Input shaping needs pycairo. Without it the bar still works, it just
# takes pointer events across its whole strip instead of only where it
@@ -77,13 +78,22 @@ HOVER_STRIP = 14
IDLE_HIDE_SECONDS = 4
LEAVE_HIDE_SECONDS = 1
# The loading surface goes away as soon as playback is on screen. The
# deadline is a safety net: whatever happens, DVD mode must never leave a
# cover over a screen with nothing behind it. Generous, because an
# optical drive can take a while to spin a disc up.
WINDOW_POLL_MS = 500
# How VLC is recognised in the sway tree. Qt5 puts it on XWayland, so
# the X11 class is what usually matches.
VLC_WINDOW = (r"vlc", r"vlc-.*")
# The loading surface goes away only once VLC has genuinely taken the
# screen. The deadline is a safety net: whatever happens, DVD mode must
# never leave a cover over a screen with nothing behind it. Generous,
# because an optical drive can take a while to spin a disc up.
READY_POLL_MS = 250
LOADING_DEADLINE_SECONDS = 20
# Held after VLC looks ready, so the disc menu has drawn its first frames
# before the cover comes off. Without this the handover lands on VLC's
# own black startup window instead of on the menu.
STABILISE_MS = 800
# Long enough for the cover to be on screen before VLC is even started,
# so no part of VLC's window is ever visible against the wallpaper.
LAUNCH_DELAY_MS = 120
@@ -448,7 +458,7 @@ class ControlBar(Gtk.Window):
class DvdSession:
def __init__(self):
self.config = read_device_config()
self.scale = ui_scale(self.config)
self.scale = SHELL_SCALE
self.device = optical.default_device()
self.port = free_port()
@@ -555,7 +565,7 @@ class DvdSession:
GLib.child_watch_add(GLib.PRIORITY_DEFAULT, self.pid, self.on_exit)
GLib.timeout_add(WINDOW_POLL_MS, self.watch_startup)
GLib.timeout_add(READY_POLL_MS, self.watch_startup)
GLib.timeout_add_seconds(STATUS_POLL_SECONDS, self.watch_state)
return GLib.SOURCE_REMOVE
@@ -565,22 +575,38 @@ class DvdSession:
self.pid = None
Gtk.main_quit()
def watch_startup(self):
"""Hold the cover until playback is genuinely on screen.
def player_is_fullscreen(self):
"""True once VLC's window exists and has taken the whole output.
Playing or paused means the video output exists, which is also
when VLC takes the display. The deadline is the safety net: DVD
mode must never leave a cover over a screen with nothing behind
it, even if the disc never opens.
Playback state alone is not enough. VLC reports playing while its
window is still the ordinary tiled one sway gave it, which is the
black startup window the cover exists to hide. It only takes the
display when it goes fullscreen, and that is what the tree says.
"""
self.elapsed += WINDOW_POLL_MS / 1000
for window in sway.windows():
if sway.matches(window, VLC_WINDOW) and window["fullscreen"]:
return True
ready = self.control.state() in ("playing", "paused")
return False
def watch_startup(self):
"""Hold the cover until VLC is genuinely ready to replace it.
Three things have to line up: VLC says it is playing, its window
is fullscreen, and a moment has passed for the disc menu to draw.
The deadline is the safety net: DVD mode must never leave a cover
over a screen with nothing behind it, even if the disc never
opens.
"""
self.elapsed += READY_POLL_MS / 1000
# The cheap local check first; the tree is only worth walking
# once VLC claims to be playing something.
playing = self.control.state() in ("playing", "paused")
ready = playing and self.player_is_fullscreen()
if ready or self.elapsed >= LOADING_DEADLINE_SECONDS:
# A moment for the video window to actually take the screen,
# so the handover has no frame of wallpaper in it.
GLib.timeout_add(400, self.drop_loading)
GLib.timeout_add(STABILISE_MS, self.drop_loading)
return GLib.SOURCE_REMOVE
return GLib.SOURCE_CONTINUE
@@ -641,7 +667,7 @@ def main():
print("classroom-dvd-session: no optical drive", file=sys.stderr)
return 1
load_css(ui_scale(read_device_config()))
load_css(SHELL_SCALE)
session = DvdSession()
session.start()