Skip to content

feat: enable macOS media key support via AppKit run loop#726

Merged
hasezoey merged 2 commits into
tramhao:masterfrom
grumbachraphael-blip:master
Jul 9, 2026
Merged

feat: enable macOS media key support via AppKit run loop#726
hasezoey merged 2 commits into
tramhao:masterfrom
grumbachraphael-blip:master

Conversation

@grumbachraphael-blip

Copy link
Copy Markdown
Contributor

Summary

Fixes macOS media keys (F-row play/pause/next/previous) not reaching termusic — instead they opened Apple Music or were ignored.

Root cause

souvlaki registers MPRemoteCommandCenter handlers for media key events,
but macOS delivers those events exclusively through AppKit's main run
loop. Rust CLI binaries don't initialize an NSApplication run loop by
default, so the registered handlers never received callbacks.

Additionally, MPRemoteCommandCenter dispatches callbacks via GCD's main
dispatch queue, which executes blocks on the process's main thread. The
existing #[tokio::main] setup monopolized the main thread, preventing
GCD from dispatching the callbacks.

Changes

— macOS module

  • init_macos_main_thread(): Initializes NSApplication with
    ActivationPolicyAccessory (no Dock icon, but can receive media
    events).
  • pump_run_loop(): Pumps the main CFRunLoop in 50ms intervals,
    processing GCD blocks from MPRemoteCommandCenter.
  • 5-second threshold on Previous: if >5s into the current track,
    restart it; if ≤5s, skip to the previous track (standard behaviour
    matching Spotify / Apple Music).

— Main thread restructure (macOS only)

  • macOS: spawn the Tokio async runtime on a background thread, freeing
    the main thread to pump CFRunLoop for AppKit event dispatch.
  • Other platforms: unchanged (tokio::runtime::Runtime::new().block_on(actual_main())
    instead of #[tokio::main]).

  • Added objc = 0.2 gated behind [target.'cfg(target_os = \"macos\")'.dependencies].

Code style

  • #[allow(unsafe_code)] on the macos module, matching the existing
    pattern used by the Windows module in the same file.
  • #[allow(unexpected_cfgs)] at the playback crate root for the older
    objc crate's internal cfg(cargo-clippy) usage.

Testing

  1. Build with cargo build --features mpv --release on macOS.
  2. Run the server, connect with termusic --backend mpv.
  3. Press F7/F8/F9 (play/pause, next, previous) — should control termusic.
  4. Quickly press Previous twice: first press restarts the current track
    (if >5s in), second press skips to the previous track.

Closes #...

@hasezoey hasezoey left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for investigating what the issue is and trying to fix it.
As i cannot test it myself as i dont have a macos device, i would like some feedback from actual macos users.

Aside from that, please remove the changes that are beside the "fix macos media control" changes (ie your Updated TUI to be like I wanted commit).
Additionally, please remove the 5-second threshold on Previous: if >5s into the current track, change as that is also beside the point and would make it inconsistent with the rest of termusic.
All of the changes removed can be another PR if you want.

Comment thread playback/src/lib.rs Outdated
Comment thread playback/src/mpris.rs Outdated
Comment thread server/src/server.rs
Comment thread playback/src/mpris.rs Outdated
@hasezoey hasezoey linked an issue Jul 5, 2026 that may be closed by this pull request
@grumbachraphael-blip

Copy link
Copy Markdown
Contributor Author

Sorry I'm new to this PR world so I didn't think that my commits after the PR was created will be in the PR. I will drop these commits. Thanks.

@grumbachraphael-blip

Copy link
Copy Markdown
Contributor Author

I verified that it works well with the media buttons, but I haven't checked the Now Playing integration yet. I'll test that and report back

@grumbachraphael-blip grumbachraphael-blip force-pushed the master branch 2 times, most recently from 030ac1a to 83ca8e7 Compare July 5, 2026 18:10
@grumbachraphael-blip

Copy link
Copy Markdown
Contributor Author

I changed what you wanted and tested the now playing integration. It It works perfectly fine. You can skip forward, go to the previous song, and play and pause normally. Waiting to be merged...

@hasezoey hasezoey left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for resolving the changes, just some minor style nitpicks left (along with some extra testing).
Also please fix the lint (see failing CI).

Personally, i dont like things that require to run a specific thread, but if that is required to make this work, i have nothing against adding it (except are the things that are created on one thread and require to run on that thread (ex. a MutexGuard)).

Comment thread server/src/server.rs Outdated
Comment thread playback/src/mpris.rs Outdated
Comment thread playback/src/mpris.rs

@hasezoey hasezoey left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some testing has now been done in #640 to confirm this PR is working.
So here is a recap of the things i would like still to be done before merging:

  • consider switching to objc2 or objr as objc is not updated anymore
  • reduce the changes in server.rs

And in #640 it was noted that artwork display seemingly does not work, consider investigating this please.

@grumbachraphael-blip

Copy link
Copy Markdown
Contributor Author

I've tested, and sometimes the artwork doesn't show up and sometimes it does. I will test it thoroughly tomorrow.

@hasezoey

hasezoey commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

I've tested, and sometimes the artwork doesn't show up and sometimes it does

Do you have any indicator as to what tracks work and what dont?

But based on that some do work, it is not the issue that macos does not want it encoded as base64 inline-data instead of a file, right?

grumbachraphael-blip added a commit to grumbachraphael-blip/termusic that referenced this pull request Jul 6, 2026
…llows

Changes per hasezoey's review on PR tramhao#726:
- server/src/server.rs: use run_with_run_loop from macos module
- playback/src/mpris.rs: add run_with_run_loop() to macos module
- playback/src/mpris.rs: objc 0.2 -> objc2 0.6 (AnyClass::get)
- playback/src/lib.rs: remove crate-wide allow(unexpected_cfgs)
- playback/Cargo.toml: objc = "0.2" -> objc2 = "0.6"
@grumbachraphael-blip grumbachraphael-blip force-pushed the master branch 4 times, most recently from eca00d9 to 588b488 Compare July 6, 2026 16:09
@grumbachraphael-blip

Copy link
Copy Markdown
Contributor Author

I've tested, and sometimes the artwork doesn't show up and sometimes it does

Do you have any indicator as to what tracks work and what dont?

But based on that some do work, it is not the issue that macos does not want it encoded as base64 inline-data instead of a file, right?

Root cause of intermittent artwork on macOS:
souvlaki v0.8.3's macOS backend (platform/macos/mod.rs) passes cover_url directly to NSImage initWithContentsOfURL:. This method does not officially support data: URIs — it expects file:// or https://. On some macOS versions or image sizes it may still succeed (Apple's internal URL loading may handle data URIs opportunistically), but the behavior is unreliable and undocumented.
termusic sends all artwork as data:image/{jpg,png};base64,... URIs. Our debug logging confirmed every track has found=true and set_metadata: success — the data is correctly extracted and passed to souvlaki. The failure is 100% in souvlaki's macOS backend not being designed for data URIs.
Fix options:

  1. Upstream (souvlaki): Decode the base64 data URI in the macOS backend, create NSData from the bytes, then NSImage initWithData: instead of initWithContentsOfURL:
  2. Workaround in termusic: Write cover art to a temp file and pass file:// URL; or bypass souvlaki for artwork on macOS entirely using objc2 directly (NSData → NSImage → MPMediaItemArtwork)

