Skip to content

Implement music attenuation on Linux #1117

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open

Conversation

dakrk
Copy link

@dakrk dakrk commented Mar 6, 2025

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.

@hyperbx hyperbx requested a review from DarioSamo March 6, 2025 21:30
@DarioSamo
Copy link
Contributor

DarioSamo commented Mar 7, 2025

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!

@AlexOxorn
Copy link

AlexOxorn commented Apr 25, 2025

Re: #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; }
    );
}

@AlexOxorn
Copy link

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.

@dakrk
Copy link
Author

dakrk commented Apr 25, 2025

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.

@AlexOxorn
Copy link

AlexOxorn commented Apr 25, 2025

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

@AlexOxorn
Copy link

AlexOxorn commented Apr 25, 2025

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

@dakrk
Copy link
Author

dakrk commented Apr 25, 2025

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.

@AlexOxorn
Copy link

AlexOxorn commented Apr 25, 2025

I tried it, and it does not create an MPRIS instance. (screenshot while playing a YT embeded video from discord)
image

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

@dakrk
Copy link
Author

dakrk commented Apr 25, 2025

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 status has 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

@AlexOxorn
Copy link

AlexOxorn commented Apr 25, 2025

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

@AlexOxorn
Copy link

I also booted up steam, went to a random game page and played the trailer... nothing. Attenuation did not kick in

@dakrk
Copy link
Author

dakrk commented Apr 25, 2025

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.

@AlexOxorn
Copy link

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?

@AlexOxorn
Copy link

AlexOxorn commented Apr 25, 2025

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.

@AlexOxorn
Copy link

AlexOxorn commented Apr 25, 2025

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.

logging the MPRISPropertiesChanged method, changing tabs only seems to triger a Metadata property change

D-Bus is weird :(

@dakrk
Copy link
Author

dakrk commented Apr 25, 2025

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.

@AlexOxorn
Copy link

AlexOxorn commented Apr 25, 2025

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.

@dakrk
Copy link
Author

dakrk commented Apr 25, 2025

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_unref call (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, as g_bus_get_sync is 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.

Comment on lines 50 to 53
if (!g_activePlayers.empty())
g_activeStatus = g_playerStatus[g_activePlayers.front()];
else
g_activeStatus = PlaybackStatus::Stopped;
Copy link
Author

Choose a reason for hiding this comment

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

You'd be making a copy of each string by it not being a reference, no?
At this point the whole queue system could just be removed if we're doing this.

@AlexOxorn
Copy link

AlexOxorn commented Apr 25, 2025

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"

@AlexOxorn
Copy link

AlexOxorn commented Apr 25, 2025

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
0cea9c1

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.

(All my editors were eager to fix inconsistent CRLF's in the CMakeLists,
sorry)
@dakrk
Copy link
Author

dakrk commented Apr 25, 2025

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.

@AlexOxorn
Copy link

PR made
dakrk#1

@dakrk
Copy link
Author

dakrk commented Apr 25, 2025

There we go, thank you!

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.

Request to add Music Attenuation support on Linux with/via MPRIS sources
3 participants