linux: add support for multiple media players (#222)

also add wait for setting card profiles to complete
This commit is contained in:
Kavish Devar
2025-10-02 12:06:44 +05:30
committed by GitHub
parent a6d6e0e13c
commit b5d1ad0dd5
3 changed files with 112 additions and 51 deletions

View File

@@ -53,6 +53,7 @@ void MediaController::handleEarDetection(EarDetection *earDetection)
{ {
if (getCurrentMediaState() == Playing) if (getCurrentMediaState() == Playing)
{ {
LOG_DEBUG("Pausing playback for ear detection");
pause(); pause();
} }
} }
@@ -64,7 +65,7 @@ void MediaController::handleEarDetection(EarDetection *earDetection)
activateA2dpProfile(); activateA2dpProfile();
// Resume if conditions are met and we previously paused // Resume if conditions are met and we previously paused
if (shouldResume && wasPausedByApp && isActiveOutputDeviceAirPods()) if (shouldResume && !pausedByAppServices.isEmpty() && isActiveOutputDeviceAirPods())
{ {
play(); play();
} }
@@ -211,6 +212,7 @@ void MediaController::activateA2dpProfile() {
if (!m_pulseAudio->setCardProfile(m_deviceOutputName, preferredProfile)) { if (!m_pulseAudio->setCardProfile(m_deviceOutputName, preferredProfile)) {
LOG_ERROR("Failed to activate A2DP profile: " << preferredProfile); LOG_ERROR("Failed to activate A2DP profile: " << preferredProfile);
} }
LOG_INFO("A2DP profile activated successfully");
} }
void MediaController::removeAudioOutputDevice() { void MediaController::removeAudioOutputDevice() {
@@ -248,31 +250,53 @@ MediaController::MediaState MediaController::getCurrentMediaState() const
return mediaStateFromPlayerctlOutput(PlayerStatusWatcher::getCurrentPlaybackStatus("")); return mediaStateFromPlayerctlOutput(PlayerStatusWatcher::getCurrentPlaybackStatus(""));
} }
bool MediaController::sendMediaPlayerCommand(const QString &method) QStringList MediaController::getPlayingMediaPlayers()
{ {
// Connect to the session bus QStringList playingServices;
QDBusConnection bus = QDBusConnection::sessionBus(); QDBusConnection bus = QDBusConnection::sessionBus();
// Find available MPRIS-compatible media players
QStringList services = bus.interface()->registeredServiceNames().value(); QStringList services = bus.interface()->registeredServiceNames().value();
QStringList mprisServices;
for (const QString &service : services) for (const QString &service : services)
{ {
if (service.startsWith("org.mpris.MediaPlayer2.")) if (!service.startsWith("org.mpris.MediaPlayer2."))
{ {
mprisServices << service; continue;
}
QDBusInterface playerInterface(
service,
"/org/mpris/MediaPlayer2",
"org.mpris.MediaPlayer2.Player",
bus);
if (!playerInterface.isValid())
{
continue;
}
QVariant playbackStatus = playerInterface.property("PlaybackStatus");
if (playbackStatus.isValid() && playbackStatus.toString() == "Playing")
{
playingServices << service;
LOG_DEBUG("Found playing service: " << service);
} }
} }
if (mprisServices.isEmpty()) return playingServices;
}
void MediaController::play()
{
if (pausedByAppServices.isEmpty())
{ {
LOG_ERROR("No MPRIS-compatible media players found on DBus"); LOG_INFO("No services to resume");
return false; return;
} }
bool success = false; QDBusConnection bus = QDBusConnection::sessionBus();
// Try each MPRIS service until one succeeds int resumedCount = 0;
for (const QString &service : mprisServices)
for (const QString &service : pausedByAppServices)
{ {
QDBusInterface playerInterface( QDBusInterface playerInterface(
service, service,
@@ -282,63 +306,87 @@ bool MediaController::sendMediaPlayerCommand(const QString &method)
if (!playerInterface.isValid()) if (!playerInterface.isValid())
{ {
LOG_ERROR("Invalid DBus interface for service: " << service); LOG_WARN("Service no longer available: " << service);
continue; continue;
} }
// Send the Play or Pause command QDBusReply<void> reply = playerInterface.call("Play");
if (method == "Play" || method == "Pause") if (reply.isValid())
{ {
QDBusReply<void> reply = playerInterface.call(method); LOG_INFO("Resumed playback for: " << service);
if (reply.isValid()) resumedCount++;
{
LOG_INFO("Successfully sent " << method << " to " << service);
success = true;
break; // Exit after the first successful command
}
else
{
LOG_ERROR("Failed to send " << method << " to " << service
<< ": " << reply.error().message());
}
} }
else else
{ {
LOG_ERROR("Unsupported method: " << method); LOG_ERROR("Failed to resume " << service << ": " << reply.error().message());
return false;
} }
} }
if (!success) if (resumedCount > 0)
{ {
LOG_ERROR("No media player responded successfully to " << method); LOG_INFO("Resumed " << resumedCount << " media player(s) via DBus");
} pausedByAppServices.clear();
return success;
}
void MediaController::play()
{
if (sendMediaPlayerCommand("Play"))
{
LOG_INFO("Resumed playback via DBus");
wasPausedByApp = false;
} }
else else
{ {
LOG_ERROR("Failed to resume playback via DBus"); LOG_ERROR("Failed to resume any media players via DBus");
} }
} }
void MediaController::pause() void MediaController::pause()
{ {
if (sendMediaPlayerCommand("Pause")) QDBusConnection bus = QDBusConnection::sessionBus();
QStringList services = bus.interface()->registeredServiceNames().value();
pausedByAppServices.clear();
int pausedCount = 0;
for (const QString &service : services)
{ {
LOG_INFO("Paused playback via DBus"); if (!service.startsWith("org.mpris.MediaPlayer2."))
wasPausedByApp = true; {
continue;
}
QDBusInterface playerInterface(
service,
"/org/mpris/MediaPlayer2",
"org.mpris.MediaPlayer2.Player",
bus);
if (!playerInterface.isValid())
{
continue;
}
QVariant playbackStatus = playerInterface.property("PlaybackStatus");
LOG_DEBUG("PlaybackStatus for " << service << ": " << playbackStatus.toString());
if (!playbackStatus.isValid() || playbackStatus.toString() != "Playing")
{
continue;
}
QDBusReply<void> reply = playerInterface.call("Pause");
LOG_DEBUG("Pausing service: " << service);
if (reply.isValid())
{
LOG_INFO("Paused playback for: " << service);
pausedByAppServices << service;
pausedCount++;
}
else
{
LOG_ERROR("Failed to pause " << service << ": " << reply.error().message());
}
}
if (pausedCount > 0)
{
LOG_INFO("Paused " << pausedCount << " media player(s) via DBus");
} }
else else
{ {
LOG_ERROR("Failed to pause playback via DBus"); LOG_INFO("No playing media players found to pause");
} }
} }

View File

@@ -55,9 +55,9 @@ Q_SIGNALS:
private: private:
MediaState mediaStateFromPlayerctlOutput(const QString &output) const; MediaState mediaStateFromPlayerctlOutput(const QString &output) const;
QString getAudioDeviceName(); QString getAudioDeviceName();
bool sendMediaPlayerCommand(const QString &method); QStringList getPlayingMediaPlayers();
bool wasPausedByApp = false; QStringList pausedByAppServices;
int initialVolume = -1; int initialVolume = -1;
QString connectedDeviceMacAddress; QString connectedDeviceMacAddress;
EarDetectionBehavior earDetectionBehavior = PauseWhenOneRemoved; EarDetectionBehavior earDetectionBehavior = PauseWhenOneRemoved;

View File

@@ -157,7 +157,14 @@ bool PulseAudioController::setSinkVolume(const QString &sinkName, int volumePerc
pa_cvolume_set(&volume, 2, (volumePercent * PA_VOLUME_NORM) / 100); pa_cvolume_set(&volume, 2, (volumePercent * PA_VOLUME_NORM) / 100);
pa_threaded_mainloop_lock(m_mainloop); pa_threaded_mainloop_lock(m_mainloop);
pa_operation *op = pa_context_set_sink_volume_by_name(m_context, sinkName.toUtf8().constData(), &volume, nullptr, nullptr);
auto successCallback = [](pa_context *c, int success, void *userdata) {
pa_threaded_mainloop *mainloop = static_cast<pa_threaded_mainloop*>(userdata);
pa_threaded_mainloop_signal(mainloop, 0);
};
pa_operation *op = pa_context_set_sink_volume_by_name(m_context, sinkName.toUtf8().constData(), &volume, successCallback, m_mainloop);
bool success = waitForOperation(op); bool success = waitForOperation(op);
if (op) pa_operation_unref(op); if (op) pa_operation_unref(op);
pa_threaded_mainloop_unlock(m_mainloop); pa_threaded_mainloop_unlock(m_mainloop);
@@ -170,10 +177,16 @@ bool PulseAudioController::setCardProfile(const QString &cardName, const QString
if (!m_initialized) return false; if (!m_initialized) return false;
pa_threaded_mainloop_lock(m_mainloop); pa_threaded_mainloop_lock(m_mainloop);
auto successCallback = [](pa_context *c, int success, void *userdata) {
pa_threaded_mainloop *mainloop = static_cast<pa_threaded_mainloop*>(userdata);
pa_threaded_mainloop_signal(mainloop, 0);
};
pa_operation *op = pa_context_set_card_profile_by_name(m_context, pa_operation *op = pa_context_set_card_profile_by_name(m_context,
cardName.toUtf8().constData(), cardName.toUtf8().constData(),
profileName.toUtf8().constData(), profileName.toUtf8().constData(),
nullptr, nullptr); successCallback, m_mainloop);
bool success = waitForOperation(op); bool success = waitForOperation(op);
if (op) pa_operation_unref(op); if (op) pa_operation_unref(op);
pa_threaded_mainloop_unlock(m_mainloop); pa_threaded_mainloop_unlock(m_mainloop);