@grumbachraphael-blip grumbachraphael-blip force-pushed the master branch 2 times, most recently from 7e56152 to 7be7108 Compare July 6, 2026 16:40
@grumbachraphael-blip

Copy link
Copy Markdown
Contributor Author
2. Workaround in termusic: Write cover art to a temp file and pass file:// URL; or bypass souvlaki for artwork on macOS entirely using objc2 directly (NSData → NSImage → MPMediaItemArtwork)

fixed by writing the cover to tmp file, every time you change the song it replaces the image and the now plaing shows that images, work every time

@hasezoey

hasezoey commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

I am back a night later and see like 20 emails for this pr alone.
With the most recent change about 2 hours ago, so i am gonna ask: is this ready for review again?

@grumbachraphael-blip

Copy link
Copy Markdown
Contributor Author

Sorry for the inconvenience, but i think so, thank you very much

@hasezoey hasezoey left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks better now that objc2 is used, though there are still some style nitpicks, mainly documentation related.

Comment thread playback/src/mpris.rs Outdated
Comment thread playback/src/mpris.rs Outdated
Comment thread playback/src/mpris.rs
Comment thread playback/src/mpris.rs Outdated
Comment thread playback/src/mpris.rs Outdated
Comment thread playback/src/mpris.rs Outdated
Comment thread server/src/server.rs
@hasezoey

hasezoey commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Btw, as a side-effect of using a file for the coverart in mpris again, is that artwork will likely work with GNOME again.

@Porkepix Could you try this PR on your linux machine and see if it still would cause the issue described in #731? This would at least rule-out / narrow down the issue GNOME is having.

@grumbachraphael-blip

grumbachraphael-blip commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Btw, as a side-effect of using a file for the coverart in mpris again, is that artwork will likely work with GNOME again.

As i do not have a Linux machine I cannot check. I will wait for a report from a GNOME user.

