From bb087578df32615821aa45ac9ed954ff4e7dee3b Mon Sep 17 00:00:00 2001 From: Josh Megnauth Date: Sun, 9 Nov 2025 21:33:47 -0500 Subject: [PATCH] Prevent screen turning off during playback Closes: #157 XDG portals expose a D-Bus interface allowing apps to prevent screen idling, user switching, and other actions. That interface, org.freedesktop.portal.Inhibit, does all of the heavy lifting here. Idling = the screen dimming or shutting off. Idling is inhibited when a video is actively playing. Idling is NOT inhibited when videos aren't playing - this includes paused or stopped videos. --- Cargo.lock | 213 ++++++++++++++++++++++++++++++++++++++++++--- Cargo.toml | 5 +- src/config.rs | 2 +- src/localize.rs | 2 +- src/main.rs | 58 +++++++++--- src/menu.rs | 7 +- src/mpris.rs | 4 +- src/thumbnail.rs | 4 +- src/video.rs | 15 ++-- src/xdg_portals.rs | 61 +++++++++++++ 10 files changed, 331 insertions(+), 40 deletions(-) create mode 100644 src/xdg_portals.rs diff --git a/Cargo.lock b/Cargo.lock index af1a1db..5e6af79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,7 +109,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.2.15", "once_cell", "version_check", "zerocopy", @@ -249,7 +249,7 @@ dependencies = [ "enumflags2", "futures-channel", "futures-util", - "rand", + "rand 0.8.5", "serde", "serde_repr", "tokio", @@ -266,7 +266,7 @@ dependencies = [ "enumflags2", "futures-channel", "futures-util", - "rand", + "rand 0.8.5", "serde", "serde_repr", "tokio", @@ -274,6 +274,23 @@ dependencies = [ "zbus 4.4.0", ] +[[package]] +name = "ashpd" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0986d5b4f0802160191ad75f8d33ada000558757db3defb70299ca95d9fcbd" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.2", + "serde", + "serde_repr", + "tokio", + "url", + "zbus 5.4.0", +] + [[package]] name = "async-broadcast" version = "0.5.1" @@ -1005,7 +1022,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom", + "getrandom 0.2.15", "once_cell", "tiny-keccak", ] @@ -1082,6 +1099,7 @@ dependencies = [ name = "cosmic-player" version = "0.1.0" dependencies = [ + "ashpd 0.12.0", "clap_lex", "env_logger", "fork", @@ -1984,6 +2002,18 @@ dependencies = [ "wasi", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + [[package]] name = "gif" version = "0.12.0" @@ -4063,7 +4093,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand", + "rand 0.8.5", ] [[package]] @@ -4279,6 +4309,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.8.5" @@ -4286,8 +4322,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", ] [[package]] @@ -4297,7 +4343,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -4306,7 +4362,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", ] [[package]] @@ -4405,7 +4470,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom", + "getrandom 0.2.15", "libredox", "thiserror 1.0.69", ] @@ -5118,7 +5183,7 @@ checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" dependencies = [ "cfg-if", "fastrand 2.3.0", - "getrandom", + "getrandom 0.2.15", "once_cell", "rustix 0.38.43", "windows-sys 0.59.0", @@ -5700,6 +5765,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.99" @@ -6426,6 +6500,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + [[package]] name = "write16" version = "1.0.0" @@ -6600,7 +6689,7 @@ dependencies = [ "nix 0.26.4", "once_cell", "ordered-stream", - "rand", + "rand 0.8.5", "serde", "serde_repr", "sha1", @@ -6638,7 +6727,7 @@ dependencies = [ "hex", "nix 0.29.0", "ordered-stream", - "rand", + "rand 0.8.5", "serde", "serde_repr", "sha1", @@ -6653,6 +6742,36 @@ dependencies = [ "zvariant 4.2.0", ] +[[package]] +name = "zbus" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbddd8b6cb25d5d8ec1b23277b45299a98bfb220f1761ca11e186d5c702507f8" +dependencies = [ + "async-broadcast 0.7.2", + "async-recursion", + "async-trait", + "enumflags2", + "event-listener 5.4.0", + "futures-core", + "futures-util", + "hex", + "nix 0.29.0", + "ordered-stream", + "serde", + "serde_repr", + "static_assertions", + "tokio", + "tracing", + "uds_windows", + "windows-sys 0.59.0", + "winnow 0.7.13", + "xdg-home", + "zbus_macros 5.4.0", + "zbus_names 4.2.0", + "zvariant 5.8.0", +] + [[package]] name = "zbus_macros" version = "3.15.2" @@ -6680,6 +6799,21 @@ dependencies = [ "zvariant_utils 2.1.0", ] +[[package]] +name = "zbus_macros" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac404d48b4e9cf193c8b49589f3280ceca5ff63519e7e64f55b4cf9c47ce146" +dependencies = [ + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "syn 2.0.96", + "zbus_names 4.2.0", + "zvariant 5.8.0", + "zvariant_utils 3.2.1", +] + [[package]] name = "zbus_names" version = "2.6.1" @@ -6702,6 +6836,18 @@ dependencies = [ "zvariant 4.2.0", ] +[[package]] +name = "zbus_names" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +dependencies = [ + "serde", + "static_assertions", + "winnow 0.7.13", + "zvariant 5.8.0", +] + [[package]] name = "zeno" version = "0.3.2" @@ -6809,6 +6955,21 @@ dependencies = [ "zvariant_derive 4.2.0", ] +[[package]] +name = "zvariant" +version = "5.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c" +dependencies = [ + "endi", + "enumflags2", + "serde", + "url", + "winnow 0.7.13", + "zvariant_derive 5.8.0", + "zvariant_utils 3.2.1", +] + [[package]] name = "zvariant_derive" version = "3.15.2" @@ -6835,6 +6996,19 @@ dependencies = [ "zvariant_utils 2.1.0", ] +[[package]] +name = "zvariant_derive" +version = "5.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006" +dependencies = [ + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "syn 2.0.96", + "zvariant_utils 3.2.1", +] + [[package]] name = "zvariant_utils" version = "1.0.1" @@ -6856,3 +7030,16 @@ dependencies = [ "quote", "syn 2.0.96", ] + +[[package]] +name = "zvariant_utils" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.96", + "winnow 0.7.13", +] diff --git a/Cargo.toml b/Cargo.toml index c77b3a2..6ec48f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,13 @@ [package] name = "cosmic-player" version = "0.1.0" -edition = "2021" +edition = "2024" [build-dependencies] vergen = { version = "8", features = ["git", "gitcl"] } [dependencies] +ashpd = { version = "0.12", optional = true } gstreamer-tag = "0.23" image = "0.24.9" serde = { version = "1", features = ["serde_derive"] } @@ -48,7 +49,7 @@ fork = "0.2" [features] default = ["mpris-server", "xdg-portal", "wgpu"] -xdg-portal = ["libcosmic/xdg-portal"] +xdg-portal = ["ashpd", "libcosmic/xdg-portal"] wgpu = ["iced_video_player/wgpu", "libcosmic/wgpu"] [profile.release-with-debug] diff --git a/src/config.rs b/src/config.rs index dfb895a..c8acef2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only use cosmic::{ - cosmic_config::{self, cosmic_config_derive::CosmicConfigEntry, CosmicConfigEntry}, + cosmic_config::{self, CosmicConfigEntry, cosmic_config_derive::CosmicConfigEntry}, theme, }; use serde::{Deserialize, Serialize}; diff --git a/src/localize.rs b/src/localize.rs index 70272fb..d7d9ce5 100644 --- a/src/localize.rs +++ b/src/localize.rs @@ -4,8 +4,8 @@ use std::str::FromStr; use std::sync::OnceLock; use i18n_embed::{ - fluent::{fluent_language_loader, FluentLanguageLoader}, DefaultLocalizer, LanguageLoader, Localizer, + fluent::{FluentLanguageLoader, fluent_language_loader}, }; use icu_collator::{Collator, CollatorOptions, Numeric}; use icu_provider::DataLocale; diff --git a/src/main.rs b/src/main.rs index adb9ccf..3b1a303 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,23 +2,25 @@ // SPDX-License-Identifier: GPL-3.0-only use cosmic::{ - app::{command, message, Command, Core, Settings}, + Application, ApplicationExt, Element, + app::{Command, Core, Settings, command, message}, cosmic_config::{self, CosmicConfigEntry}, cosmic_theme, executor, font, iced::{ + Alignment, Background, Border, Color, ContentFit, Length, Limits, event::{self, Event}, keyboard::{Event as KeyEvent, Key, Modifiers}, mouse::{Event as MouseEvent, ScrollDelta}, subscription::Subscription, - window, Alignment, Background, Border, Color, ContentFit, Length, Limits, + window, }, iced_style, theme, - widget::{self, menu::action::MenuAction, nav_bar, segmented_button, Slider}, - Application, ApplicationExt, Element, + widget::{self, Slider, menu::action::MenuAction, nav_bar, segmented_button}, }; use iced_video_player::{ + Video, VideoPlayer, gst::{self, prelude::*}, - gst_pbutils, Video, VideoPlayer, + gst_pbutils, }; use std::{ any::TypeId, @@ -32,8 +34,8 @@ use std::{ use tokio::sync::mpsc; use crate::{ - config::{Config, ConfigState, CONFIG_VERSION}, - key_bind::{key_binds, KeyBind}, + config::{CONFIG_VERSION, Config, ConfigState}, + key_bind::{KeyBind, key_binds}, project::ProjectNode, }; @@ -47,6 +49,8 @@ mod mpris; mod project; mod thumbnail; mod video; +#[cfg(feature = "xdg-portal")] +mod xdg_portals; static CONTROLS_TIMEOUT: Duration = Duration::new(2, 0); @@ -141,6 +145,7 @@ fn main() -> Result<(), Box> { settings = settings.theme(config.app_theme.theme()); settings = settings.size_limits(Limits::NONE.min_width(360.0).min_height(180.0)); + let flags = Flags { config, config_state_handler, @@ -313,6 +318,8 @@ pub struct App { current_audio: i32, text_codes: Vec, current_text: Option, + #[cfg(feature = "xdg-portal")] + inhibit: tokio::sync::watch::Sender, } impl App { @@ -339,6 +346,7 @@ impl App { self.current_text = None; self.update_mpris_meta(); self.update_nav_bar_active(); + self.allow_idle(); was_open } @@ -363,7 +371,7 @@ impl App { let video = match video::new_video(&url) { Ok(ok) => ok, - Err(err) => return err + Err(err) => return err, }; self.duration = video.duration().as_secs_f64(); @@ -420,6 +428,7 @@ impl App { self.current_text = None; } + self.inhibit_idle(); self.update_flags(); self.update_mpris_meta(); self.update_title() @@ -792,6 +801,20 @@ impl App { let title = "COSMIC Media Player"; self.set_window_title(title.to_string()) } + + /// Allow screen to dim or turn off if there is no input from the user. + /// + /// Basically, undo [`Self::inhibit_idle`]. + fn allow_idle(&self) { + #[cfg(feature = "xdg-portal")] + let _ = self.inhibit.send(false); + } + + /// Prevent screen from dimming or turning off if there is no keyboard/mouse input. + fn inhibit_idle(&self) { + #[cfg(feature = "xdg-portal")] + let _ = self.inhibit.send(true); + } } /// Implement [`cosmic::Application`] to integrate with COSMIC. @@ -820,6 +843,13 @@ impl Application for App { fn init(mut core: Core, flags: Self::Flags) -> (Self, Command) { core.window.content_container = false; + #[cfg(feature = "xdg-portal")] + let inhibit = { + let (tx, rx) = tokio::sync::watch::channel(false); + std::mem::drop(tokio::spawn(crate::xdg_portals::inhibit(rx))); + tx + }; + let mut app = App { core, flags, @@ -843,6 +873,8 @@ impl Application for App { current_audio: -1, text_codes: Vec::new(), current_text: None, + #[cfg(feature = "xdg-portal")] + inhibit, }; // Do not show nav bar by default. Will be opened by open_project if needed @@ -1176,12 +1208,18 @@ impl Application for App { self.dropdown_opt = None; if let Some(video) = &mut self.video_opt { - video.set_paused(match message { + let pause = match message { Message::Play => false, Message::Pause => true, _ => !video.paused(), - }); + }; + video.set_paused(pause); self.update_controls(true); + if pause { + self.allow_idle(); + } else { + self.inhibit_idle(); + } } } Message::Scrolled(delta) => { diff --git a/src/menu.rs b/src/menu.rs index fe8b102..955f91b 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -1,13 +1,12 @@ // SPDX-License-Identifier: GPL-3.0-only use cosmic::{ - theme, - widget::menu::{self, key_bind::KeyBind, ItemHeight, ItemWidth, MenuBar}, - Element, + Element, theme, + widget::menu::{self, ItemHeight, ItemWidth, MenuBar, key_bind::KeyBind}, }; use std::{collections::HashMap, path::PathBuf}; -use crate::{fl, Action, Config, ConfigState, Message}; +use crate::{Action, Config, ConfigState, Message, fl}; pub fn menu_bar<'a>( _config: &Config, diff --git a/src/mpris.rs b/src/mpris.rs index d89023c..533a1be 100644 --- a/src/mpris.rs +++ b/src/mpris.rs @@ -3,12 +3,12 @@ use cosmic::iced::{ subscription::{self, Subscription}, }; use mpris_server::{ - zbus::{fdo, Result}, LoopStatus, Metadata, PlaybackRate, PlaybackStatus, PlayerInterface, Property, RootInterface, Server, Signal, Time, TrackId, Volume, + zbus::{Result, fdo}, }; use std::{any::TypeId, future, process}; -use tokio::sync::{mpsc, Mutex}; +use tokio::sync::{Mutex, mpsc}; use crate::{Message, MprisEvent, MprisMeta, MprisState}; diff --git a/src/thumbnail.rs b/src/thumbnail.rs index 6651a68..58c7314 100644 --- a/src/thumbnail.rs +++ b/src/thumbnail.rs @@ -1,5 +1,5 @@ use cosmic::iced_core::image::Data; -use iced_video_player::{Position}; +use iced_video_player::Position; use image::{DynamicImage, ImageFormat, RgbaImage}; use std::{error::Error, num::NonZero, path::Path, time::Duration}; use url::Url; @@ -15,7 +15,7 @@ pub fn main( let thumbnails = { let mut video = match video::new_video(input) { Ok(ok) => ok, - Err(_err) => return Err(Into::into(format!("missing required plugin"))) + Err(_err) => return Err(Into::into(format!("missing required plugin"))), }; let duration = video.duration(); diff --git a/src/video.rs b/src/video.rs index 339aead..c91e9e9 100644 --- a/src/video.rs +++ b/src/video.rs @@ -1,12 +1,14 @@ - use iced_video_player::{ + Video, gst::{self, prelude::*}, - gst_app, gst_pbutils, Video, + gst_app, gst_pbutils, }; use cosmic::app::{Command, message}; -pub fn new_video(url: &url::Url) -> Result>> { +pub fn new_video( + url: &url::Url, +) -> Result>> { //TODO: this code came from iced_video_player::Video::new and has been modified to stop the pipeline on error //TODO: remove unwraps and enable playback of files with only audio. gst::init().unwrap(); @@ -26,7 +28,10 @@ pub fn new_video(url: &url::Url) -> Result Result +// SPDX-License-Identifier: GPL-3.0-only + +//! Integrations with XDG portals. + +use ashpd::{ + desktop::inhibit::{InhibitFlags, InhibitProxy}, + enumflags2::{BitFlags, make_bitflags}, +}; +use log::{debug, warn}; +use tokio::sync::watch::Receiver; + +// Actions to inhibit. Currently, COSMIC defaults to the GTK portal for Inhibit. That +// implementation only supports inhibiting idling and trying to inhibit anything else causes the +// D-Bus call to silently fail. We will only inhibit idling until COSMIC gets a bespoke Inhibit. +const INHIBIT_FLAGS: BitFlags = make_bitflags!(InhibitFlags::{Idle}); + +/// Inhibit idle and user switching while media is played. +/// +/// # Usage +/// Enable the inhibitor by setting the watcher to `true`. Disable the inhibitor by sending a +/// `false`. Sending multiple consecutive trues/falses is safe and guarded internally. +/// +/// Portal: +/// https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Inhibit.html +pub async fn inhibit(mut signal: Receiver) -> ashpd::Result<()> { + let proxy = InhibitProxy::new().await?; + // Mark the watcher's value as unseen so a temporary or mutable bool isn't needed in the loop. + signal.mark_changed(); + + loop { + if signal.wait_for(|&status| status).await.is_err() { + // The watcher will likely only be closed when the app is closed. + debug!("Inhibit task's watcher is closed"); + break; + } + // Copying the bool is important or else we would needlessly hold the lock below. + let should_inhibit = *signal.borrow_and_update(); + + if should_inhibit + && let Some(inhibit_handle) = proxy + .inhibit(None, INHIBIT_FLAGS, "") + .await + .inspect_err(|e| warn!("Failed to call inhibit portal endpoint: {e}")) + .ok() + { + // We don't have to check the bool because it's already checked to be false in the + // closure. We also don't have to break on error because the next iteration of the loop + // would break anyway. + let _ = signal.wait_for(|&status| !status).await; + if let Err(e) = inhibit_handle.close().await { + // This should only happen if the inhibit portal silently fails which GTK (and + // others!) apparently do. + warn!("Removing the inhibitor failed: {e}"); + break; + } + } + } + + Ok(()) +}