Phonon

pulsesupport.cpp
1/*
2 Copyright (C) 2010 Colin Guthrie <cguthrie@mandriva.org>
3 Copyright (C) 2013 Harald Sitter <sitter@kde.org>
4
5 This library is free software; you can redistribute it and/or
6 modify it under the terms of the GNU Lesser General Public
7 License as published by the Free Software Foundation; either
8 version 2.1 of the License, or (at your option) version 3, or any
9 later version accepted by the membership of KDE e.V. (or its
10 successor approved by the membership of KDE e.V.), Nokia Corporation
11 (or its successors, if any) and the KDE Free Qt Foundation, which shall
12 act as a proxy defined in Section 6 of version 3 of the license.
13
14 This library is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17 Lesser General Public License for more details.
18
19 You should have received a copy of the GNU Lesser General Public
20 License along with this library. If not, see <http://www.gnu.org/licenses/>.
21*/
22
23#include "pulsesupport.h"
24
25#include <QAbstractEventDispatcher>
26#include <QApplication>
27#include <QDebug>
28#include <QIcon>
29#include <QMutex>
30#include <QStringList>
31#include <QTimer>
32
33#ifdef HAVE_PULSEAUDIO
34#include "pulsestream_p.h"
35#include <pulse/pulseaudio.h>
36#include <pulse/xmalloc.h>
37#include <pulse/glib-mainloop.h>
38
39#define HAVE_PULSEAUDIO_DEVICE_MANAGER PA_CHECK_VERSION(0,9,21)
40#if HAVE_PULSEAUDIO_DEVICE_MANAGER
41# include <pulse/ext-device-manager.h>
42#endif
43#endif // HAVE_PULSEAUDIO
44
45#include "phononnamespace_p.h"
46#include "platform_p.h"
47
48#define PA_PROP_PHONON_STREAMID "phonon.streamid"
49
50namespace Phonon
51{
52
53QMutex probeMutex;
54static PulseSupport *s_instance = nullptr;
55static bool s_wasShutDown = false;
56static bool s_pulseActive = false;
57
58#ifdef HAVE_PULSEAUDIO
59/***
60* Prints a conditional debug message based on the current debug level
61* If obj is provided, classname and objectname will be printed as well
62*
63* see debugLevel()
64*/
65
66static int debugLevel() {
67 static int level = -1;
68 if (level < 1) {
69 level = 0;
70 QByteArray pulseenv = qgetenv("PHONON_PULSEAUDIO_DEBUG");
71 int l = pulseenv.toInt();
72 if (l > 0)
73 level = (l > 2 ? 2 : l);
74 }
75 return level;
76}
77
78static void logMessage(const QString &message, int priority = 2, QObject *obj=nullptr);
79static void logMessage(const QString &message, int priority, QObject *obj)
80{
81 if (debugLevel() > 0) {
82 QString output;
83 if (obj) {
84 // Strip away namespace from className
85 QByteArray className(obj->metaObject()->className());
86 int nameLength = className.length() - className.lastIndexOf(':') - 1;
87 className = className.right(nameLength);
88 output.asprintf("%s %s (%s %p)", message.toLatin1().constData(),
89 obj->objectName().toLatin1().constData(),
90 className.constData(), obj);
91 }
92 else {
93 output = message;
94 }
95 if (priority <= debugLevel()) {
96 qDebug() << QString::fromLatin1("PulseSupport(%1): %2").arg(priority).arg(output);
97 }
98 }
99}
100
101
102class AudioDevice
103{
104 public:
105 inline
106 AudioDevice(QString name, QString desc, QString icon, uint32_t index)
107 : pulseName(name), pulseIndex(index)
108 {
109 properties["name"] = desc;
110 properties["description"] = QLatin1String(""); // We don't have descriptions (well we do, but we use them as the name!)
111 properties["icon"] = icon;
112 properties["available"] = (index != PA_INVALID_INDEX);
113 properties["isAdvanced"] = false; // Nothing is advanced!
114
115 DeviceAccessList dal;
116 dal.append(DeviceAccess("pulse", desc));
117 properties["deviceAccessList"] = QVariant::fromValue<DeviceAccessList>(dal);
118 }
119
120 // Needed for QMap
121 inline AudioDevice() {}
122
123 QString pulseName;
124 uint32_t pulseIndex;
126};
127bool operator!=(const AudioDevice &a, const AudioDevice &b)
128{
129 return !(a.pulseName == b.pulseName && a.properties == b.properties);
130}
131
132class PulseUserData
133{
134 public:
135 inline
136 PulseUserData()
137 {
138 }
139
140 QMap<QString, AudioDevice> newOutputDevices;
141 QMap<Phonon::Category, QMap<int, int> > newOutputDevicePriorities; // prio, device
142
143 QMap<QString, AudioDevice> newCaptureDevices;
144 QMap<Phonon::CaptureCategory, QMap<int, int> > newCaptureDevicePriorities; // prio, device
145};
146
147static pa_glib_mainloop *s_mainloop = nullptr;
148static pa_context *s_context = nullptr;
149
150
151
152static int s_deviceIndexCounter = 0;
153
154static QMap<QString, int> s_outputDeviceIndexes;
155static QMap<int, AudioDevice> s_outputDevices;
156static QMap<Phonon::Category, QMap<int, int> > s_outputDevicePriorities; // prio, device
157static QMap<QString, PulseStream*> s_outputStreams;
158
159static const Phonon::CaptureCategory s_audioCapCategories[] = {
160 Phonon::NoCaptureCategory,
161 Phonon::CommunicationCaptureCategory,
162 Phonon::RecordingCaptureCategory,
163 Phonon::ControlCaptureCategory
164};
165
166static const int s_audioCapCategoriesCount = sizeof(s_audioCapCategories) / sizeof(Phonon::CaptureCategory);
167
168static QMap<QString, int> s_captureDeviceIndexes;
169static QMap<int, AudioDevice> s_captureDevices;
170static QMap<Phonon::CaptureCategory, QMap<int, int> > s_captureDevicePriorities; // prio, device
171static QMap<QString, PulseStream*> s_captureStreams;
172
173static PulseStream* findStreamByPulseIndex(QMap<QString, PulseStream*> map, uint32_t index)
174{
176 for (it = map.begin(); it != map.end(); ++it)
177 if ((*it)->index() == index)
178 return *it;
179 return nullptr;
180}
181
182static Phonon::Category pulseRoleToPhononCategory(const char *role, bool *success)
183{
184 Q_ASSERT(role);
185 Q_ASSERT(success);
186 *success = true;
187 QByteArray r(role);
188 if (r == "none")
189 return Phonon::NoCategory;
190 if (r == "video")
191 return Phonon::VideoCategory;
192 if (r == "music")
193 return Phonon::MusicCategory;
194 if (r == "game")
195 return Phonon::GameCategory;
196 if (r == "event")
197 return Phonon::NotificationCategory;
198 if (r == "phone")
199 return Phonon::CommunicationCategory;
200 if (r == "a11y")
201 return Phonon::AccessibilityCategory;
202
203 // ^^ "animation" and "production" have no mapping
204
205 *success = false;
206 return Phonon::NoCategory;
207}
208
209static Phonon::CaptureCategory pulseRoleToPhononCaptureCategory(const char *role, bool *success)
210{
211 Q_ASSERT(role);
212 Q_ASSERT(success);
213 *success = true;
214 QByteArray r(role);
215 if (r == "none")
216 return Phonon::NoCaptureCategory;
217 if (r == "phone")
218 return Phonon::CommunicationCaptureCategory;
219 if (r == "production")
220 return Phonon::RecordingCaptureCategory;
221 if (r == "a11y")
222 return Phonon::ControlCaptureCategory;
223
224 *success = false;
225 return Phonon::NoCaptureCategory;
226}
227
228static const QByteArray phononCategoryToPulseRole(Phonon::Category category)
229{
230 switch (category) {
231 case Phonon::NoCategory:
232 return QByteArray("none");
233 case Phonon::VideoCategory:
234 return QByteArray("video");
235 case Phonon::MusicCategory:
236 return QByteArray("music");
237 case Phonon::GameCategory:
238 return QByteArray("game");
239 case Phonon::NotificationCategory:
240 return QByteArray("event");
241 case Phonon::CommunicationCategory:
242 return QByteArray("phone");
243 case Phonon::AccessibilityCategory:
244 return QByteArray("a11y");
245 default:
246 return QByteArray();
247 }
248}
249
250static const QByteArray phononCaptureCategoryToPulseRole(Phonon::CaptureCategory category)
251{
252 switch (category) {
253 case Phonon::NoCaptureCategory:
254 return QByteArray("none");
255 case Phonon::CommunicationCaptureCategory:
256 return QByteArray("phone");
257 case Phonon::RecordingCaptureCategory:
258 return QByteArray("production");
259 case Phonon::ControlCaptureCategory:
260 return QByteArray("a11y");
261 default:
262 return QByteArray();
263 }
264}
265
266static void createGenericDevices()
267{
268 // OK so we don't have the device manager extension, but we can show a single device and fake it.
269 int index;
270 s_outputDeviceIndexes.clear();
271 s_outputDevices.clear();
272 s_outputDevicePriorities.clear();
273 index = s_deviceIndexCounter++;
274 s_outputDeviceIndexes.insert(QLatin1String("sink:default"), index);
275 s_outputDevices.insert(index, AudioDevice(QLatin1String("sink:default"), QObject::tr("PulseAudio Sound Server"), QLatin1String("audio-backend-pulseaudio"), 0));
276 for (int i = Phonon::NoCategory; i <= Phonon::LastCategory; ++i) {
277 Phonon::Category cat = static_cast<Phonon::Category>(i);
278 s_outputDevicePriorities[cat].insert(0, index);
279 }
280
281 s_captureDeviceIndexes.clear();
282 s_captureDevices.clear();
283 s_captureDevicePriorities.clear();
284 index = s_deviceIndexCounter++;
285 s_captureDeviceIndexes.insert(QLatin1String("source:default"), index);
286 s_captureDevices.insert(index, AudioDevice(QLatin1String("source:default"), QObject::tr("PulseAudio Sound Server"), QLatin1String("audio-backend-pulseaudio"), 0));
287 for (int i = 0; i < s_audioCapCategoriesCount; ++i) {
288 Phonon::CaptureCategory cat = s_audioCapCategories[i];
289 s_captureDevicePriorities[cat].insert(0, index);
290 }
291}
292
293#if HAVE_PULSEAUDIO_DEVICE_MANAGER
294static void ext_device_manager_read_cb(pa_context *c, const pa_ext_device_manager_info *info, int eol, void *userdata) {
295 Q_ASSERT(c);
296 Q_ASSERT(userdata);
297
298 PulseUserData *u = reinterpret_cast<PulseUserData*>(userdata);
299
300 if (eol < 0) {
301 logMessage(QString::fromLatin1("Failed to initialize device manager extension: %1").arg(pa_strerror(pa_context_errno(c))));
302 if (s_context != c) {
303 logMessage(QLatin1String("Falling back to single device mode"));
304 // Only create our generic devices during the probe phase.
305 createGenericDevices();
306 // As this is our probe phase, exit immediately
307 pa_context_disconnect(c);
308 }
309 delete u;
310
311 return;
312 }
313
314 if (eol) {
315 // We're done reading the data, so order it by priority and copy it into the
316 // static variables where it can then be accessed by those classes that need it.
317
319
320 // Check for new output devices or things changing about known output devices.
321 bool output_changed = false;
322 for (newdev_it = u->newOutputDevices.begin(); newdev_it != u->newOutputDevices.end(); ++newdev_it) {
323 QString name = newdev_it.key();
324
325 // The name + index map is always written when a new device is added.
326 Q_ASSERT(s_outputDeviceIndexes.contains(name));
327
328 int index = s_outputDeviceIndexes[name];
329 if (!s_outputDevices.contains(index)) {
330 // This is a totally new device
331 output_changed = true;
332 logMessage(QString("Brand New Output Device Found."));
333 s_outputDevices.insert(index, *newdev_it);
334 } else if (s_outputDevices[index] != *newdev_it) {
335 // We have this device already, but is it different?
336 output_changed = true;
337 logMessage(QString("Change to Existing Output Device (may be Added/Removed or something else)"));
338 s_outputDevices.remove(index);
339 s_outputDevices.insert(index, *newdev_it);
340 }
341 }
342 // Go through the output devices we know about and see if any are no longer mentioned in the list.
343 QMutableMapIterator<QString, int> output_existing_it(s_outputDeviceIndexes);
344 while (output_existing_it.hasNext()) {
345 output_existing_it.next();
346 if (!u->newOutputDevices.contains(output_existing_it.key())) {
347 output_changed = true;
348 logMessage(QString("Output Device Completely Removed"));
349 s_outputDevices.remove(output_existing_it.value());
350 output_existing_it.remove();
351 }
352 }
353
354 // Check for new capture devices or things changing about known capture devices.
355 bool capture_changed = false;
356 for (newdev_it = u->newCaptureDevices.begin(); newdev_it != u->newCaptureDevices.end(); ++newdev_it) {
357 QString name = newdev_it.key();
358
359 // The name + index map is always written when a new device is added.
360 Q_ASSERT(s_captureDeviceIndexes.contains(name));
361
362 int index = s_captureDeviceIndexes[name];
363 if (!s_captureDevices.contains(index)) {
364 // This is a totally new device
365 capture_changed = true;
366 logMessage(QString("Brand New Capture Device Found."));
367 s_captureDevices.insert(index, *newdev_it);
368 } else if (s_captureDevices[index] != *newdev_it) {
369 // We have this device already, but is it different?
370 capture_changed = true;
371 logMessage(QString("Change to Existing Capture Device (may be Added/Removed or something else)"));
372 s_captureDevices.remove(index);
373 s_captureDevices.insert(index, *newdev_it);
374 }
375 }
376 // Go through the capture devices we know about and see if any are no longer mentioned in the list.
377 QMutableMapIterator<QString, int> capture_existing_it(s_captureDeviceIndexes);
378 while (capture_existing_it.hasNext()) {
379 capture_existing_it.next();
380 if (!u->newCaptureDevices.contains(capture_existing_it.key())) {
381 capture_changed = true;
382 logMessage(QString("Capture Device Completely Removed"));
383 s_captureDevices.remove(capture_existing_it.value());
384 capture_existing_it.remove();
385 }
386 }
387
388 // Just copy across the new priority lists as we know they are valid
389 if (s_outputDevicePriorities != u->newOutputDevicePriorities) {
390 output_changed = true;
391 s_outputDevicePriorities = u->newOutputDevicePriorities;
392 }
393 if (s_captureDevicePriorities != u->newCaptureDevicePriorities) {
394 capture_changed = true;
395 s_captureDevicePriorities = u->newCaptureDevicePriorities;
396 }
397
398 if (s_instance) {
399 // This won't be emitted during the connection probe phase
400 // which is intentional
401 if (output_changed)
402 s_instance->emitObjectDescriptionChanged(AudioOutputDeviceType);
403 if (capture_changed)
404 s_instance->emitObjectDescriptionChanged(AudioCaptureDeviceType);
405 }
406
407 // We can free the user data as we will not be called again.
408 delete u;
409
410 // Some debug
411 logMessage(QString("Output Device Priority List:"));
412 for (int i = Phonon::NoCategory; i <= Phonon::LastCategory; ++i) {
413 Phonon::Category cat = static_cast<Phonon::Category>(i);
414 if (s_outputDevicePriorities.contains(cat)) {
415 logMessage(QString(" Phonon Category %1").arg(cat));
416 int count = 0;
417 foreach (int j, s_outputDevicePriorities[cat]) {
418 QHash<QByteArray, QVariant> &props = s_outputDevices[j].properties;
419 logMessage(QString(" %1. %2 (Available: %3)").arg(++count).arg(props["name"].toString()).arg(props["available"].toBool()));
420 }
421 }
422 }
423 logMessage(QString("Capture Device Priority List:"));
424 for (int i = 0; i < s_audioCapCategoriesCount; ++i) {
425 Phonon::CaptureCategory cat = s_audioCapCategories[i];
426 if (s_captureDevicePriorities.contains(cat)) {
427 logMessage(QString(" Phonon Category %1").arg(cat));
428 int count = 0;
429 foreach (int j, s_captureDevicePriorities[cat]) {
430 QHash<QByteArray, QVariant> &props = s_captureDevices[j].properties;
431 logMessage(QString(" %1. %2 (Available: %3)").arg(++count).arg(props["name"].toString()).arg(props["available"].toBool()));
432 }
433 }
434 }
435
436 // If this is our probe phase, exit now as we're finished reading
437 // our device info and can exit and reconnect
438 if (s_context != c)
439 pa_context_disconnect(c);
440
441 return; // eol
442 }
443
444 // If we aren't at eol we expect info to be valid!
445 Q_ASSERT(info);
446 Q_ASSERT(info->name);
447 Q_ASSERT(info->description);
448 Q_ASSERT(info->icon);
449
450 // QString wrapper
451 QString name(info->name);
452 int index;
453 QMap<Phonon::Category, QMap<int, int> > *new_prio_map_cats = nullptr; // prio, device
454 QMap<Phonon::CaptureCategory, QMap<int, int> > *new_prio_map_capcats = nullptr; // prio, device
455 QMap<QString, AudioDevice> *new_devices = nullptr;
456
457 bool isSink = false;
458 bool isSource = false;
459
460 if (name.startsWith(QLatin1String("sink:"))) {
461 isSink = true;
462
463 new_devices = &u->newOutputDevices;
464 new_prio_map_cats = &u->newOutputDevicePriorities;
465
466 if (s_outputDeviceIndexes.contains(name))
467 index = s_outputDeviceIndexes[name];
468 else
469 index = s_outputDeviceIndexes[name] = s_deviceIndexCounter++;
470 } else if (name.startsWith(QLatin1String("source:"))) {
471 isSource = true;
472
473 new_devices = &u->newCaptureDevices;
474 new_prio_map_capcats = &u->newCaptureDevicePriorities;
475
476 if (s_captureDeviceIndexes.contains(name))
477 index = s_captureDeviceIndexes[name];
478 else
479 index = s_captureDeviceIndexes[name] = s_deviceIndexCounter++;
480 } else {
481 // This indicates a bug in pulseaudio.
482 return;
483 }
484
485 Q_ASSERT(new_devices);
486 Q_ASSERT(!isSink || new_prio_map_cats);
487 Q_ASSERT(!isSource || new_prio_map_capcats);
488
489 // Add the new device itself.
490 new_devices->insert(name, AudioDevice(name, QString::fromUtf8(info->description), QString::fromUtf8(info->icon), info->index));
491
492 // For each role in the priority, map it to a phonon category and store the order.
493 for (uint32_t i = 0; i < info->n_role_priorities; ++i) {
494 pa_ext_device_manager_role_priority_info* role_prio = &info->role_priorities[i];
495 Q_ASSERT(role_prio->role);
496
497 bool conversionSuccess;
498
499 if (isSink) {
500 Phonon::Category cat = pulseRoleToPhononCategory(role_prio->role, &conversionSuccess);
501 if (conversionSuccess) {
502 (*new_prio_map_cats)[cat].insert(role_prio->priority, index);
503 }
504 }
505
506 if (isSource) {
507 Phonon::CaptureCategory capcat = pulseRoleToPhononCaptureCategory(role_prio->role, &conversionSuccess);
508 if (conversionSuccess) {
509 (*new_prio_map_capcats)[capcat].insert(role_prio->priority, index);
510 }
511 }
512 }
513}
514
515static void ext_device_manager_subscribe_cb(pa_context *c, void *) {
516 Q_ASSERT(c);
517
518 pa_operation *o;
519 PulseUserData *u = new PulseUserData;
520 if (!(o = pa_ext_device_manager_read(c, ext_device_manager_read_cb, u))) {
521 logMessage(QString::fromLatin1("pa_ext_device_manager_read() failed."));
522 delete u;
523 return;
524 }
525 pa_operation_unref(o);
526}
527#endif
528
529static void sink_input_cb(pa_context *c, const pa_sink_input_info *i, int eol, void *userdata) {
530 Q_UNUSED(userdata);
531 Q_ASSERT(c);
532
533 if (eol < 0) {
534 if (pa_context_errno(c) == PA_ERR_NOENTITY)
535 return;
536
537 logMessage(QLatin1String("Sink input callback failure"));
538 return;
539 }
540
541 if (eol > 0)
542 return;
543
544 Q_ASSERT(i);
545
546 // loop through (*i) and extract phonon->streamindex...
547 const char *t;
548 if ((t = pa_proplist_gets(i->proplist, PA_PROP_PHONON_STREAMID))) {
549 logMessage(QString::fromLatin1("Found PulseAudio stream index %1 for Phonon Output Stream %2").arg(i->index).arg(QLatin1String(t)));
550
551 // We only care about our own streams (other phonon processes are irrelevant)
552 if (s_outputStreams.contains(QLatin1String(t))) {
553 PulseStream *stream = s_outputStreams[QString(t)];
554 stream->setIndex(i->index);
555 stream->setVolume(&i->volume);
556 stream->setMute(!!i->mute);
557
558 // Find the sink's phonon index and notify whoever cares...
559 if (PA_INVALID_INDEX != i->sink) {
561 for (it = s_outputDevices.begin(); it != s_outputDevices.end(); ++it) {
562 if ((*it).pulseIndex == i->sink) {
563 stream->setDevice(it.key());
564 break;
565 }
566 }
567 }
568 }
569 }
570}
571
572static void source_output_cb(pa_context *c, const pa_source_output_info *i, int eol, void *userdata) {
573 Q_UNUSED(userdata);
574 Q_ASSERT(c);
575
576 if (eol < 0) {
577 if (pa_context_errno(c) == PA_ERR_NOENTITY)
578 return;
579
580 logMessage(QLatin1String("Source output callback failure"));
581 return;
582 }
583
584 if (eol > 0)
585 return;
586
587 Q_ASSERT(i);
588
589 // loop through (*i) and extract phonon->streamindex...
590 const char *t;
591 if ((t = pa_proplist_gets(i->proplist, PA_PROP_PHONON_STREAMID))) {
592 logMessage(QString::fromLatin1("Found PulseAudio stream index %1 for Phonon Capture Stream %2").arg(i->index).arg(QLatin1String(t)));
593
594 // We only care about our own streams (other phonon processes are irrelevant)
595 if (s_captureStreams.contains(QLatin1String(t))) {
596 PulseStream *stream = s_captureStreams[QString(t)];
597 stream->setIndex(i->index);
598 //stream->setVolume(&i->volume);
599 //stream->setMute(!!i->mute);
600
601 // Find the source's phonon index and notify whoever cares...
602 if (PA_INVALID_INDEX != i->source) {
604 for (it = s_captureDevices.begin(); it != s_captureDevices.end(); ++it) {
605 if ((*it).pulseIndex == i->source) {
606 stream->setDevice(it.key());
607 break;
608 }
609 }
610 }
611 }
612 }
613}
614
615static void subscribe_cb(pa_context *c, pa_subscription_event_type_t t, uint32_t index, void *userdata) {
616 Q_UNUSED(userdata);
617
618 switch (t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) {
619 case PA_SUBSCRIPTION_EVENT_SINK_INPUT:
620 if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
621 PulseStream *stream = findStreamByPulseIndex(s_outputStreams, index);
622 if (stream) {
623 logMessage(QString::fromLatin1("Phonon Output Stream %1 is gone at the PA end. Marking it as invalid in our cache as we may reuse it.").arg(stream->uuid()));
624 stream->setIndex(PA_INVALID_INDEX);
625 }
626 } else {
627 pa_operation *o;
628 if (!(o = pa_context_get_sink_input_info(c, index, sink_input_cb, nullptr))) {
629 logMessage(QString::fromLatin1("pa_context_get_sink_input_info() failed"));
630 return;
631 }
632 pa_operation_unref(o);
633 }
634 break;
635
636 case PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT:
637 if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
638 PulseStream *stream = findStreamByPulseIndex(s_captureStreams, index);
639 if (stream) {
640 logMessage(QString::fromLatin1("Phonon Capture Stream %1 is gone at the PA end. Marking it as invalid in our cache as we may reuse it.").arg(stream->uuid()));
641 stream->setIndex(PA_INVALID_INDEX);
642 }
643 } else {
644 pa_operation *o;
645 if (!(o = pa_context_get_source_output_info(c, index, source_output_cb, nullptr))) {
646 logMessage(QString::fromLatin1("pa_context_get_sink_input_info() failed"));
647 return;
648 }
649 pa_operation_unref(o);
650 }
651 break;
652 }
653}
654
655
656static QString statename(pa_context_state_t state)
657{
658 switch (state)
659 {
660 case PA_CONTEXT_UNCONNECTED: return QLatin1String("Unconnected");
661 case PA_CONTEXT_CONNECTING: return QLatin1String("Connecting");
662 case PA_CONTEXT_AUTHORIZING: return QLatin1String("Authorizing");
663 case PA_CONTEXT_SETTING_NAME: return QLatin1String("Setting Name");
664 case PA_CONTEXT_READY: return QLatin1String("Ready");
665 case PA_CONTEXT_FAILED: return QLatin1String("Failed");
666 case PA_CONTEXT_TERMINATED: return QLatin1String("Terminated");
667 }
668
669 return QString::fromLatin1("Unknown state: %0").arg(state);
670}
671
672static void context_state_callback(pa_context *c, void *)
673{
674 Q_ASSERT(c);
675
676 logMessage(QString::fromLatin1("context_state_callback %1").arg(statename(pa_context_get_state(c))));
677 pa_context_state_t state = pa_context_get_state(c);
678 if (state == PA_CONTEXT_READY) {
679 // We've connected to PA, so it is active
680 s_pulseActive = true;
681
682 // Attempt to load things up
683 pa_operation *o;
684
685 // 1. Register for the stream changes (except during probe)
686 if (s_context == c) {
687 pa_context_set_subscribe_callback(c, subscribe_cb, nullptr);
688
689 if (!(o = pa_context_subscribe(c, (pa_subscription_mask_t)
690 (PA_SUBSCRIPTION_MASK_SINK_INPUT|
691 PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT), nullptr, nullptr))) {
692 logMessage(QLatin1String("pa_context_subscribe() failed"));
693 return;
694 }
695 pa_operation_unref(o);
696
697 // In the case of reconnection or simply lagging behind the stream object creation
698 // on startup (due to the probe+reconnect system), we invalidate all loaded streams
699 // and then load up info about all streams.
700 for (QMap<QString, PulseStream*>::iterator it = s_outputStreams.begin(); it != s_outputStreams.end(); ++it) {
701 PulseStream *stream = *it;
702 logMessage(QString::fromLatin1("Phonon Output Stream %1 is gone at the PA end. Marking it as invalid in our cache as we may reuse it.").arg(stream->uuid()));
703 stream->setIndex(PA_INVALID_INDEX);
704 }
705 if (!(o = pa_context_get_sink_input_info_list(c, sink_input_cb, nullptr))) {
706 logMessage(QString::fromLatin1("pa_context_get_sink_input_info_list() failed"));
707 return;
708 }
709 pa_operation_unref(o);
710
711 for (QMap<QString, PulseStream*>::iterator it = s_captureStreams.begin(); it != s_captureStreams.end(); ++it) {
712 PulseStream *stream = *it;
713 logMessage(QString::fromLatin1("Phonon Capture Stream %1 is gone at the PA end. Marking it as invalid in our cache as we may reuse it.").arg(stream->uuid()));
714 stream->setIndex(PA_INVALID_INDEX);
715 }
716 if (!(o = pa_context_get_source_output_info_list(c, source_output_cb, nullptr))) {
717 logMessage(QString::fromLatin1("pa_context_get_source_output_info_list() failed"));
718 return;
719 }
720 pa_operation_unref(o);
721 }
722
723#if HAVE_PULSEAUDIO_DEVICE_MANAGER
724 // 2a. Attempt to initialise Device Manager info (except during probe)
725 if (s_context == c) {
726 pa_ext_device_manager_set_subscribe_cb(c, ext_device_manager_subscribe_cb, nullptr);
727 if (!(o = pa_ext_device_manager_subscribe(c, 1, nullptr, nullptr))) {
728 logMessage(QString::fromLatin1("pa_ext_device_manager_subscribe() failed"));
729 return;
730 }
731 pa_operation_unref(o);
732 }
733
734 // 3. Attempt to read info from Device Manager
735 PulseUserData *u = new PulseUserData;
736 if (!(o = pa_ext_device_manager_read(c, ext_device_manager_read_cb, u))) {
737 if (s_context != c) {
738 logMessage(QString::fromLatin1("pa_ext_device_manager_read() failed. Attempting to continue without device manager support"));
739 // Only create our generic devices during the probe phase.
740 createGenericDevices();
741 // As this is our probe phase, exit immediately
742 pa_context_disconnect(c);
743 }
744 delete u;
745
746 return;
747 }
748 pa_operation_unref(o);
749
750#else
751 // If we know do not have Device Manager support, we just create our dummy devices now
752 if (s_context != c) {
753 // Only create our generic devices during the probe phase.
754 createGenericDevices();
755 // As this is our probe phase, exit immediately
756 pa_context_disconnect(c);
757 }
758
759#endif
760 } else if (!PA_CONTEXT_IS_GOOD(state)) {
761 /// @todo Deal with reconnection...
762 //logMessage(QString("Connection to PulseAudio lost: %1").arg(pa_strerror(pa_context_errno(c))));
763
764 // If this is our probe phase, exit our context immediately
765 if (s_context != c)
766 pa_context_disconnect(c);
767 else {
768 pa_context_unref(s_context);
769 s_context = nullptr;
770 QTimer::singleShot(50, PulseSupport::getInstance(), SLOT(connectToDaemon()));
771 }
772 }
773}
774#endif // HAVE_PULSEAUDIO
775
776PulseSupport *PulseSupport::getInstanceOrNull(bool allowNull)
777{
778 if (s_wasShutDown && allowNull) {
779 return nullptr;
780 }
781
782 if (nullptr == s_instance) {
783 /*
784 * In order to prevent the instance being used from multiple threads
785 * prior to it being constructed fully, we need to ensure we obtain a
786 * lock prior to creating it. After we acquire the lock, check to see
787 * if the object is created again before proceeding.
788 */
789 probeMutex.lock();
790 if (nullptr == s_instance)
791 s_instance = new PulseSupport();
792 probeMutex.unlock();
793 }
794 return s_instance;
795}
796
797PulseSupport *PulseSupport::getInstance()
798{
799 return getInstanceOrNull(false);
800}
801
802void PulseSupport::shutdown()
803{
804 if (nullptr != s_instance) {
805 delete s_instance;
806 s_instance = nullptr;
807 s_wasShutDown = true;
808 }
809}
810
811void PulseSupport::debug()
812{
813#ifdef HAVE_PULSEAUDIO
814 logMessage(QString::fromLatin1("Have we been initialised yet? %1").arg(s_instance ? "Yes" : "No"));
815 if (s_instance) {
816 logMessage(QString::fromLatin1("Connected to PulseAudio? %1").arg(s_pulseActive ? "Yes" : "No"));
817 logMessage(QString::fromLatin1("PulseAudio support 'Active'? %1").arg(s_instance->isActive() ? "Yes" : "No"));
818 }
819#endif
820}
821
822PulseSupport::PulseSupport()
823 : QObject()
824 , mEnabled(false)
825 , m_requested(false)
826{
827#ifdef HAVE_PULSEAUDIO
828
829 // To allow for easy debugging, give an easy way to disable this pulseaudio check
830 QByteArray pulseenv = qgetenv("PHONON_PULSEAUDIO_DISABLE");
831 if (pulseenv.toInt()) {
832 logMessage(QLatin1String("PulseAudio support disabled: PHONON_PULSEAUDIO_DISABLE is set"));
833 return;
834 }
835
837 qWarning("WARNING: Cannot construct PulseSupport because there is no Eventloop."
838 " May be because of application shutdown.");
839 return;
840 }
841
842 // We require a glib event loop
843 if (!QByteArray(QAbstractEventDispatcher::instance()->metaObject()->className()).contains("EventDispatcherGlib") &&
844 !QByteArray(QAbstractEventDispatcher::instance()->metaObject()->className()).contains("GlibEventDispatcher")) {
845 qWarning("WARNING: Disabling PulseAudio integration for lack of GLib event loop.");
846 return;
847 }
848
849 // First of all connect to PA via simple/blocking means and if that succeeds,
850 // use a fully async integrated mainloop method to connect and get proper support.
851 pa_mainloop *p_test_mainloop;
852 if (!(p_test_mainloop = pa_mainloop_new())) {
853 logMessage(QLatin1String("PulseAudio support disabled: Unable to create mainloop"));
854 return;
855 }
856
857 pa_context *p_test_context;
858 if (!(p_test_context = pa_context_new(pa_mainloop_get_api(p_test_mainloop), "libphonon-probe"))) {
859 logMessage(QLatin1String("PulseAudio support disabled: Unable to create context"));
860 pa_mainloop_free(p_test_mainloop);
861 return;
862 }
863
864 logMessage(QLatin1String("Probing for PulseAudio..."));
865 // (cg) Convert to PA_CONTEXT_NOFLAGS when PulseAudio 0.9.19 is required
866 if (pa_context_connect(p_test_context, nullptr, static_cast<pa_context_flags_t>(0), nullptr) < 0) {
867 logMessage(QString::fromLatin1("PulseAudio support disabled: %1").arg(QString::fromLocal8Bit(pa_strerror(pa_context_errno(p_test_context)))));
868 pa_context_disconnect(p_test_context);
869 pa_context_unref(p_test_context);
870 pa_mainloop_free(p_test_mainloop);
871 return;
872 }
873
874 pa_context_set_state_callback(p_test_context, &context_state_callback, nullptr);
875 for (;;) {
876 pa_mainloop_iterate(p_test_mainloop, 1, nullptr);
877
878 if (!PA_CONTEXT_IS_GOOD(pa_context_get_state(p_test_context))) {
879 logMessage(QLatin1String("PulseAudio probe complete."));
880 break;
881 }
882 }
883 pa_context_disconnect(p_test_context);
884 pa_context_unref(p_test_context);
885 pa_mainloop_free(p_test_mainloop);
886
887 if (!s_pulseActive) {
888 logMessage(QLatin1String("PulseAudio support is not available."));
889 return;
890 }
891
892 // If we're still here, PA is available.
893 logMessage(QLatin1String("PulseAudio support enabled"));
894
895 // Now we connect for real using a proper main loop that we can forget
896 // all about processing.
897 s_mainloop = pa_glib_mainloop_new(nullptr);
898 Q_ASSERT(s_mainloop);
899
900 connectToDaemon();
901#endif
902}
903
904PulseSupport::~PulseSupport()
905{
906#ifdef HAVE_PULSEAUDIO
907 if (s_context) {
908 pa_context_disconnect(s_context);
909 s_context = nullptr;
910 }
911
912 if (s_mainloop) {
913 pa_glib_mainloop_free(s_mainloop);
914 s_mainloop = nullptr;
915 }
916#endif
917}
918
919
920void PulseSupport::connectToDaemon()
921{
922#ifdef HAVE_PULSEAUDIO
923 pa_mainloop_api *api = pa_glib_mainloop_get_api(s_mainloop);
924
925 s_context = pa_context_new(api, "libphonon");
926 if (pa_context_connect(s_context, nullptr, PA_CONTEXT_NOFAIL, nullptr) >= 0)
927 pa_context_set_state_callback(s_context, &context_state_callback, nullptr);
928#endif
929}
930
931bool PulseSupport::isActive()
932{
933#ifdef HAVE_PULSEAUDIO
934 return mEnabled && isUsed();
935#else
936 return false;
937#endif
938}
939
940bool PulseSupport::isUsed()
941{
942 return isRequested() && isUsable();
943}
944
945bool PulseSupport::isUsable() const
946{
947 return s_pulseActive;
948}
949
950bool PulseSupport::isRequested() const
951{
952 return m_requested;
953}
954
955void PulseSupport::request(bool requested)
956{
957 m_requested = requested;
958}
959
960void PulseSupport::enable(bool enabled)
961{
962 mEnabled = enabled;
963 request(enabled); // compat, enable needs to imply request.
964#ifdef HAVE_PULSEAUDIO
965 logMessage(QString::fromLocal8Bit("Enabled Breakdown: mEnabled: %1, s_pulseActive %2").arg(mEnabled ? "Yes" : "No" ).arg(s_pulseActive ? "Yes" : "No"));
966#endif
967}
968
969QList<int> PulseSupport::objectDescriptionIndexes(ObjectDescriptionType type) const
970{
972
973 if (type != AudioOutputDeviceType && type != AudioCaptureDeviceType)
974 return list;
975
976#ifdef HAVE_PULSEAUDIO
977 if (s_pulseActive) {
978 switch (type) {
979
980 case AudioOutputDeviceType: {
982 for (it = s_outputDeviceIndexes.begin(); it != s_outputDeviceIndexes.end(); ++it) {
983 list.append(*it);
984 }
985 break;
986 }
987 case AudioCaptureDeviceType: {
989 for (it = s_captureDeviceIndexes.begin(); it != s_captureDeviceIndexes.end(); ++it) {
990 list.append(*it);
991 }
992 break;
993 }
994 default:
995 break;
996 }
997 }
998#endif
999
1000 return list;
1001}
1002
1003QHash<QByteArray, QVariant> PulseSupport::objectDescriptionProperties(ObjectDescriptionType type, int index) const
1004{
1006
1007 if (type != AudioOutputDeviceType && type != AudioCaptureDeviceType)
1008 return ret;
1009
1010#ifndef HAVE_PULSEAUDIO
1011 Q_UNUSED(index);
1012#else
1013 if (s_pulseActive) {
1014 switch (type) {
1015
1016 case AudioOutputDeviceType:
1017 Q_ASSERT(s_outputDevices.contains(index));
1018 ret = s_outputDevices[index].properties;
1019 break;
1020
1021 case AudioCaptureDeviceType:
1022 Q_ASSERT(s_captureDevices.contains(index));
1023 ret = s_captureDevices[index].properties;
1024 break;
1025
1026 default:
1027 break;
1028 }
1029 }
1030#endif
1031
1032 return ret;
1033}
1034
1035QList<int> PulseSupport::objectIndexesByCategory(ObjectDescriptionType type, Category category) const
1036{
1037 QList<int> ret;
1038
1039 if (type != AudioOutputDeviceType)
1040 return ret;
1041
1042#ifndef HAVE_PULSEAUDIO
1043 Q_UNUSED(category);
1044#else
1045 if (s_pulseActive) {
1046 if (s_outputDevicePriorities.contains(category))
1047 ret = s_outputDevicePriorities[category].values();
1048 }
1049#endif
1050
1051 return ret;
1052}
1053
1054QList<int> PulseSupport::objectIndexesByCategory(ObjectDescriptionType type, CaptureCategory category) const
1055{
1056 QList<int> ret;
1057
1058 if (type != AudioCaptureDeviceType)
1059 return ret;
1060
1061#ifndef HAVE_PULSEAUDIO
1062 Q_UNUSED(category);
1063#else
1064 if (s_pulseActive) {
1065 if (s_captureDevicePriorities.contains(category))
1066 ret = s_captureDevicePriorities[category].values();
1067 }
1068#endif
1069
1070 return ret;
1071}
1072
1073#ifdef HAVE_PULSEAUDIO
1074static void setDevicePriority(QString role, QStringList list)
1075{
1076 logMessage(QString::fromLatin1("Reindexing %1: %2").arg(role).arg(list.join(QLatin1String(", "))));
1077
1078 char **devices;
1079 devices = pa_xnew(char *, list.size()+1);
1080 int i = 0;
1081 foreach (const QString &str, list) {
1082 devices[i++] = pa_xstrdup(str.toUtf8().constData());
1083 }
1084 devices[list.size()] = nullptr;
1085
1086#if HAVE_PULSEAUDIO_DEVICE_MANAGER
1087 pa_operation *o;
1088 if (!(o = pa_ext_device_manager_reorder_devices_for_role(s_context, role.toUtf8().constData(), (const char**)devices, nullptr, nullptr)))
1089 logMessage(QString::fromLatin1("pa_ext_device_manager_reorder_devices_for_role() failed"));
1090 else
1091 pa_operation_unref(o);
1092#endif
1093
1094 for (i = 0; i < list.size(); ++i)
1095 pa_xfree(devices[i]);
1096 pa_xfree(devices);
1097}
1098
1099static void setDevicePriority(Category category, QStringList list)
1100{
1101 QString role = phononCategoryToPulseRole(category);
1102 if (role.isEmpty())
1103 return;
1104
1105 setDevicePriority(role, list);
1106}
1107
1108static void setDevicePriority(CaptureCategory category, QStringList list)
1109{
1110 QString role = phononCaptureCategoryToPulseRole(category);
1111 if (role.isEmpty())
1112 return;
1113
1114 setDevicePriority(role, list);
1115}
1116#endif // HAVE_PULSEAUDIO
1117
1118void PulseSupport::setOutputDevicePriorityForCategory(Category category, QList<int> order)
1119{
1120#ifndef HAVE_PULSEAUDIO
1121 Q_UNUSED(category);
1122 Q_UNUSED(order);
1123#else
1126
1127 for (it = order.begin(); it != order.end(); ++it) {
1128 if (s_outputDevices.contains(*it)) {
1129 list << s_outputDeviceIndexes.key(*it);
1130 }
1131 }
1132 setDevicePriority(category, list);
1133#endif
1134}
1135
1136void PulseSupport::setCaptureDevicePriorityForCategory(CaptureCategory category, QList<int> order)
1137{
1138#ifndef HAVE_PULSEAUDIO
1139 Q_UNUSED(category);
1140 Q_UNUSED(order);
1141#else
1144
1145 for (it = order.begin(); it != order.end(); ++it) {
1146 if (s_captureDevices.contains(*it)) {
1147 list << s_captureDeviceIndexes.key(*it);
1148 }
1149 }
1150 setDevicePriority(category, list);
1151#endif
1152}
1153
1154void PulseSupport::setCaptureDevicePriorityForCategory(Category category, QList<int> order)
1155{
1156 CaptureCategory cat = categoryToCaptureCategory(category);
1157 setCaptureDevicePriorityForCategory(cat, order);
1158}
1159
1160#ifdef HAVE_PULSEAUDIO
1161static PulseStream* register_stream(QMap<QString,PulseStream*> &map, QString streamUuid, QString role)
1162{
1163 logMessage(QString::fromLatin1("Initialising streamindex %1").arg(streamUuid));
1164
1165 PulseStream *stream = new PulseStream(streamUuid, role);
1166 map[streamUuid] = stream;
1167
1168 // Setup environment...
1169 // These values are considered static, so we force property overrides for them.
1170 if (!Platform::applicationName().isEmpty())
1171 qputenv(QString("PULSE_PROP_OVERRIDE_%1").arg(PA_PROP_APPLICATION_NAME).toUtf8(),
1172 Platform::applicationName().toUtf8());
1173 if (!qApp->applicationVersion().isEmpty())
1174 qputenv(QString("PULSE_PROP_OVERRIDE_%1").arg(PA_PROP_APPLICATION_VERSION).toUtf8(),
1175 qApp->applicationVersion().toUtf8());
1176 if (!qApp->applicationName().isEmpty()) {
1177 QString icon;
1178 if (!qApp->windowIcon().isNull()){
1179 // Try to get the fromTheme() name of the QIcon.
1180 icon = qApp->windowIcon().name();
1181 }
1182 if (icon.isEmpty()) {
1183 // If we failed to get a proper icon name, use the appname instead.
1184 icon = qApp->applicationName().toLower();
1185 }
1186 qputenv(QString("PULSE_PROP_OVERRIDE_%1").arg(PA_PROP_APPLICATION_ICON_NAME).toUtf8(),
1187 icon.toUtf8());
1188 }
1189
1190 return stream;
1191}
1192
1193static PulseStream* register_stream(QMap<QString,PulseStream*> &map, QString streamUuid, Category category)
1194{
1195 QString role = phononCategoryToPulseRole(category);
1196 return register_stream(map, streamUuid, role);
1197}
1198
1199static PulseStream* register_stream(QMap<QString,PulseStream*> &map, QString streamUuid, CaptureCategory category)
1200{
1201 QString role = phononCaptureCategoryToPulseRole(category);
1202 return register_stream(map, streamUuid, role);
1203}
1204
1205#endif
1206
1207PulseStream *PulseSupport::registerOutputStream(QString streamUuid, Category category)
1208{
1209#ifndef HAVE_PULSEAUDIO
1210 Q_UNUSED(streamUuid);
1211 Q_UNUSED(category);
1212 return NULL;
1213#else
1214 return register_stream(s_outputStreams, streamUuid, category);
1215#endif
1216}
1217
1218PulseStream *PulseSupport::registerCaptureStream(QString streamUuid, CaptureCategory category)
1219{
1220#ifndef HAVE_PULSEAUDIO
1221 Q_UNUSED(streamUuid);
1222 Q_UNUSED(category);
1223 return NULL;
1224#else
1225 return register_stream(s_captureStreams, streamUuid, category);
1226#endif
1227}
1228
1229PulseStream *PulseSupport::registerCaptureStream(QString streamUuid, Category category)
1230{
1231#ifndef HAVE_PULSEAUDIO
1232 Q_UNUSED(streamUuid);
1233 Q_UNUSED(category);
1234 return NULL;
1235#else
1236 return register_stream(s_captureStreams, streamUuid, category);
1237#endif
1238}
1239
1240QHash<QString, QString> PulseSupport::streamProperties(QString streamUuid) const
1241{
1243
1244#ifdef HAVE_PULSEAUDIO
1245 PulseStream *stream = nullptr;
1246
1247 // Try to find the stream among the known output streams.
1248 if (!stream)
1249 stream = s_outputStreams.value(streamUuid);
1250
1251 // Not an output stream, try capture streams.
1252 if (!stream)
1253 stream = s_captureStreams.value(streamUuid);
1254
1255 // Also no capture stream, start crying and return an empty hash.
1256 if (!stream) {
1257 qWarning() << Q_FUNC_INFO << "Requested UUID Could not be found. Returning with empty properties.";
1258 return properties;
1259 }
1260
1261 properties[QLatin1String(PA_PROP_PHONON_STREAMID)] = stream->uuid();
1262 properties[QLatin1String(PA_PROP_MEDIA_ROLE)] = stream->role();
1263
1264 // Tear down environment before returning. This is to prevent backends from
1265 // being overridden by the environment if present.
1266 QHashIterator<QString, QString> it(properties);
1267 while (it.hasNext()) {
1268 it.next();
1269 unsetenv(QString("PULSE_PROP_OVERRIDE_%1").arg(it.key()).toUtf8());
1270 }
1271#endif // HAVE_PULSEAUDIO
1272
1273 return properties;
1274}
1275
1276void PulseSupport::setupStreamEnvironment(QString streamUuid)
1277{
1278 pDebug() << "Please note that your current Phonon backend is trying to force"
1279 " stream dependent PulseAudio properties through environment variables."
1280 " Slightly imprecise timing in doing so will cause the first"
1281 " of two subsequently started AudioOutputs to have disfunct volume"
1282 " control. Also see https://bugs.kde.org/show_bug.cgi?id=321288";
1283
1284 const QHash<QString, QString> properties = streamProperties(streamUuid);
1285
1286 QHashIterator<QString, QString> it(properties);
1287 while (it.hasNext()) {
1288 it.next();
1289 pDebug() << "PULSE_PROP_OVERRIDE_" << it.key() << " = " << it.value();
1290 qputenv(QString("PULSE_PROP_OVERRIDE_%1").arg(it.key()).toUtf8(), it.value().toUtf8());
1291 }
1292}
1293
1294void PulseSupport::emitObjectDescriptionChanged(ObjectDescriptionType type)
1295{
1296 if (isUsed())
1297 emit objectDescriptionChanged(type);
1298}
1299
1300bool PulseSupport::setOutputName(QString streamUuid, QString name) {
1301#ifndef HAVE_PULSEAUDIO
1302 Q_UNUSED(streamUuid);
1303 Q_UNUSED(name);
1304 return false;
1305#else
1306 logMessage(QString::fromLatin1("Unimplemented: Need to find a way to set either application.name or media.name in SI proplist"));
1307 Q_UNUSED(streamUuid);
1308 Q_UNUSED(name);
1309 return true;
1310#endif
1311}
1312
1313bool PulseSupport::setOutputDevice(QString streamUuid, int device) {
1314#ifndef HAVE_PULSEAUDIO
1315 Q_UNUSED(streamUuid);
1316 Q_UNUSED(device);
1317 return false;
1318#else
1319 if (s_outputDevices.size() < 2)
1320 return true;
1321
1322 if (!s_outputDevices.contains(device)) {
1323 logMessage(QString::fromLatin1("Attempting to set Output Device for invalid device id %1.").arg(device));
1324 return false;
1325 }
1326 const QVariant var = s_outputDevices[device].properties["name"];
1327 logMessage(QString::fromLatin1("Attempting to set Output Device to '%1' for Output Stream %2").arg(var.toString()).arg(streamUuid));
1328
1329 // Attempt to look up the pulse stream index.
1330 if (s_outputStreams.contains(streamUuid) && s_outputStreams[streamUuid]->index() != PA_INVALID_INDEX) {
1331 logMessage(QString::fromLatin1("... Found in map. Moving now"));
1332
1333 uint32_t pulse_device_index = s_outputDevices[device].pulseIndex;
1334 uint32_t pulse_stream_index = s_outputStreams[streamUuid]->index();
1335
1336 logMessage(QString::fromLatin1("Moving Pulse Sink Input %1 to '%2' (Pulse Sink %3)").arg(pulse_stream_index).arg(var.toString()).arg(pulse_device_index));
1337
1338 /// @todo Find a way to move the stream without saving it... We don't want to pollute the stream restore db.
1339 pa_operation* o;
1340 if (!(o = pa_context_move_sink_input_by_index(s_context, pulse_stream_index, pulse_device_index, nullptr, nullptr))) {
1341 logMessage(QString::fromLatin1("pa_context_move_sink_input_by_index() failed"));
1342 return false;
1343 }
1344 pa_operation_unref(o);
1345 } else {
1346 logMessage(QString::fromLatin1("... Not found in map. We will be notified of the device when the stream appears and we can process any moves needed then"));
1347 }
1348 return true;
1349#endif
1350}
1351
1352bool PulseSupport::setOutputVolume(QString streamUuid, qreal volume) {
1353#ifndef HAVE_PULSEAUDIO
1354 Q_UNUSED(streamUuid);
1355 Q_UNUSED(volume);
1356 return false;
1357#else
1358 logMessage(QString::fromLatin1("Attempting to set volume to %1 for Output Stream %2").arg(volume).arg(streamUuid));
1359
1360 // Attempt to look up the pulse stream index.
1361 if (s_outputStreams.contains(streamUuid) && s_outputStreams[streamUuid]->index() != PA_INVALID_INDEX) {
1362 PulseStream *stream = s_outputStreams[streamUuid];
1363
1364 uint8_t channels = stream->channels();
1365 if (channels < 1) {
1366 logMessage(QString::fromLatin1("Channel count is less than 1. Cannot set volume."));
1367 return false;
1368 }
1369
1370 pa_cvolume vol;
1371 pa_cvolume_set(&vol, channels, (volume * PA_VOLUME_NORM));
1372
1373 logMessage(QString::fromLatin1("Found PA index %1. Calling pa_context_set_sink_input_volume()").arg(stream->index()));
1374 pa_operation* o;
1375 if (!(o = pa_context_set_sink_input_volume(s_context, stream->index(), &vol, nullptr, nullptr))) {
1376 logMessage(QString::fromLatin1("pa_context_set_sink_input_volume() failed"));
1377 return false;
1378 }
1379 pa_operation_unref(o);
1380 } else if (s_outputStreams.contains(streamUuid) && s_outputStreams[streamUuid]->index() == PA_INVALID_INDEX) {
1381 logMessage(QString::fromLatin1("Setting volume on an invalid stream ..... this better be intended"));
1382 PulseStream *stream = s_outputStreams[streamUuid];
1383 stream->setCachedVolume(volume);
1384 }
1385 return true;
1386#endif
1387}
1388
1389bool PulseSupport::setOutputMute(QString streamUuid, bool mute) {
1390#ifndef HAVE_PULSEAUDIO
1391 Q_UNUSED(streamUuid);
1392 Q_UNUSED(mute);
1393 return false;
1394#else
1395 logMessage(QString::fromLatin1("Attempting to %1 mute for Output Stream %2").arg(mute ? "set" : "unset").arg(streamUuid));
1396
1397 // Attempt to look up the pulse stream index.
1398 if (s_outputStreams.contains(streamUuid) && s_outputStreams[streamUuid]->index() != PA_INVALID_INDEX) {
1399 PulseStream *stream = s_outputStreams[streamUuid];
1400
1401 logMessage(QString::fromLatin1("Found PA index %1. Calling pa_context_set_sink_input_mute()").arg(stream->index()));
1402 pa_operation* o;
1403 if (!(o = pa_context_set_sink_input_mute(s_context, stream->index(), (mute ? 1 : 0), nullptr, nullptr))) {
1404 logMessage(QString::fromLatin1("pa_context_set_sink_input_mute() failed"));
1405 return false;
1406 }
1407 pa_operation_unref(o);
1408 }
1409 return true;
1410#endif
1411}
1412
1413bool PulseSupport::setCaptureDevice(QString streamUuid, int device) {
1414#ifndef HAVE_PULSEAUDIO
1415 Q_UNUSED(streamUuid);
1416 Q_UNUSED(device);
1417 return false;
1418#else
1419 if (s_captureDevices.size() < 2)
1420 return true;
1421
1422 if (!s_captureDevices.contains(device)) {
1423 logMessage(QString::fromLatin1("Attempting to set Capture Device for invalid device id %1.").arg(device));
1424 return false;
1425 }
1426 const QVariant var = s_captureDevices[device].properties["name"];
1427 logMessage(QString::fromLatin1("Attempting to set Capture Device to '%1' for Capture Stream %2").arg(var.toString()).arg(streamUuid));
1428
1429 // Attempt to look up the pulse stream index.
1430 if (s_captureStreams.contains(streamUuid) && s_captureStreams[streamUuid]->index() == PA_INVALID_INDEX) {
1431 logMessage(QString::fromLatin1("... Found in map. Moving now"));
1432
1433 uint32_t pulse_device_index = s_captureDevices[device].pulseIndex;
1434 uint32_t pulse_stream_index = s_captureStreams[streamUuid]->index();
1435
1436 logMessage(QString::fromLatin1("Moving Pulse Source Output %1 to '%2' (Pulse Sink %3)").arg(pulse_stream_index).arg(var.toString()).arg(pulse_device_index));
1437
1438 /// @todo Find a way to move the stream without saving it... We don't want to pollute the stream restore db.
1439 pa_operation* o;
1440 if (!(o = pa_context_move_source_output_by_index(s_context, pulse_stream_index, pulse_device_index, nullptr, nullptr))) {
1441 logMessage(QString::fromLatin1("pa_context_move_source_output_by_index() failed"));
1442 return false;
1443 }
1444 pa_operation_unref(o);
1445 } else {
1446 logMessage(QString::fromLatin1("... Not found in map. We will be notified of the device when the stream appears and we can process any moves needed then"));
1447 }
1448 return true;
1449#endif
1450}
1451
1452void PulseSupport::clearStreamCache(QString streamUuid) {
1453#ifndef HAVE_PULSEAUDIO
1454 Q_UNUSED(streamUuid);
1455 return;
1456#else
1457 logMessage(QString::fromLatin1("Clearing stream cache for stream %1").arg(streamUuid));
1458 if (s_outputStreams.contains(streamUuid)) {
1459 PulseStream *stream = s_outputStreams[streamUuid];
1460 s_outputStreams.remove(streamUuid);
1461 delete stream;
1462 } else if (s_captureStreams.contains(streamUuid)) {
1463 PulseStream *stream = s_captureStreams[streamUuid];
1464 s_captureStreams.remove(streamUuid);
1465 delete stream;
1466 }
1467#endif
1468}
1469
1470} // namespace Phonon
1471
1472#include "moc_pulsesupport.cpp"
char * toString(const EngineQuery &query)
KIOCORE_EXPORT bool operator!=(const UDSEntry &entry, const UDSEntry &other)
QStringView level(QStringView ifopt)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
QString name(StandardAction id)
KGuiItem properties()
Category category(StandardShortcut id)
QAbstractEventDispatcher * instance(QThread *thread)
const char * constData() const const
int toInt(bool *ok, int base) const const
void append(QList< T > &&value)
iterator begin()
iterator end()
qsizetype size() const const
iterator begin()
void clear()
bool contains(const Key &key) const const
iterator end()
iterator insert(const Key &key, const T &value)
Key key(const T &value, const Key &defaultKey) const const
size_type remove(const Key &key)
size_type size() const const
T value(const Key &key, const T &defaultValue) const const
QList< T > values() const const
const char * className() const const
void lock()
void unlock()
virtual const QMetaObject * metaObject() const const
QString tr(const char *sourceText, const char *disambiguation, int n)
QString arg(Args &&... args) const const
QString asprintf(const char *cformat,...)
QString fromLatin1(QByteArrayView str)
QString fromLocal8Bit(QByteArrayView str)
QString fromUtf8(QByteArrayView str)
bool isEmpty() const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QByteArray toLatin1() const const
QString toLower() const const
QByteArray toUtf8() const const
QString join(QChar separator) const const
QFuture< void > map(Iterator begin, Iterator end, MapFunctor &&function)
QVariant fromValue(T &&value)
QString toString() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Dec 21 2024 16:58:26 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.