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:
co-authored by
Claude Opus 5
parent
719b856040
commit
37f8166401
+16
-4
@@ -64,10 +64,22 @@ dock is a top-layer surface and DVD mode has to cover it. Its exclusive
|
|||||||
zone is `-1`, which tells sway not to move it out of the way of the
|
zone is `-1`, which tells sway not to move it out of the way of the
|
||||||
strip the dock reserves.
|
strip the dock reserves.
|
||||||
|
|
||||||
It comes down when VLC reports playing or paused, meaning the video
|
It comes down only when **all three** of these hold:
|
||||||
output exists and has taken the display, plus a moment for the handover.
|
|
||||||
A twenty second deadline takes it down regardless: a cover with nothing
|
1. VLC reports playing or paused, and
|
||||||
behind it is the one outcome DVD mode must never produce.
|
2. VLC's window is fullscreen in the sway tree, and
|
||||||
|
3. 800ms have passed since both became true.
|
||||||
|
|
||||||
|
Playback state alone is not enough, and used to be the whole condition.
|
||||||
|
VLC reports playing while its window is still the ordinary tiled one
|
||||||
|
sway gave it — which is exactly the black startup window the cover
|
||||||
|
exists to hide. It only takes the display when it goes fullscreen, and
|
||||||
|
the sway tree is what says so. The final hold gives the disc menu time
|
||||||
|
to draw its first frames, so the handover lands on the menu rather than
|
||||||
|
on VLC's black.
|
||||||
|
|
||||||
|
A twenty second deadline takes the cover down regardless: a cover with
|
||||||
|
nothing behind it is the one outcome DVD mode must never produce.
|
||||||
|
|
||||||
VLC is still the backend because its `dvdnav` module is what decodes
|
VLC is still the backend because its `dvdnav` module is what decodes
|
||||||
disc menus, and nothing else on Debian does that reliably. But nothing
|
disc menus, and nothing else on Debian does that reliably. But nothing
|
||||||
|
|||||||
@@ -46,8 +46,9 @@ sys.path.insert(
|
|||||||
|
|
||||||
import classroom_optical as optical # noqa: E402
|
import classroom_optical as optical # noqa: E402
|
||||||
import classroom_style as style # noqa: E402
|
import classroom_style as style # noqa: E402
|
||||||
|
import classroom_sway as sway # noqa: E402
|
||||||
import classroom_vlc as vlc # 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
|
# Input shaping needs pycairo. Without it the bar still works, it just
|
||||||
# takes pointer events across its whole strip instead of only where it
|
# takes pointer events across its whole strip instead of only where it
|
||||||
@@ -77,13 +78,22 @@ HOVER_STRIP = 14
|
|||||||
IDLE_HIDE_SECONDS = 4
|
IDLE_HIDE_SECONDS = 4
|
||||||
LEAVE_HIDE_SECONDS = 1
|
LEAVE_HIDE_SECONDS = 1
|
||||||
|
|
||||||
# The loading surface goes away as soon as playback is on screen. The
|
# How VLC is recognised in the sway tree. Qt5 puts it on XWayland, so
|
||||||
# deadline is a safety net: whatever happens, DVD mode must never leave a
|
# the X11 class is what usually matches.
|
||||||
# cover over a screen with nothing behind it. Generous, because an
|
VLC_WINDOW = (r"vlc", r"vlc-.*")
|
||||||
# optical drive can take a while to spin a disc up.
|
|
||||||
WINDOW_POLL_MS = 500
|
# 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
|
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,
|
# 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.
|
# so no part of VLC's window is ever visible against the wallpaper.
|
||||||
LAUNCH_DELAY_MS = 120
|
LAUNCH_DELAY_MS = 120
|
||||||
@@ -448,7 +458,7 @@ class ControlBar(Gtk.Window):
|
|||||||
class DvdSession:
|
class DvdSession:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.config = read_device_config()
|
self.config = read_device_config()
|
||||||
self.scale = ui_scale(self.config)
|
self.scale = SHELL_SCALE
|
||||||
|
|
||||||
self.device = optical.default_device()
|
self.device = optical.default_device()
|
||||||
self.port = free_port()
|
self.port = free_port()
|
||||||
@@ -555,7 +565,7 @@ class DvdSession:
|
|||||||
|
|
||||||
GLib.child_watch_add(GLib.PRIORITY_DEFAULT, self.pid, self.on_exit)
|
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)
|
GLib.timeout_add_seconds(STATUS_POLL_SECONDS, self.watch_state)
|
||||||
|
|
||||||
return GLib.SOURCE_REMOVE
|
return GLib.SOURCE_REMOVE
|
||||||
@@ -565,22 +575,38 @@ class DvdSession:
|
|||||||
self.pid = None
|
self.pid = None
|
||||||
Gtk.main_quit()
|
Gtk.main_quit()
|
||||||
|
|
||||||
def watch_startup(self):
|
def player_is_fullscreen(self):
|
||||||
"""Hold the cover until playback is genuinely on screen.
|
"""True once VLC's window exists and has taken the whole output.
|
||||||
|
|
||||||
Playing or paused means the video output exists, which is also
|
Playback state alone is not enough. VLC reports playing while its
|
||||||
when VLC takes the display. The deadline is the safety net: DVD
|
window is still the ordinary tiled one sway gave it, which is the
|
||||||
mode must never leave a cover over a screen with nothing behind
|
black startup window the cover exists to hide. It only takes the
|
||||||
it, even if the disc never opens.
|
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:
|
if ready or self.elapsed >= LOADING_DEADLINE_SECONDS:
|
||||||
# A moment for the video window to actually take the screen,
|
GLib.timeout_add(STABILISE_MS, self.drop_loading)
|
||||||
# so the handover has no frame of wallpaper in it.
|
|
||||||
GLib.timeout_add(400, self.drop_loading)
|
|
||||||
return GLib.SOURCE_REMOVE
|
return GLib.SOURCE_REMOVE
|
||||||
|
|
||||||
return GLib.SOURCE_CONTINUE
|
return GLib.SOURCE_CONTINUE
|
||||||
@@ -641,7 +667,7 @@ def main():
|
|||||||
print("classroom-dvd-session: no optical drive", file=sys.stderr)
|
print("classroom-dvd-session: no optical drive", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
load_css(ui_scale(read_device_config()))
|
load_css(SHELL_SCALE)
|
||||||
|
|
||||||
session = DvdSession()
|
session = DvdSession()
|
||||||
session.start()
|
session.start()
|
||||||
|
|||||||
@@ -106,6 +106,101 @@ print("6. the disc is the last argument")
|
|||||||
|
|
||||||
check("dvd:// selects dvdnav for menus", command[-1] == "dvd:///dev/sr0")
|
check("dvd:// selects dvdnav for menus", command[-1] == "dvd:///dev/sr0")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("7. the loading surface waits for a fullscreen VLC window")
|
||||||
|
|
||||||
|
import json # noqa: E402
|
||||||
|
import os # noqa: E402
|
||||||
|
import subprocess # noqa: E402
|
||||||
|
import tempfile # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def tree_with(app, fullscreen):
|
||||||
|
return {
|
||||||
|
"tree": {
|
||||||
|
"type": "root", "id": 1, "nodes": [{
|
||||||
|
"type": "output", "id": 2, "name": "HDMI-A-1", "nodes": [{
|
||||||
|
"type": "workspace", "id": 3, "name": "1",
|
||||||
|
"nodes": ([] if app is None else [{
|
||||||
|
"type": "con", "id": 10, "pid": 100,
|
||||||
|
"app_id": None,
|
||||||
|
"window_properties": {"class": app, "instance": app},
|
||||||
|
"focused": True,
|
||||||
|
"fullscreen_mode": 1 if fullscreen else 0,
|
||||||
|
"nodes": [], "floating_nodes": [],
|
||||||
|
}]),
|
||||||
|
"floating_nodes": [],
|
||||||
|
}], "floating_nodes": [],
|
||||||
|
}], "floating_nodes": [],
|
||||||
|
},
|
||||||
|
"workspaces": [{"name": "1", "focused": True, "visible": True}],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
work = tempfile.mkdtemp()
|
||||||
|
os.makedirs(f"{work}/bin", exist_ok=True)
|
||||||
|
|
||||||
|
with open(f"{work}/bin/swaymsg", "w") as shim:
|
||||||
|
shim.write(
|
||||||
|
"#!/usr/bin/env python3\n"
|
||||||
|
"import json, os, sys\n"
|
||||||
|
"state = json.load(open(os.environ['MOCK_TREE']))\n"
|
||||||
|
"print(json.dumps(state['tree' if sys.argv[2] == 'get_tree'"
|
||||||
|
" else 'workspaces']))\n"
|
||||||
|
)
|
||||||
|
os.chmod(f"{work}/bin/swaymsg", 0o755)
|
||||||
|
os.environ["PATH"] = f"{work}/bin:" + os.environ["PATH"]
|
||||||
|
os.environ["MOCK_TREE"] = f"{work}/tree.json"
|
||||||
|
|
||||||
|
|
||||||
|
def set_tree(app, fullscreen):
|
||||||
|
with open(f"{work}/tree.json", "w") as handle:
|
||||||
|
json.dump(tree_with(app, fullscreen), handle)
|
||||||
|
|
||||||
|
|
||||||
|
ready = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
import importlib.util
|
||||||
|
from importlib.machinery import SourceFileLoader
|
||||||
|
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version("Gtk", "3.0")
|
||||||
|
gi.require_version("GtkLayerShell", "0.1")
|
||||||
|
|
||||||
|
loader = SourceFileLoader("dvdsession", sys.argv[1] + "/scripts/classroom-dvd-session")
|
||||||
|
spec = importlib.util.spec_from_loader("dvdsession", loader)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules["dvdsession"] = module
|
||||||
|
loader.exec_module(module)
|
||||||
|
ready = module.DvdSession.player_is_fullscreen
|
||||||
|
except Exception as error: # noqa: BLE001
|
||||||
|
print(f" skip GTK unavailable here ({type(error).__name__})")
|
||||||
|
|
||||||
|
if ready is not None:
|
||||||
|
set_tree("vlc", False)
|
||||||
|
check("a tiled VLC window is not ready", ready(None) is False)
|
||||||
|
|
||||||
|
set_tree("vlc", True)
|
||||||
|
check("a fullscreen VLC window is ready", ready(None) is True)
|
||||||
|
|
||||||
|
set_tree("Google-chrome", True)
|
||||||
|
check("someone else's fullscreen window is not VLC", ready(None) is False)
|
||||||
|
|
||||||
|
set_tree(None, False)
|
||||||
|
check("no windows at all is not ready", ready(None) is False)
|
||||||
|
|
||||||
|
check("playback state alone is no longer the condition",
|
||||||
|
"player_is_fullscreen" in module.DvdSession.watch_startup.__doc__
|
||||||
|
or "fullscreen" in module.DvdSession.watch_startup.__doc__)
|
||||||
|
check("the handover is held briefly after both agree",
|
||||||
|
500 <= module.STABILISE_MS <= 1000, f"got {module.STABILISE_MS}")
|
||||||
|
check("the safety deadline is still there",
|
||||||
|
module.LOADING_DEADLINE_SECONDS == 20)
|
||||||
|
|
||||||
|
subprocess.run(["rm", "-rf", work])
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print(f"passed: {passed} failed: {failed}")
|
print(f"passed: {passed} failed: {failed}")
|
||||||
sys.exit(1 if failed else 0)
|
sys.exit(1 if failed else 0)
|
||||||
|
|||||||
Reference in New Issue
Block a user