@hasezoey

hasezoey commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Btw, as a side-effect of using a file for the coverart in mpris again, is that artwork will likely work with GNOME again.

As i do not have a Linux machine I cannot check. I will wait for a report from a GNOME user.

This was only meant as a side-note and not relevant to the PR being merged.

@Porkepix

Porkepix commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Btw, as a side-effect of using a file for the coverart in mpris again, is that artwork will likely work with GNOME again.

@Porkepix Could you try this PR on your linux machine and see if it still would cause the issue described in #731? This would at least rule-out / narrow down the issue GNOME is having.

Is this PR supposed to have any effect on the matter to specifically try this one?
Also note the crash reported in #731 is from termusic latest stable, 0.13.2, not master; so you could prefer to check this first?

I only build masters on a regular basis on my other Linux computer (better and more recent CPU, and 4x more RAM; on the one where I got the crash I tend to avoid due to how heavy it weighs regarding RAM usage).

@hasezoey

hasezoey commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

I only build masters on a regular basis on my other Linux computer (better and more recent CPU, and 4x more RAM; on the one where I got the crash I tend to avoid due to how heavy it weighs regarding RAM usage).

Ah ok.

Is this PR supposed to have any effect on the matter to specifically try this one?

Yes, because from what i remember on GNOME the artwork was also not working, which is the environment you reported to be using in #731 and because only some songs seemingly had the issue and some did not, so this would help narrow down (or rule-out) what parts cause GNOME to lock-up.

@Porkepix

Porkepix commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

So, building from this branch:

  • cargo build, no issue.
  • cargo build --features cover or cargo build --features cover-ueberzug > issue reproduced.

I can fairly claim issue comes from the code around ueberzug, and this with both ueberzug or ueberzugpp installed (I didn't tried if none is installed, didn't try to build without).

I didn't checked if it was fixing covers not showing for GNOME for other songs though, is it something needed too?

@hasezoey

hasezoey commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

I can fairly claim issue comes from the code around ueberzug, and this with both ueberzug or ueberzugpp installed (I didn't tried if none is installed, didn't try to build without).

Thanks for narrowing the issue.

I didn't checked if it was fixing covers not showing for GNOME for other songs though, is it something needed too?

Yes that would be great, but not necessary.

Comment thread playback/src/mpris.rs Outdated
Comment thread playback/src/mpris.rs Outdated
Comment thread playback/src/mpris.rs Outdated
Comment thread playback/src/mpris.rs Outdated

@hasezoey hasezoey left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks almost ready, still some minor style nitpicks.

Once done, please reduce the commits down to 2 (one for the macos change and one for the coverart change)
Note that the commit message style is meant to include the crate that is modified in the scope field. (see CONTRIBUING, also see recent past commits for reference)

Comment thread playback/src/mpris.rs
Comment on lines +458 to +459
Ok(()) | Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import std::sync::mpsc::RecvTimeoutError

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — imported RecvTimeoutError and used short form in the match arms.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not look applied

Comment thread playback/src/mpris.rs
Comment on lines +61 to +68
let mut path = std::env::temp_dir();
path.push("termusic-cover");
if let Some(mime) = v.mime_type()
&& let Some(ext) = mime.ext()
{
path.set_extension(ext);
}
std::fs::write(&path, v.data())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just remembered we are also doing a similar thing for the ueberzug case already and there always set the JPG extension.

This is just a side-note and does not require any change in this PR.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upon further investigation (which i overlooked), we do always set the JPG extension, but also always convert the image to JPG.

Comment thread server/src/server.rs

@hasezoey hasezoey left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great, minor style nitpicks (which can be fixed after this PR), though i will re-test tomorrow and then merge.

Comment thread playback/src/mpris.rs
Comment on lines +458 to +459
Ok(()) | Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not look applied

Comment thread playback/Cargo.toml
# soundtouch= { git = 'https://github.com/Drewol/soundtouch-rs.git' }

[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any dependency version should be defined (at least currently) in the workspace's Cargo.toml

Comment thread README.md
| :-------------------: | :-------------: |
| `termusic-server.log` | The server logs |
| `termusic-tui.log` | The TUI logs |
| `termusic-cover` | MPRIS cover |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| `termusic-cover` | MPRIS cover |
| `termusic-cover.EXT` | MPRIS cover |

@grumbachraphael-blip

Copy link
Copy Markdown
Contributor Author

so should i make the changes or no?

@hasezoey hasezoey merged commit 24bf17d into tramhao:master Jul 9, 2026
8 checks passed
@hasezoey hasezoey mentioned this pull request Jul 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unable to use media control buttons on macOS

3 participants