feat(windows): keep the system awake while a track plays

Linux holds a logind inhibitor fd; Windows has SetThreadExecutionState, and
nothing was calling it.

The plan asked for the call to sit in `set_playback`. It cannot: that API is
PER-THREAD -- Windows drops a thread's requests when it exits -- and this
module's own contract says outbound calls are "free from any thread"
(media_controls_qt.rs:34). In practice they arrive on the mpris-server thread,
a tokio worker, or the GUI thread, whichever pushed. A request made from a pool
worker would evaporate with it, silently. So the state is owned by one thread
that lives as long as the process and receives a bool.

ES_SYSTEM_REQUIRED without ES_DISPLAY_REQUIRED: an audio player keeps the
MACHINE awake, not the screen. No ES_AWAYMODE_REQUIRED either -- that is for
media-centre appliances and it suppresses the sleep the user asked for.

Exposed as a free function rather than a `PlatformHandle` method, and called
BEFORE the `handle()` early return on both publishers. Hanging it off the
handle tied it to SMTC: a failed souvlaki init, or an HWND that never arrived,
would have left playback working and the machine falling asleep mid-track --
two unrelated features failing together for no reason.

`held` is updated only after the API succeeds, so a failed release cannot leave
Windows inhibited while later `false` messages are skipped as no-ops.

cc
This commit is contained in:
vicrodh committed 2026-08-27 13:55:45 -06:00
1 parent 9b0d8d7e0f
commit c0664030ee
4 files changed
+111

No files matched your search

+5
View File
@@ -217,6 +217,10 @@ pub(crate) fn push_now_playing(track: &qbz_models::QueueTrack, title: &str, albu
// smaller set of users.
maybe_notify(track, title, album);
push_metadata(track, title, album);
// BEFORE the early return, and for the same reason `maybe_notify` is:
// keeping the machine awake has nothing to do with whether the OS media
// integration started, and a track is about to play either way.
qbz_media_controls::set_sleep_inhibit(true);
let Some(mc) = handle() else {
return;
};
@@ -299,6 +303,7 @@ pub(crate) fn clear_now_playing() {
/// transitions the MPRIS `Position` getter answers from the last value pushed
/// here — the same behaviour as the Slint build.
pub(crate) fn push_playback_state(is_playing: bool, position_secs: u64) {
qbz_media_controls::set_sleep_inhibit(is_playing);
let Some(mc) = handle() else {
return;
};