Implement music attenuation on Linux
This is implemented by listening for event changes from MPRIS2 clients over D-Bus. Unfortunately there is no good API for this nor a good way to identify current player as there is on Windows, and as such we have to track and queue them manually just to get a seemingly good enough result. This also gives us the issue of needing to guess and manually prioritise which player to use when we first start, as we cannot determine when something before us started.
playerctld is preferred to be read on systems that have it available, however that has the behaviour of setting its status to the most recent change. Those who use this and have media key scripts use it would likely be expecting such behaviour though.
GLib's GIO library is used for D-Bus as it is much more sane to parse messages with. This doesn't introduce any new dependencies as nativefiledialog-extended has GTK3 as a dependency, which depends on GLib and GIO.
This should close #820.
(All my editors were eager to fix inconsistent CRLF's in the CMakeLists, sorry)
Edit:
The new commit by @AlexOxorn removes the queue system which originally made it more similar to Windows, after it was deduced that it may not work as well on this platform. Instead, it checks to see if anything is playing before attenuating, instead of what was attempted to be deemed the "active" player.
All of this in paper sounds good to me, will just take a while to get to reviewing it and making sure the protocol is implemented properly. Thanks for the PR!
Re: https://github.com/hedge-dev/UnleashedRecomp/issues/820 I still think doing something along this line makes sense. I tried using firefox and linux, and neither seemed to cause an issue, though I have not tried the specifc case of running discord through firefox
static std::atomic<bool> g_isPlaying = false;
static void UpdateActiveStatus() {
g_isPlaying = std::ranges::any_of(
g_activePlayers,
[](std::string name) { return g_playerStatus[name] == PlaybackStatus::Playing; }
);
}
Okay wait, I noticed something about how chrome works. I don't know if it's an implemention detail of this, but even if I have chrome play 2 different media, it has only 1 entry in g_playerStatus meaning it will just hold whatever the last value was.
I definitely need to look over this a bit more to understand what it's doing.
At this point, perhaps it would be. I kind of wanted it to be more consistent to Windows but if clients of this are so misbehaved maybe it's not even worth it.
After trying this more it appears maybe the Discord bug shouldn't really be catered for. Not only is using it there not as common, it only seems to happen if you click off a YouTube video while it's still playing. Getting rid of it is as simple as refreshing the page.
...or, your most recent post came through just as I was typing this, because it appears browsers share the same MPRIS2 instance and not just Chrome (as you've just encountered), playing media in another tab resets the state too.
So playing around with it, playerctl --player chromium -F status has the same behaviour as this implementation.
- I play youtube video on TAB 1
- MPRIS is tied to tab 1
- chrome status is set to playing
- attenuation kicks in.
- I move to tab2, start video
- MPRIS is tied to tab 2
- chrome status is still playing
- attenuation continues.
- I pause the vid on tab2
- MPRIS is still tied to tab 2
- chrome status is set to paused
- attenuation stop.
- I move to tab1, without doing ANYTHING else
- MPRIS is tied back to tab1
- chrome status is set to playing
- attenuation starts up again
I don't think there is anything around this. I think chrome itself just ties a single MPRIS instance to the most recent tab with a playing media. I don't think there is anything that can be done otherwise.
Interestingly, firefox works differently.
- I play youtube video on TAB 1
- MPRIS is tied to tab 1
- firefox status is set to playing
- attenuation kicks in.
- I move to tab2, start video
- MPRIS is tied to tab 2
- firefox status is still playing
- attenuation continues.
- I pause the vid on tab2
- MPRIS is still tied to tab 2
- firefox status is set to paused
- attenuation stop.
- I move to tab1, without doing ANYTHING else
- MPRIS is still tied to tab2
- firefox status is still paused
- attenuation still off
- I hit pause then play on tab 1
- MPRIS is tied to tab 1
- firefox status is set to playing
- attenuation kicks in.
At least firefox and chrome have seperate insances from eachother (I tested) Now how multiple chromium based browsers, idk
Oh, I see. Thanks for noticing that. Do you have the Discord desktop application? I'd be interested to know if Electron creates an MPRIS instance, and if whatever "start YouTube embed but switching away says still Playing" issue somehow happens there.
I tried it, and it does not create an MPRIS instance. (screenshot while playing a YT embeded video from discord)
So ya, TL;DR chrome ties MPRIS to the last open tab with playing media firefox ties MPRIS to the last tab that recieved a "play" command This behaviour also holds across multiple windows, not just multiple tabs
Alright, great! That's quite a lot of weight off our shoulders if Electron doesn't, especially with the whole notification sound thing earlier. I suppose Steam also ought to be checked given it's extensive use of CEF nowadays too.
Reading again though:
playerctl --player chromium -F statushas the same behaviour as this implementation.
Assuming by "this implementation" you mean mine, as you have playerctl there's a chance playerctld is running and preferred from the moment it's encountered, which is why you'd be experiencing them to have the same behaviour.
Still a bit torn between which method should be employed without playerctld present though. I also don't really want the chance of anybody reporting any "music doesn't play" issues :P
I sudo apt purge playerctl, and the behaviour from Unleashed recomp is the same, so unless there is some hidden playerctl library/deamon unbenounced to me, it seems like what I described is what happens w/o playerctl
Also until testing about an hour ago, I did not have playerctl. I only installed it to test what happens if multiple tabs in chrome play videos
I also booted up steam, went to a random game page and played the trailer... nothing. Attenuation did not kick in
Hmm, running dbus-monitor, it appears when you switch to/from a tab playing media, the MPRIS session drops from the bus and one immediately appears up again. In my implementation I would think this to shove Chromium to the back of the queue. As for Steam, that's good too.
That probably explains the calls to DBusNameOwnerChanged that I logged. I just mostly ignored it, because it didn't change what was in the g_activePlayers list.
I'm mostly new to MPRIS, so does "name ownership" basically mean something similar to what I described as MPRIS "binding" to a tab?
And to clairify what I mean by "this implementation" I'm mostly talking about how you maintain g_playerStatus and g_activePlayers and how it matches what I saw from playerctl and still see after purging it.
I'm not talking about how you only check the first element g_activePlayers, Mainly because in those tests, I was only checking the one player, so wether you check all or one of the active players made no difference.
I still thing checking every g_activePlayers to see if any of them are playing is best, and through testing both chrome and firefox at the same time, the result was what you would expect.
Take the individual result from the chrome only and firefox only tests, then if either result in the playing status, attenuation turns on, if they are both paused or stopped, then attentuation turns off.
That probably explains the calls to
DBusNameOwnerChangedthat I logged. I just mostly ignored it, because it didn't change what was in theg_activePlayerslist.
logging the MPRISPropertiesChanged method, changing tabs only seems to triger a Metadata property change
D-Bus is weird :(
does "name ownership" basically mean something similar to what I described as MPRIS "binding" to a tab?
Perhaps. I'm no longer in the position to check the exact bus behaviour at the moment so I can't confirm much.
With D-Bus, the name ownership signal is sent and used to detect when something has appeared, disappeared, or changed names on the bus. There is also the concept of a unique name (usually something like :display.pid/:0.14851) and a well-known name (which for an MPRIS client would be something like org.mpris.MediaPlayer2.exampleplayer). The well-known name prefix would be used to identify that this is an MPRIS2 client in this case, and provides a stable way to refer to an application. There's also a thing where you can register a service and it only starts the moment its well-known name is accessed, but that's besides the point.
I still thing checking every g_activePlayers to see if any of them are playing is best
I'm feeling more confident with that now that things have been tested more.
D-Bus is weird :(
Yeah... it is. Be thankful this is only checking if something's playing or not, and not listening for other changes. Another time I tried to use this to listen to metadata changes to print current song in the chat of something, players had differing behaviours, such as Spotify emitting duplicate signals, and I think players not even emitting PropertiesChanged to indicate that metadata had changed at all.
I already have a commit ready. I can either make a pull request to your repo. I could just make a new PR with both our commits. I can send you a patch file. Or I could just leave it to you. Alternatively, I can use github's suggestions feature
That being said, I would like to have my contribution be seen in the form of a commit, but I won't insist on it.
I could make a patch to the repo but mention you in it, as you did provide valuable testing.
I made a review on your PR a bit ago but annoyingly this repository makes them invisible until approved, here's what it said (under the g_bus_get_sync line):
This paired with the
g_object_unrefcall (which would disconnect as the ref count hits zero) seems likely it would reconnect to the session bus every frame, as opposed to the session being kept open if you omitted it, asg_bus_get_syncis documented to share the same connection if one is already open. (That and having blocking calls everywhere else could hurt performance.)
(Not mentioned is since every connect would increase the reference count it could potentially overflow at a certain point anyway)
The only true fix for that would be to only connect once upon start, but what I do with spawning a new thread is much less intrusive and should remove the performance impact of doing all those blocking calls on the main thread.
I made a review on your PR a bit ago but annoyingly this repository makes them invisible until approved, here's what it said (under the g_bus_get_sync line)
Oh ya, that PR was made BEFORE I even knew about this PR, as well as at the time, only learned about the library like 2 hours beforehand. At the time, it was mostly just a curiosity if it could be done on my part. You can ignore that PR entirely.
My commit, is just a minimal change to what you already have, which is significantly better designed as a base. Basically matching the "suggested changes" from the above "review"
Okay so I made a new commit, I unfortunately can't make a PR to your repo because my branch has all of the updated commits since your main branch last forked :(
Compleltely gets rid of the g_activePlayers list and the preferential checks for g_playerctldBus.
Also the any_of check now loops over g_playerStatus | std::views::values
I can still provide a patch 0001-Check-all-active-players-for-Playing-Status.txt https://github.com/hedge-dev/UnleashedRecomp/commit/0cea9c10c2620e3f6ef9ab671de9b708bcf3eebd
Right, I'll pull so you can PR and hope it doesn't explode, one moment.
Also fell into the damn review thing again, I was meant to reply this to you earlier:
Right, SSO would be active here as unique connection names are small enough. Well-known names wouldn't be though so that might've been what I was thinking of. All good then.
PR made https://github.com/dakrk/UnleashedRecomp/pull/1
There we go, thank you!