Skip to content

Commit b5d1ad0

Browse files
authored
linux: add support for multiple media players (#222)
also add wait for setting card profiles to complete
1 parent a6d6e0e commit b5d1ad0

File tree

3 files changed

+112
-51
lines changed

3 files changed

+112
-51
lines changed

linux/media/mediacontroller.cpp

Lines changed: 95 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ void MediaController::handleEarDetection(EarDetection *earDetection)
5353
{
5454
if (getCurrentMediaState() == Playing)
5555
{
56+
LOG_DEBUG("Pausing playback for ear detection");
5657
pause();
5758
}
5859
}
@@ -64,7 +65,7 @@ void MediaController::handleEarDetection(EarDetection *earDetection)
6465
activateA2dpProfile();
6566

6667
// Resume if conditions are met and we previously paused
67-
if (shouldResume && wasPausedByApp && isActiveOutputDeviceAirPods())
68+
if (shouldResume && !pausedByAppServices.isEmpty() && isActiveOutputDeviceAirPods())
6869
{
6970
play();
7071
}
@@ -211,6 +212,7 @@ void MediaController::activateA2dpProfile() {
211212
if (!m_pulseAudio->setCardProfile(m_deviceOutputName, preferredProfile)) {
212213
LOG_ERROR("Failed to activate A2DP profile: " << preferredProfile);
213214
}
215+
LOG_INFO("A2DP profile activated successfully");
214216
}
215217

216218
void MediaController::removeAudioOutputDevice() {
@@ -248,31 +250,53 @@ MediaController::MediaState MediaController::getCurrentMediaState() const
248250
return mediaStateFromPlayerctlOutput(PlayerStatusWatcher::getCurrentPlaybackStatus(""));
249251
}
250252

251-
bool MediaController::sendMediaPlayerCommand(const QString &method)
253+
QStringList MediaController::getPlayingMediaPlayers()
252254
{
253-
// Connect to the session bus
255+
QStringList playingServices;
254256
QDBusConnection bus = QDBusConnection::sessionBus();
255257

256-
// Find available MPRIS-compatible media players
257258
QStringList services = bus.interface()->registeredServiceNames().value();
258-
QStringList mprisServices;
259259
for (const QString &service : services)
260260
{
261-
if (service.startsWith("org.mpris.MediaPlayer2."))
261+
if (!service.startsWith("org.mpris.MediaPlayer2."))
262262
{
263-
mprisServices << service;
263+
continue;
264+
}
265+
266+
QDBusInterface playerInterface(
267+
service,
268+
"/org/mpris/MediaPlayer2",
269+
"org.mpris.MediaPlayer2.Player",
270+
bus);
271+
272+
if (!playerInterface.isValid())
273+
{
274+
continue;
275+
}
276+
277+
QVariant playbackStatus = playerInterface.property("PlaybackStatus");
278+
if (playbackStatus.isValid() && playbackStatus.toString() == "Playing")
279+
{
280+
playingServices << service;
281+
LOG_DEBUG("Found playing service: " << service);
264282
}
265283
}
266284

267-
if (mprisServices.isEmpty())
285+
return playingServices;
286+
}
287+
288+
void MediaController::play()
289+
{
290+
if (pausedByAppServices.isEmpty())
268291
{
269-
LOG_ERROR("No MPRIS-compatible media players found on DBus");
270-
return false;
292+
LOG_INFO("No services to resume");
293+
return;
271294
}
272295

273-
bool success = false;
274-
// Try each MPRIS service until one succeeds
275-
for (const QString &service : mprisServices)
296+
QDBusConnection bus = QDBusConnection::sessionBus();
297+
int resumedCount = 0;
298+
299+
for (const QString &service : pausedByAppServices)
276300
{
277301
QDBusInterface playerInterface(
278302
service,
@@ -282,63 +306,87 @@ bool MediaController::sendMediaPlayerCommand(const QString &method)
282306

283307
if (!playerInterface.isValid())
284308
{
285-
LOG_ERROR("Invalid DBus interface for service: " << service);
309+
LOG_WARN("Service no longer available: " << service);
286310
continue;
287311
}
288312

289-
// Send the Play or Pause command
290-
if (method == "Play" || method == "Pause")
313+
QDBusReply<void> reply = playerInterface.call("Play");
314+
if (reply.isValid())
291315
{
292-
QDBusReply<void> reply = playerInterface.call(method);
293-
if (reply.isValid())
294-
{
295-
LOG_INFO("Successfully sent " << method << " to " << service);
296-
success = true;
297-
break; // Exit after the first successful command
298-
}
299-
else
300-
{
301-
LOG_ERROR("Failed to send " << method << " to " << service
302-
<< ": " << reply.error().message());
303-
}
316+
LOG_INFO("Resumed playback for: " << service);
317+
resumedCount++;
304318
}
305319
else
306320
{
307-
LOG_ERROR("Unsupported method: " << method);
308-
return false;
321+
LOG_ERROR("Failed to resume " << service << ": " << reply.error().message());
309322
}
310323
}
311324

312-
if (!success)
313-
{
314-
LOG_ERROR("No media player responded successfully to " << method);
315-
}
316-
return success;
317-
}
318-
319-
void MediaController::play()
320-
{
321-
if (sendMediaPlayerCommand("Play"))
325+
if (resumedCount > 0)
322326
{
323-
LOG_INFO("Resumed playback via DBus");
324-
wasPausedByApp = false;
327+
LOG_INFO("Resumed " << resumedCount << " media player(s) via DBus");
328+
pausedByAppServices.clear();
325329
}
326330
else
327331
{
328-
LOG_ERROR("Failed to resume playback via DBus");
332+
LOG_ERROR("Failed to resume any media players via DBus");
329333
}
330334
}
331335

332336
void MediaController::pause()
333337
{
334-
if (sendMediaPlayerCommand("Pause"))
338+
QDBusConnection bus = QDBusConnection::sessionBus();
339+
QStringList services = bus.interface()->registeredServiceNames().value();
340+
341+
pausedByAppServices.clear();
342+
int pausedCount = 0;
343+
344+
for (const QString &service : services)
345+
{
346+
if (!service.startsWith("org.mpris.MediaPlayer2."))
347+
{
348+
continue;
349+
}
350+
351+
QDBusInterface playerInterface(
352+
service,
353+
"/org/mpris/MediaPlayer2",
354+
"org.mpris.MediaPlayer2.Player",
355+
bus);
356+
357+
if (!playerInterface.isValid())
358+
{
359+
continue;
360+
}
361+
362+
QVariant playbackStatus = playerInterface.property("PlaybackStatus");
363+
LOG_DEBUG("PlaybackStatus for " << service << ": " << playbackStatus.toString());
364+
if (!playbackStatus.isValid() || playbackStatus.toString() != "Playing")
365+
{
366+
continue;
367+
}
368+
369+
QDBusReply<void> reply = playerInterface.call("Pause");
370+
LOG_DEBUG("Pausing service: " << service);
371+
if (reply.isValid())
372+
{
373+
LOG_INFO("Paused playback for: " << service);
374+
pausedByAppServices << service;
375+
pausedCount++;
376+
}
377+
else
378+
{
379+
LOG_ERROR("Failed to pause " << service << ": " << reply.error().message());
380+
}
381+
}
382+
383+
if (pausedCount > 0)
335384
{
336-
LOG_INFO("Paused playback via DBus");
337-
wasPausedByApp = true;
385+
LOG_INFO("Paused " << pausedCount << " media player(s) via DBus");
338386
}
339387
else
340388
{
341-
LOG_ERROR("Failed to pause playback via DBus");
389+
LOG_INFO("No playing media players found to pause");
342390
}
343391
}
344392

linux/media/mediacontroller.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ class MediaController : public QObject
5555
private:
5656
MediaState mediaStateFromPlayerctlOutput(const QString &output) const;
5757
QString getAudioDeviceName();
58-
bool sendMediaPlayerCommand(const QString &method);
58+
QStringList getPlayingMediaPlayers();
5959

60-
bool wasPausedByApp = false;
60+
QStringList pausedByAppServices;
6161
int initialVolume = -1;
6262
QString connectedDeviceMacAddress;
6363
EarDetectionBehavior earDetectionBehavior = PauseWhenOneRemoved;

linux/media/pulseaudiocontroller.cpp

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,14 @@ bool PulseAudioController::setSinkVolume(const QString &sinkName, int volumePerc
157157
pa_cvolume_set(&volume, 2, (volumePercent * PA_VOLUME_NORM) / 100);
158158

159159
pa_threaded_mainloop_lock(m_mainloop);
160-
pa_operation *op = pa_context_set_sink_volume_by_name(m_context, sinkName.toUtf8().constData(), &volume, nullptr, nullptr);
160+
161+
auto successCallback = [](pa_context *c, int success, void *userdata) {
162+
pa_threaded_mainloop *mainloop = static_cast<pa_threaded_mainloop*>(userdata);
163+
pa_threaded_mainloop_signal(mainloop, 0);
164+
};
165+
166+
pa_operation *op = pa_context_set_sink_volume_by_name(m_context, sinkName.toUtf8().constData(), &volume, successCallback, m_mainloop);
167+
161168
bool success = waitForOperation(op);
162169
if (op) pa_operation_unref(op);
163170
pa_threaded_mainloop_unlock(m_mainloop);
@@ -170,10 +177,16 @@ bool PulseAudioController::setCardProfile(const QString &cardName, const QString
170177
if (!m_initialized) return false;
171178

172179
pa_threaded_mainloop_lock(m_mainloop);
180+
181+
auto successCallback = [](pa_context *c, int success, void *userdata) {
182+
pa_threaded_mainloop *mainloop = static_cast<pa_threaded_mainloop*>(userdata);
183+
pa_threaded_mainloop_signal(mainloop, 0);
184+
};
185+
173186
pa_operation *op = pa_context_set_card_profile_by_name(m_context,
174187
cardName.toUtf8().constData(),
175188
profileName.toUtf8().constData(),
176-
nullptr, nullptr);
189+
successCallback, m_mainloop);
177190
bool success = waitForOperation(op);
178191
if (op) pa_operation_unref(op);
179192
pa_threaded_mainloop_unlock(m_mainloop);

0 commit comments

Comments
 (0)