feat(app): media keys and desktop media widget, keep the screen on during a film

- MPRIS on Linux and SMTC on Windows through souvlaki: play/pause, next,
  previous, stop and seeking from media keys and the panel widget, with
  the title, episode, duration and the cached cover as a file:// URL
- the screensaver is held off while a film plays on the player page,
  through the desktop portal with org.freedesktop.ScreenSaver as the
  fallback, and SetThreadExecutionState on Windows. mpv's own
  stop-screensaver has no window to act for under the render API
This commit is contained in:
nvrl committed 2026-10-02 20:16:27 +02:00
1 parent c60ffc7ab1
commit 959547cdc8
8 files changed
+573 -3

No files matched your search

+65
View File
@@ -428,6 +428,12 @@ pub struct App {
crawl_progress: std::collections::BTreeMap<String, usize>,
/// The window's size, written at exit for the next launch.
window: crate::window::WindowMemory,
/// Media keys and the desktop's media widget.
media: crate::media::MediaSession,
/// The cover handed to it, worked out once per player.
media_cover: Option<(u64, Option<String>)>,
/// Holds the screensaver off while a film is on screen and playing.
inhibitor: crate::inhibit::Inhibitor,
pub downloads: Vec<DownloadItem>,
pub offline_files: std::collections::HashSet<DownloadKey>,
pub confirm_partial_delete: Option<DownloadKey>,
@@ -494,6 +500,9 @@ impl App {
crawling: false,
crawl_progress: Default::default(),
window,
media: crate::media::MediaSession::new(&cc.egui_ctx, window_handle(cc)),
media_cover: None,
inhibitor: crate::inhibit::Inhibitor::new(),
downloads: Vec::new(),
offline_files: std::collections::HashSet::new(),
confirm_partial_delete: None,
@@ -740,6 +749,34 @@ impl App {
}
}
/// The playing title's artwork as a `file://` URL, if it is on disk.
///
/// The cached file rather than the provider's URL: the desktop would
/// otherwise fetch it from the provider itself, and a desktop shell is not
/// something the viewer agreed would talk to one.
fn media_cover(&mut self) -> Option<String> {
let playback = self.playback.as_ref()?;
if let Some((id, cover)) = &self.media_cover
&& *id == playback.id
{
return cover.clone();
}
let cover = self
.metadata
.get(&playback.target.title_key)
.and_then(|record| record.metadata.as_ref())
.and_then(|found| {
found
.poster_url
.as_deref()
.or_else(|| found.tile_art().map(|(url, _)| url))
})
.and_then(|url| self.engine.poster_file(url))
.map(|path| format!("file://{}", path.display()));
self.media_cover = Some((playback.id, cover.clone()));
cover
}
fn share_name(&self, id: &str) -> String {
self.shares
.iter()
@@ -1343,6 +1380,9 @@ impl eframe::App for App {
self.window.observe(ctx);
let mut actions: Vec<Action> = Vec::new();
self.media.drain(self.playback.as_ref(), &mut actions);
let cover = self.media_cover();
self.media.show(self.playback.as_ref(), cover.as_deref());
// A player page with nothing playing and nothing opening is a black
// window with no way out. It should be unreachable — `pump` routes away
@@ -1351,6 +1391,15 @@ impl eframe::App for App {
self.page = Page::Library;
}
self.engine.set_picture_shown(self.page == Page::Player);
// On screen and moving: a film paused, or playing behind another page,
// is no reason to keep the screen awake.
self.inhibitor.set(
self.page == Page::Player
&& self
.playback
.as_ref()
.is_some_and(|playback| playback.loaded && !playback.paused),
);
if self.page == Page::Player {
self.overlay.observe(ctx);
@@ -1656,6 +1705,22 @@ impl eframe::App for App {
}
}
/// The native window, for the parts of the OS that want it named — SMTC on
/// Windows. Nothing else asks.
#[cfg(windows)]
fn window_handle(cc: &eframe::CreationContext<'_>) -> Option<*mut std::ffi::c_void> {
use raw_window_handle::{HasWindowHandle as _, RawWindowHandle};
match cc.window_handle().ok()?.as_raw() {
RawWindowHandle::Win32(handle) => Some(handle.hwnd.get() as *mut std::ffi::c_void),
_ => None,
}
}
#[cfg(not(windows))]
fn window_handle(_cc: &eframe::CreationContext<'_>) -> Option<*mut std::ffi::c_void> {
None
}
/// What the top bar says while shares are being listed: one share by name,
/// several as a count, with the files found across all of them.
fn crawl_label(progress: &std::collections::BTreeMap<String, usize>) -> String {