mirror of
https://github.com/talwat/lowfi
synced 2026-09-14 16:49:46 +00:00
feat: add Windows SMTC media key support (#134)
This commit is contained in:
parent
53ac389b7d
commit
d86a51e933
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@ -19,6 +19,7 @@ jobs:
|
||||
bin: lowfi
|
||||
name: lowfi-linux-x86_64
|
||||
command: build
|
||||
features: ""
|
||||
|
||||
- release_for: Windows x86_64
|
||||
os: windows-latest
|
||||
@ -26,6 +27,7 @@ jobs:
|
||||
bin: lowfi.exe
|
||||
name: lowfi-windows-x86_64.exe
|
||||
command: build
|
||||
features: "--features media-controls"
|
||||
|
||||
- release_for: MacOS x86_64
|
||||
os: macos-latest
|
||||
@ -33,6 +35,7 @@ jobs:
|
||||
bin: lowfi
|
||||
name: lowfi-darwin-x86_64
|
||||
command: build
|
||||
features: ""
|
||||
|
||||
- release_for: MacOS aarch64
|
||||
os: macos-latest
|
||||
@ -40,6 +43,7 @@ jobs:
|
||||
bin: lowfi
|
||||
name: lowfi-darwin-aarch64
|
||||
command: build
|
||||
features: ""
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
@ -61,7 +65,7 @@ jobs:
|
||||
with:
|
||||
command: ${{ matrix.command }}
|
||||
target: ${{ matrix.target }}
|
||||
args: "--locked --release"
|
||||
args: --locked --release ${{ matrix.features }}
|
||||
|
||||
- name: Rename binary
|
||||
run: |
|
||||
@ -72,4 +76,4 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
gh release upload ${{github.event.release.tag_name}} \
|
||||
./target/${{ matrix.target }}/release/${{ matrix.name }} \
|
||||
./target/${{ matrix.target }}/release/${{ matrix.name }} \
|
||||
142
Cargo.lock
generated
142
Cargo.lock
generated
@ -281,6 +281,12 @@ version = "2.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
||||
|
||||
[[package]]
|
||||
name = "block"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.6.2"
|
||||
@ -422,6 +428,36 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cocoa"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"block",
|
||||
"cocoa-foundation",
|
||||
"core-foundation 0.9.4",
|
||||
"core-graphics",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
"objc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cocoa-foundation"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"block",
|
||||
"core-foundation 0.9.4",
|
||||
"core-graphics-types",
|
||||
"libc",
|
||||
"objc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
@ -459,6 +495,16 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.1"
|
||||
@ -475,6 +521,30 @@ version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "core-graphics"
|
||||
version = "0.22.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation 0.9.4",
|
||||
"core-graphics-types",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-graphics-types"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation 0.9.4",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coreaudio-rs"
|
||||
version = "0.14.2"
|
||||
@ -516,7 +586,7 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows",
|
||||
"windows 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -632,6 +702,12 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dispatch"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
|
||||
|
||||
[[package]]
|
||||
name = "dispatch2"
|
||||
version = "0.3.1"
|
||||
@ -802,6 +878,21 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||
dependencies = [
|
||||
"foreign-types-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
@ -1449,10 +1540,12 @@ dependencies = [
|
||||
"scraper",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"souvlaki",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"unicode-segmentation",
|
||||
"url",
|
||||
"windows 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1470,6 +1563,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "malloc_buf"
|
||||
version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markup5ever"
|
||||
version = "0.39.0"
|
||||
@ -1629,6 +1731,15 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
|
||||
dependencies = [
|
||||
"malloc_buf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2"
|
||||
version = "0.6.4"
|
||||
@ -2223,7 +2334,7 @@ version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
|
||||
dependencies = [
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"jni 0.22.4",
|
||||
"log",
|
||||
@ -2308,7 +2419,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
@ -2493,6 +2604,22 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "souvlaki"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5855c8f31521af07d896b852eaa9eca974ddd3211fc2ae292e58dda8eb129bc8"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"block",
|
||||
"cocoa",
|
||||
"core-graphics",
|
||||
"dispatch",
|
||||
"objc",
|
||||
"thiserror 1.0.69",
|
||||
"windows 0.44.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.1"
|
||||
@ -3235,6 +3362,15 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.44.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b"
|
||||
dependencies = [
|
||||
"windows-targets 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.62.2"
|
||||
|
||||
11
Cargo.toml
11
Cargo.toml
@ -21,6 +21,8 @@ repository = "https://github.com/talwat/lowfi"
|
||||
default = ["default-tracklist"]
|
||||
default-tracklist = []
|
||||
mpris = ["dep:mpris-server", "dep:arc-swap"]
|
||||
# Windows System Media Transport Controls (hardware media keys / overlay).
|
||||
media-controls = ["dep:souvlaki", "dep:windows"]
|
||||
extra-audio-formats = ["rodio/default"]
|
||||
scrape = [
|
||||
"dep:serde",
|
||||
@ -66,3 +68,12 @@ indicatif = { version = "0.18.4", optional = true }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
libc = "0.2.186"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
souvlaki = { version = "0.8.3", optional = true, default-features = false }
|
||||
windows = { version = "0.62", optional = true, features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
] }
|
||||
23
README.md
23
README.md
@ -65,8 +65,11 @@ Then, simply run:
|
||||
```sh
|
||||
cargo install lowfi
|
||||
|
||||
# If you want MPRIS support.
|
||||
# If you want MPRIS support (Linux).
|
||||
cargo install lowfi --features mpris
|
||||
|
||||
# If you want Windows hardware media keys / SMTC overlay.
|
||||
cargo install lowfi --features media-controls
|
||||
```
|
||||
|
||||
And make sure `$HOME/.cargo/bin` is added to `$PATH`.
|
||||
@ -154,6 +157,11 @@ Yeah, that's it.
|
||||
> MPRIS is currently an [optional feature](#extra-features) in cargo (enabled with `--features mpris`)
|
||||
> due to it being only for Linux, as well as the fact that the main point of
|
||||
> lowfi is it's unique & minimal interface.
|
||||
>
|
||||
> On **Windows**, terminal media keys (via crossterm) only work while the
|
||||
> terminal is focused. Build with `--features media-controls` to register
|
||||
> System Media Transport Controls (SMTC) so play/pause/next (and volume when
|
||||
> the OS exposes it) work globally, with track metadata in the Windows media overlay.
|
||||
|
||||
### Bookmarks
|
||||
|
||||
@ -200,6 +208,19 @@ More information can be found by running `lowfi help scrape`.
|
||||
|
||||
Enables MPRIS. It's not rocket science.
|
||||
|
||||
#### `media-controls` - Windows media keys (SMTC)
|
||||
|
||||
Enables System Media Transport Controls on Windows via the `souvlaki` crate.
|
||||
Hardware play/pause/next keys work while another app is focused, and the current
|
||||
track can appear in the Windows media overlay. Soft-fails if SMTC cannot be
|
||||
registered so playback still works.
|
||||
|
||||
```sh
|
||||
cargo run --features media-controls
|
||||
# or
|
||||
cargo build --release --features media-controls
|
||||
```
|
||||
|
||||
#### `extra-audio-formats` - Extra Audio Formats
|
||||
|
||||
This is only relevant to those using a custom track list, in which case
|
||||
|
||||
@ -201,6 +201,11 @@ impl Player {
|
||||
|
||||
#[cfg(feature = "mpris")]
|
||||
self.ui.mpris.handle(&message).await?;
|
||||
|
||||
#[cfg(all(feature = "media-controls", windows))]
|
||||
if let Some(controls) = self.ui.media_controls.as_mut() {
|
||||
controls.handle(&message)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
11
src/ui.rs
11
src/ui.rs
@ -15,6 +15,9 @@ pub use interface::Interface;
|
||||
#[cfg(feature = "mpris")]
|
||||
pub mod mpris;
|
||||
|
||||
#[cfg(all(feature = "media-controls", windows))]
|
||||
pub mod media_controls;
|
||||
|
||||
/// Shorthand for a [`Result`] with a [`ui::Error`].
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
@ -47,6 +50,10 @@ pub enum Error {
|
||||
#[cfg(feature = "mpris")]
|
||||
#[error("mpris fdo (zbus interface) error: {0}")]
|
||||
Fdo(#[from] mpris_server::zbus::fdo::Error),
|
||||
|
||||
#[cfg(all(feature = "media-controls", windows))]
|
||||
#[error("media controls error: {0}")]
|
||||
MediaControls(String),
|
||||
}
|
||||
|
||||
/// The UI state, which is all of the information that
|
||||
@ -118,6 +125,10 @@ pub struct Handle {
|
||||
#[cfg(feature = "mpris")]
|
||||
pub mpris: mpris::Server,
|
||||
|
||||
/// Windows SMTC handle; None if registration soft-failed.
|
||||
#[cfg(all(feature = "media-controls", windows))]
|
||||
pub media_controls: Option<media_controls::Server>,
|
||||
|
||||
/// Logger which can be used to log important events.
|
||||
logger: Logger,
|
||||
}
|
||||
|
||||
@ -12,6 +12,10 @@ impl crate::Tasks {
|
||||
#[cfg(feature = "mpris")]
|
||||
let mpris = ui::mpris::Server::new(state.clone(), self.tx(), urx.resubscribe()).await?;
|
||||
|
||||
#[cfg(all(feature = "media-controls", windows))]
|
||||
let media_controls =
|
||||
ui::media_controls::Server::try_new(state.clone(), self.tx(), urx.resubscribe());
|
||||
|
||||
let params = interface::Params::try_from(args)?;
|
||||
let interface = interface::Interface::new(params)?;
|
||||
let logger = interface.logger.clone();
|
||||
@ -26,6 +30,8 @@ impl crate::Tasks {
|
||||
logger,
|
||||
#[cfg(feature = "mpris")]
|
||||
mpris,
|
||||
#[cfg(all(feature = "media-controls", windows))]
|
||||
media_controls,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
245
src/ui/media_controls.rs
Normal file
245
src/ui/media_controls.rs
Normal file
@ -0,0 +1,245 @@
|
||||
//! Windows System Media Transport Controls (SMTC) via [`souvlaki`].
|
||||
//! Soft-fails on init so playback still works without SMTC.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use souvlaki::{
|
||||
MediaControlEvent, MediaControls, MediaMetadata, MediaPlayback, PlatformConfig,
|
||||
};
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
|
||||
use crate::player::Current;
|
||||
use crate::ui::{self, Update};
|
||||
use crate::Message;
|
||||
|
||||
/// Handle to SMTC; keeps the dummy HWND alive and syncs metadata/playback.
|
||||
pub struct Server {
|
||||
controls: MediaControls,
|
||||
/// Must outlive `controls` on Windows.
|
||||
_dummy_window: windows::DummyWindow,
|
||||
pump_shutdown: std::sync::mpsc::Sender<()>,
|
||||
sink: Arc<rodio::Player>,
|
||||
current: Current,
|
||||
list: String,
|
||||
receiver: broadcast::Receiver<Update>,
|
||||
}
|
||||
|
||||
impl Drop for Server {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.pump_shutdown.send(());
|
||||
}
|
||||
}
|
||||
|
||||
impl Server {
|
||||
fn map_event(event: MediaControlEvent) -> Option<Message> {
|
||||
match event {
|
||||
MediaControlEvent::Play => Some(Message::Play),
|
||||
MediaControlEvent::Pause | MediaControlEvent::Stop => Some(Message::Pause),
|
||||
MediaControlEvent::Toggle => Some(Message::PlayPause),
|
||||
MediaControlEvent::Next => Some(Message::Next),
|
||||
MediaControlEvent::Quit => Some(Message::Quit),
|
||||
MediaControlEvent::SetVolume(v) => Some(Message::SetVolume(v as f32)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates SMTC controls, or `None` if registration fails (playback continues).
|
||||
pub fn try_new(
|
||||
state: ui::State,
|
||||
sender: mpsc::Sender<Message>,
|
||||
receiver: broadcast::Receiver<Update>,
|
||||
) -> Option<Self> {
|
||||
match Self::create(state, sender, receiver) {
|
||||
Ok(server) => Some(server),
|
||||
Err(err) => {
|
||||
eprintln!("warning: Windows media controls unavailable: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create(
|
||||
state: ui::State,
|
||||
sender: mpsc::Sender<Message>,
|
||||
receiver: broadcast::Receiver<Update>,
|
||||
) -> Result<Self, String> {
|
||||
let dummy_window = windows::DummyWindow::new()?;
|
||||
let config = PlatformConfig {
|
||||
dbus_name: "lowfi",
|
||||
display_name: "lowfi",
|
||||
hwnd: Some(dummy_window.handle.0 as _),
|
||||
};
|
||||
|
||||
let mut controls =
|
||||
MediaControls::new(config).map_err(|e| format!("MediaControls::new: {e:?}"))?;
|
||||
controls
|
||||
.attach(move |event| {
|
||||
if let Some(msg) = Self::map_event(event) {
|
||||
let _ = sender.try_send(msg);
|
||||
}
|
||||
})
|
||||
.map_err(|e| format!("MediaControls::attach: {e:?}"))?;
|
||||
|
||||
let (pump_shutdown, pump_rx) = std::sync::mpsc::channel();
|
||||
std::thread::Builder::new()
|
||||
.name("lowfi-smtc-pump".into())
|
||||
.spawn(move || {
|
||||
while pump_rx.try_recv().is_err() {
|
||||
windows::pump_events();
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
})
|
||||
.map_err(|e| format!("SMTC pump spawn: {e}"))?;
|
||||
|
||||
let mut server = Self {
|
||||
controls,
|
||||
_dummy_window: dummy_window,
|
||||
pump_shutdown,
|
||||
sink: state.sink,
|
||||
current: state.current,
|
||||
list: state.tracklist,
|
||||
receiver,
|
||||
};
|
||||
let _ = server.sync_playback();
|
||||
let _ = server.sync_metadata();
|
||||
Ok(server)
|
||||
}
|
||||
|
||||
/// Applies player messages to SMTC metadata / playback status.
|
||||
pub fn handle(&mut self, message: &Message) -> ui::Result<()> {
|
||||
while let Ok(update) = self.receiver.try_recv() {
|
||||
if let Update::Track(current) = update {
|
||||
self.current = current;
|
||||
}
|
||||
}
|
||||
|
||||
match message {
|
||||
Message::Play | Message::Pause | Message::PlayPause => self.sync_playback(),
|
||||
Message::Init | Message::Loaded | Message::Next => {
|
||||
self.sync_metadata()?;
|
||||
self.sync_playback()
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_playback(&mut self) -> ui::Result<()> {
|
||||
let status = if self.current.loading() {
|
||||
MediaPlayback::Stopped
|
||||
} else if self.sink.is_paused() {
|
||||
MediaPlayback::Paused { progress: None }
|
||||
} else {
|
||||
MediaPlayback::Playing { progress: None }
|
||||
};
|
||||
self.controls
|
||||
.set_playback(status)
|
||||
.map_err(|e| ui::Error::MediaControls(format!("{e:?}")))
|
||||
}
|
||||
|
||||
fn sync_metadata(&mut self) -> ui::Result<()> {
|
||||
let (title, duration) = match &self.current {
|
||||
Current::Loading(_) => ("Loading...", None),
|
||||
Current::Track(track) => (track.display.as_str(), track.duration),
|
||||
};
|
||||
self.controls
|
||||
.set_metadata(MediaMetadata {
|
||||
title: Some(title),
|
||||
album: Some(self.list.as_str()),
|
||||
artist: Some("lowfi"),
|
||||
duration,
|
||||
..Default::default()
|
||||
})
|
||||
.map_err(|e| ui::Error::MediaControls(format!("{e:?}")))
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimal hidden window + Win32 message pump required by SMTC.
|
||||
mod windows {
|
||||
use std::io::Error;
|
||||
use std::mem;
|
||||
|
||||
use ::windows::core::w;
|
||||
use ::windows::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM};
|
||||
use ::windows::Win32::System::LibraryLoader::GetModuleHandleW;
|
||||
use ::windows::Win32::UI::WindowsAndMessaging::{
|
||||
CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, PeekMessageW,
|
||||
RegisterClassExW, TranslateMessage, MSG, PM_REMOVE, WINDOW_EX_STYLE, WINDOW_STYLE,
|
||||
WM_QUIT, WNDCLASSEXW,
|
||||
};
|
||||
|
||||
pub struct DummyWindow {
|
||||
pub handle: HWND,
|
||||
}
|
||||
|
||||
impl DummyWindow {
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let class = w!("lowfi-smtc");
|
||||
unsafe {
|
||||
let instance = GetModuleHandleW(None).map_err(|e| e.to_string())?;
|
||||
let wnd = WNDCLASSEXW {
|
||||
cbSize: mem::size_of::<WNDCLASSEXW>() as u32,
|
||||
hInstance: instance.into(),
|
||||
lpszClassName: class,
|
||||
lpfnWndProc: Some(Self::wnd_proc),
|
||||
..Default::default()
|
||||
};
|
||||
if RegisterClassExW(&wnd) == 0 {
|
||||
let err = Error::last_os_error();
|
||||
// ERROR_CLASS_ALREADY_EXISTS
|
||||
if err.raw_os_error() != Some(1410) {
|
||||
return Err(err.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let handle = CreateWindowExW(
|
||||
WINDOW_EX_STYLE::default(),
|
||||
class,
|
||||
w!("lowfi"),
|
||||
WINDOW_STYLE::default(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
None,
|
||||
None,
|
||||
Some(instance.into()),
|
||||
None,
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(Self { handle })
|
||||
}
|
||||
}
|
||||
|
||||
extern "system" fn wnd_proc(
|
||||
hwnd: HWND,
|
||||
msg: u32,
|
||||
wparam: WPARAM,
|
||||
lparam: LPARAM,
|
||||
) -> LRESULT {
|
||||
unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DummyWindow {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
let _ = DestroyWindow(self.handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pump_events() {
|
||||
unsafe {
|
||||
let mut msg = MSG::default();
|
||||
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
|
||||
if msg.message == WM_QUIT {
|
||||
break;
|
||||
}
|
||||
let _ = TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user