Skip to content

[Linux] Read MPRIS PlaybackStatus without introspection - #741

Open
harisb2012 wants to merge 1 commit into
librepods-org:mainfrom
harisb2012:fix/mpris-playbackstatus-introspection
Open

[Linux] Read MPRIS PlaybackStatus without introspection#741
harisb2012 wants to merge 1 commit into
librepods-org:mainfrom
harisb2012:fix/mpris-playbackstatus-introspection

Conversation

@harisb2012

@harisb2012 harisb2012 commented Aug 22, 2026

Copy link
Copy Markdown

What goes wrong

With ear detection set to pause when one pod is removed, Spotify pauses correctly but a video playing in Chromium keeps going. It only stops when both pods come out, and that is not the pause path: removeAudioOutputDevice() sets the card profile to off, the sink disappears, and Chromium stops itself.

Why

Chromium answers Introspect with an empty document:

$ busctl --user call org.mpris.MediaPlayer2.chromium.instance11591 \
    /org/mpris/MediaPlayer2 org.freedesktop.DBus.Introspectable Introspect
s "<!DOCTYPE node PUBLIC ...>\n<node>\n</node>\n"

QDBusInterface builds its metaobject from that document, so for Chromium it carries no properties and property("PlaybackStatus") returns an invalid QVariant while the player is plainly Playing. MediaController::pause(), MediaController::getPlayingMediaPlayers() and PlayerStatusWatcher::getCurrentPlaybackStatus() all read the status that way, so Chromium is skipped in the pause loop, and with Chromium as the only player getCurrentMediaState() never returns Playing, so handleEarDetection() does not even reach pause().

Spotify ships a normal introspection document, which is why it was never affected.

Standalone reproduction, no AirPods needed:

repro.cpp
// g++ repro.cpp -o repro $(pkg-config --cflags --libs Qt6DBus Qt6Core)
#include <QCoreApplication>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDBusInterface>
#include <QDBusReply>
#include <cstdio>

int main(int argc, char **argv) {
  QCoreApplication app(argc, argv);
  QDBusConnection bus = QDBusConnection::sessionBus();
  for (const QString &service : bus.interface()->registeredServiceNames().value()) {
    if (!service.startsWith("org.mpris.MediaPlayer2.")) continue;
    QDBusInterface iface(service, "/org/mpris/MediaPlayer2",
                         "org.mpris.MediaPlayer2.Player", bus);
    QVariant viaProperty = iface.property("PlaybackStatus");
    QDBusInterface props(service, "/org/mpris/MediaPlayer2",
                         "org.freedesktop.DBus.Properties", bus);
    QDBusReply<QVariant> viaGet =
        props.call("Get", "org.mpris.MediaPlayer2.Player", "PlaybackStatus");
    printf("%s\n  isValid=%d  property()=[%s]  Properties.Get=[%s]\n",
           qPrintable(service), (int)iface.isValid(),
           qPrintable(viaProperty.toString()), qPrintable(viaGet.value().toString()));
  }
  return 0;
}

With a video playing in Chromium:

org.mpris.MediaPlayer2.chromium.instance11591
  isValid=1  property()=[]  Properties.Get=[Playing]

The fix

Read the property with a direct org.freedesktop.DBus.Properties.Get, which needs no introspection, and route the three status reads through it. Method calls were never affected, because call("Pause") goes out as a message without consulting the metaobject, so pause() and play() are otherwise unchanged.

Worth noting the Rust rewrite on linux/rust already does exactly this in media_controller.rs, using the dbus crate's Properties::get. This brings the Qt tree in line with it.

Relation to #424

#424 targets the same symptom and describes it as browsers that "don't register properly with DBus". That part is not quite right: Chromium registers fine and answers Properties.Get correctly, it just serves an empty introspection document, which is a Qt metaobject problem rather than a registration problem. That PR fixes it by shelling out to playerctl as a fallback and by removing the isActiveOutputDeviceAirPods() check. This one needs no new runtime dependency (playerctl is not installed on my machine, so that fallback would not have helped here) and keeps the active-device check intact.

The MAC formatting issue @Jimoz77 raised on that PR is a separate bug and is not addressed here.

The same fix is up against the omapods fork of this daemon at thisisgm/omarchy-pods#22, where it is running on my machine.

What I ran

Arch Linux, Qt 6.11.1, PipeWire with WirePlumber, Chromium on Wayland, AirPods Pro 3 (A3047).

Before, unpatched daemon with debug logging, one pod removed while Chromium was playing:

Playback status changed:  "Playing"
Parsed Ear Detection Status: Primary - InEar , Secondary - NotInEar
Ear detection status: primaryInEar= true , secondaryInEar= false , isAirPodsActive= true
At least one AirPod is in ear

No Pausing playback for ear detection and no Paused playback for. The video kept playing.

After, same hardware and same gesture:

Paused playback for:  "org.mpris.MediaPlayer2.chromium.instance11591"
Paused  1  media player(s) via DBus
Resumed playback for:  "org.mpris.MediaPlayer2.chromium.instance11591"
Resumed  1  media player(s) via DBus

Resume on reinsert works now too, which it never did for Chromium, since nothing was being recorded in pausedByAppServices.

cmake --build build on linux/ is clean, no new warnings.

One question

The Linux bug report template marks the Qt version as not maintained. I sent this anyway because the tree is still what several downstreams build from, and it is a three line change with no new dependency. Happy to close it if you would rather not carry patches here.

Chromium serves an empty Introspect document, so the metaobject
QDBusInterface builds from it carries no properties and property()
returns an invalid QVariant for a browser that is plainly playing.
Ear detection then skipped it, leaving browsers running with a pod out.
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.

1 participant