mirror of
https://github.com/talwat/lowfi
synced 2026-09-15 00:59: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
|
bin: lowfi
|
||||||
name: lowfi-linux-x86_64
|
name: lowfi-linux-x86_64
|
||||||
command: build
|
command: build
|
||||||
|
features: ""
|
||||||
|
|
||||||
- release_for: Windows x86_64
|
- release_for: Windows x86_64
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
@ -26,6 +27,7 @@ jobs:
|
|||||||
bin: lowfi.exe
|
bin: lowfi.exe
|
||||||
name: lowfi-windows-x86_64.exe
|
name: lowfi-windows-x86_64.exe
|
||||||
command: build
|
command: build
|
||||||
|
features: "--features media-controls"
|
||||||
|
|
||||||
- release_for: MacOS x86_64
|
- release_for: MacOS x86_64
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
@ -33,6 +35,7 @@ jobs:
|
|||||||
bin: lowfi
|
bin: lowfi
|
||||||
name: lowfi-darwin-x86_64
|
name: lowfi-darwin-x86_64
|
||||||
command: build
|
command: build
|
||||||
|
features: ""
|
||||||
|
|
||||||
- release_for: MacOS aarch64
|
- release_for: MacOS aarch64
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
@ -40,6 +43,7 @@ jobs:
|
|||||||
bin: lowfi
|
bin: lowfi
|
||||||
name: lowfi-darwin-aarch64
|
name: lowfi-darwin-aarch64
|
||||||
command: build
|
command: build
|
||||||
|
features: ""
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
env:
|
env:
|
||||||
@ -61,7 +65,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
command: ${{ matrix.command }}
|
command: ${{ matrix.command }}
|
||||||
target: ${{ matrix.target }}
|
target: ${{ matrix.target }}
|
||||||
args: "--locked --release"
|
args: --locked --release ${{ matrix.features }}
|
||||||
|
|
||||||
- name: Rename binary
|
- name: Rename binary
|
||||||
run: |
|
run: |
|
||||||
@ -72,4 +76,4 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
gh release upload ${{github.event.release.tag_name}} \
|
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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block2"
|
name = "block2"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
@ -422,6 +428,36 @@ dependencies = [
|
|||||||
"cc",
|
"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]]
|
[[package]]
|
||||||
name = "colorchoice"
|
name = "colorchoice"
|
||||||
version = "1.0.5"
|
version = "1.0.5"
|
||||||
@ -459,6 +495,16 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"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]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
@ -475,6 +521,30 @@ version = "0.8.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
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]]
|
[[package]]
|
||||||
name = "coreaudio-rs"
|
name = "coreaudio-rs"
|
||||||
version = "0.14.2"
|
version = "0.14.2"
|
||||||
@ -516,7 +586,7 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
"windows",
|
"windows 0.62.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -632,6 +702,12 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"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]]
|
[[package]]
|
||||||
name = "dispatch2"
|
name = "dispatch2"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@ -802,6 +878,21 @@ version = "1.0.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
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]]
|
[[package]]
|
||||||
name = "form_urlencoded"
|
name = "form_urlencoded"
|
||||||
version = "1.2.2"
|
version = "1.2.2"
|
||||||
@ -1449,10 +1540,12 @@ dependencies = [
|
|||||||
"scraper",
|
"scraper",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"souvlaki",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"url",
|
"url",
|
||||||
|
"windows 0.62.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1470,6 +1563,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "malloc_buf"
|
||||||
|
version = "0.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "markup5ever"
|
name = "markup5ever"
|
||||||
version = "0.39.0"
|
version = "0.39.0"
|
||||||
@ -1629,6 +1731,15 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc"
|
||||||
|
version = "0.2.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
|
||||||
|
dependencies = [
|
||||||
|
"malloc_buf",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2"
|
name = "objc2"
|
||||||
version = "0.6.4"
|
version = "0.6.4"
|
||||||
@ -2223,7 +2334,7 @@ version = "0.7.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
|
checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"core-foundation",
|
"core-foundation 0.10.1",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"jni 0.22.4",
|
"jni 0.22.4",
|
||||||
"log",
|
"log",
|
||||||
@ -2308,7 +2419,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.0",
|
"bitflags 2.13.0",
|
||||||
"core-foundation",
|
"core-foundation 0.10.1",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"security-framework-sys",
|
"security-framework-sys",
|
||||||
@ -2493,6 +2604,22 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"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]]
|
[[package]]
|
||||||
name = "stable_deref_trait"
|
name = "stable_deref_trait"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@ -3235,6 +3362,15 @@ version = "0.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
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]]
|
[[package]]
|
||||||
name = "windows"
|
name = "windows"
|
||||||
version = "0.62.2"
|
version = "0.62.2"
|
||||||
|
|||||||
11
Cargo.toml
11
Cargo.toml
@ -21,6 +21,8 @@ repository = "https://github.com/talwat/lowfi"
|
|||||||
default = ["default-tracklist"]
|
default = ["default-tracklist"]
|
||||||
default-tracklist = []
|
default-tracklist = []
|
||||||
mpris = ["dep:mpris-server", "dep:arc-swap"]
|
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"]
|
extra-audio-formats = ["rodio/default"]
|
||||||
scrape = [
|
scrape = [
|
||||||
"dep:serde",
|
"dep:serde",
|
||||||
@ -66,3 +68,12 @@ indicatif = { version = "0.18.4", optional = true }
|
|||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
libc = "0.2.186"
|
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
|
```sh
|
||||||
cargo install lowfi
|
cargo install lowfi
|
||||||
|
|
||||||
# If you want MPRIS support.
|
# If you want MPRIS support (Linux).
|
||||||
cargo install lowfi --features mpris
|
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`.
|
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`)
|
> 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
|
> due to it being only for Linux, as well as the fact that the main point of
|
||||||
> lowfi is it's unique & minimal interface.
|
> 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
|
### Bookmarks
|
||||||
|
|
||||||
@ -200,6 +208,19 @@ More information can be found by running `lowfi help scrape`.
|
|||||||
|
|
||||||
Enables MPRIS. It's not rocket science.
|
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
|
#### `extra-audio-formats` - Extra Audio Formats
|
||||||
|
|
||||||
This is only relevant to those using a custom track list, in which case
|
This is only relevant to those using a custom track list, in which case
|
||||||
|
|||||||
@ -201,6 +201,11 @@ impl Player {
|
|||||||
|
|
||||||
#[cfg(feature = "mpris")]
|
#[cfg(feature = "mpris")]
|
||||||
self.ui.mpris.handle(&message).await?;
|
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(())
|
Ok(())
|
||||||
|
|||||||
11
src/ui.rs
11
src/ui.rs
@ -15,6 +15,9 @@ pub use interface::Interface;
|
|||||||
#[cfg(feature = "mpris")]
|
#[cfg(feature = "mpris")]
|
||||||
pub mod mpris;
|
pub mod mpris;
|
||||||
|
|
||||||
|
#[cfg(all(feature = "media-controls", windows))]
|
||||||
|
pub mod media_controls;
|
||||||
|
|
||||||
/// Shorthand for a [`Result`] with a [`ui::Error`].
|
/// Shorthand for a [`Result`] with a [`ui::Error`].
|
||||||
type Result<T> = std::result::Result<T, Error>;
|
type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
@ -47,6 +50,10 @@ pub enum Error {
|
|||||||
#[cfg(feature = "mpris")]
|
#[cfg(feature = "mpris")]
|
||||||
#[error("mpris fdo (zbus interface) error: {0}")]
|
#[error("mpris fdo (zbus interface) error: {0}")]
|
||||||
Fdo(#[from] mpris_server::zbus::fdo::Error),
|
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
|
/// The UI state, which is all of the information that
|
||||||
@ -118,6 +125,10 @@ pub struct Handle {
|
|||||||
#[cfg(feature = "mpris")]
|
#[cfg(feature = "mpris")]
|
||||||
pub mpris: mpris::Server,
|
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 which can be used to log important events.
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,10 @@ impl crate::Tasks {
|
|||||||
#[cfg(feature = "mpris")]
|
#[cfg(feature = "mpris")]
|
||||||
let mpris = ui::mpris::Server::new(state.clone(), self.tx(), urx.resubscribe()).await?;
|
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 params = interface::Params::try_from(args)?;
|
||||||
let interface = interface::Interface::new(params)?;
|
let interface = interface::Interface::new(params)?;
|
||||||
let logger = interface.logger.clone();
|
let logger = interface.logger.clone();
|
||||||
@ -26,6 +30,8 @@ impl crate::Tasks {
|
|||||||
logger,
|
logger,
|
||||||
#[cfg(feature = "mpris")]
|
#[cfg(feature = "mpris")]
|
||||||
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