1 | /* This file is part of the KDE project |
2 | Copyright (C) 2009 Colin Guthrie <cguthrie@mandriva.org> |
3 | |
4 | This library is free software; you can redistribute it and/or |
5 | modify it under the terms of the GNU Lesser General Public |
6 | License as published by the Free Software Foundation; either |
7 | version 2.1 of the License, or (at your option) version 3, or any |
8 | later version accepted by the membership of KDE e.V. (or its |
9 | successor approved by the membership of KDE e.V.), Nokia Corporation |
10 | (or its successors, if any) and the KDE Free Qt Foundation, which shall |
11 | act as a proxy defined in Section 6 of version 3 of the license. |
12 | |
13 | This library is distributed in the hope that it will be useful, |
14 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
16 | Lesser General Public License for more details. |
17 | |
18 | You should have received a copy of the GNU Lesser General Public |
19 | License along with this library. If not, see <http://www.gnu.org/licenses/>. |
20 | |
21 | */ |
22 | |
23 | #include <QtCore/QAbstractEventDispatcher> |
24 | #include <QtCore/QEventLoop> |
25 | #include <QtCore/QDebug> |
26 | #include <QtCore/QStringList> |
27 | |
28 | #ifdef HAVE_PULSEAUDIO |
29 | #include <glib.h> |
30 | #include <pulse/pulseaudio.h> |
31 | #include <pulse/xmalloc.h> |
32 | #include <pulse/glib-mainloop.h> |
33 | #ifdef HAVE_PULSEAUDIO_DEVICE_MANAGER |
34 | # include <pulse/ext-device-manager.h> |
35 | #endif |
36 | #endif // HAVE_PULSEAUDIO |
37 | |
38 | #include "pulsesupport.h" |
39 | |
40 | QT_BEGIN_NAMESPACE |
41 | |
42 | namespace Phonon |
43 | { |
44 | |
45 | static PulseSupport* s_instance = NULL; |
46 | |
47 | #ifdef HAVE_PULSEAUDIO |
48 | /*** |
49 | * Prints a conditional debug message based on the current debug level |
50 | * If obj is provided, classname and objectname will be printed as well |
51 | * |
52 | * see debugLevel() |
53 | */ |
54 | |
55 | static int debugLevel() { |
56 | static int level = -1; |
57 | if (level < 1) { |
58 | level = 0; |
59 | QByteArray pulseenv = qgetenv("PHONON_PULSEAUDIO_DEBUG" ); |
60 | int l = pulseenv.toInt(); |
61 | if (l > 0) |
62 | level = (l > 2 ? 2 : l); |
63 | } |
64 | return level; |
65 | } |
66 | |
67 | static void logMessage(const QString &message, int priority = 2, QObject *obj=0); |
68 | static void logMessage(const QString &message, int priority, QObject *obj) |
69 | { |
70 | if (debugLevel() > 0) { |
71 | QString output; |
72 | if (obj) { |
73 | // Strip away namespace from className |
74 | QByteArray className(obj->metaObject()->className()); |
75 | int nameLength = className.length() - className.lastIndexOf(':') - 1; |
76 | className = className.right(nameLength); |
77 | output.sprintf("%s %s (%s %p)" , message.toLatin1().constData(), |
78 | obj->objectName().toLatin1().constData(), |
79 | className.constData(), obj); |
80 | } |
81 | else { |
82 | output = message; |
83 | } |
84 | if (priority <= debugLevel()) { |
85 | qDebug() << QString::fromLatin1("PulseSupport(%1): %2" ).arg(priority).arg(output); |
86 | } |
87 | } |
88 | } |
89 | |
90 | |
91 | class AudioDevice |
92 | { |
93 | public: |
94 | inline |
95 | AudioDevice(QString name, QString desc, QString icon, uint32_t index) |
96 | : pulseName(name), pulseIndex(index) |
97 | { |
98 | properties["name" ] = desc; |
99 | properties["description" ] = QLatin1String("" ); // We don't have descriptions (well we do, but we use them as the name!) |
100 | properties["icon" ] = icon; |
101 | properties["available" ] = (index != PA_INVALID_INDEX); |
102 | properties["isAdvanced" ] = false; // Nothing is advanced! |
103 | } |
104 | |
105 | // Needed for QMap |
106 | inline AudioDevice() {} |
107 | |
108 | QString pulseName; |
109 | uint32_t pulseIndex; |
110 | QHash<QByteArray, QVariant> properties; |
111 | }; |
112 | bool operator!=(const AudioDevice &a, const AudioDevice &b) |
113 | { |
114 | return !(a.pulseName == b.pulseName && a.properties == b.properties); |
115 | } |
116 | |
117 | class PulseUserData |
118 | { |
119 | public: |
120 | inline |
121 | PulseUserData() |
122 | { |
123 | } |
124 | |
125 | QMap<QString, AudioDevice> newOutputDevices; |
126 | QMap<Phonon::Category, QMap<int, int> > newOutputDevicePriorities; // prio, device |
127 | |
128 | QMap<QString, AudioDevice> newCaptureDevices; |
129 | QMap<Phonon::Category, QMap<int, int> > newCaptureDevicePriorities; // prio, device |
130 | }; |
131 | |
132 | static QMap<QString, Phonon::Category> s_roleCategoryMap; |
133 | |
134 | static bool s_pulseActive = false; |
135 | |
136 | static pa_glib_mainloop *s_mainloop = NULL; |
137 | static pa_context *s_context = NULL; |
138 | |
139 | |
140 | |
141 | static int s_deviceIndexCounter = 0; |
142 | |
143 | static QMap<QString, int> s_outputDeviceIndexes; |
144 | static QMap<int, AudioDevice> s_outputDevices; |
145 | static QMap<Phonon::Category, QMap<int, int> > s_outputDevicePriorities; // prio, device |
146 | static QMap<QString, uint32_t> s_outputStreamIndexMap; |
147 | |
148 | static QMap<QString, int> s_captureDeviceIndexes; |
149 | static QMap<int, AudioDevice> s_captureDevices; |
150 | static QMap<Phonon::Category, QMap<int, int> > s_captureDevicePriorities; // prio, device |
151 | static QMap<QString, uint32_t> s_captureStreamIndexMap; |
152 | |
153 | static void createGenericDevices() |
154 | { |
155 | // OK so we don't have the device manager extension, but we can show a single device and fake it. |
156 | int index; |
157 | s_outputDeviceIndexes.clear(); |
158 | s_outputDevices.clear(); |
159 | s_outputDevicePriorities.clear(); |
160 | index = s_deviceIndexCounter++; |
161 | s_outputDeviceIndexes.insert(QLatin1String("sink:default" ), index); |
162 | s_outputDevices.insert(index, AudioDevice(QLatin1String("sink:default" ), QObject::tr("PulseAudio Sound Server" ), QLatin1String("audio-backend-pulseaudio" ), 0)); |
163 | for (int i = Phonon::NoCategory; i <= Phonon::LastCategory; ++i) { |
164 | Phonon::Category cat = static_cast<Phonon::Category>(i); |
165 | s_outputDevicePriorities[cat].insert(0, index); |
166 | } |
167 | |
168 | s_captureDeviceIndexes.clear(); |
169 | s_captureDevices.clear(); |
170 | s_captureDevicePriorities.clear(); |
171 | index = s_deviceIndexCounter++; |
172 | s_captureDeviceIndexes.insert(QLatin1String("source:default" ), index); |
173 | s_captureDevices.insert(index, AudioDevice(QLatin1String("source:default" ), QObject::tr("PulseAudio Sound Server" ), QLatin1String("audio-backend-pulseaudio" ), 0)); |
174 | for (int i = Phonon::NoCategory; i <= Phonon::LastCategory; ++i) { |
175 | Phonon::Category cat = static_cast<Phonon::Category>(i); |
176 | s_captureDevicePriorities[cat].insert(0, index); |
177 | } |
178 | } |
179 | |
180 | #ifdef HAVE_PULSEAUDIO_DEVICE_MANAGER |
181 | static void ext_device_manager_read_cb(pa_context *c, const pa_ext_device_manager_info *info, int eol, void *userdata) { |
182 | Q_ASSERT(c); |
183 | Q_ASSERT(userdata); |
184 | |
185 | PulseUserData *u = reinterpret_cast<PulseUserData*>(userdata); |
186 | |
187 | if (eol < 0) { |
188 | logMessage(QString("Failed to initialize device manager extension: %1" ).arg(pa_strerror(pa_context_errno(c)))); |
189 | logMessage("Falling back to single device mode" ); |
190 | createGenericDevices(); |
191 | delete u; |
192 | |
193 | // If this is our probe phase, exit now |
194 | if (s_context != c) |
195 | pa_context_disconnect(c); |
196 | |
197 | return; |
198 | } |
199 | |
200 | if (eol) { |
201 | // We're done reading the data, so order it by priority and copy it into the |
202 | // static variables where it can then be accessed by those classes that need it. |
203 | |
204 | QMap<QString, AudioDevice>::iterator newdev_it; |
205 | |
206 | // Check for new output devices or things changing about known output devices. |
207 | bool output_changed = false; |
208 | for (newdev_it = u->newOutputDevices.begin(); newdev_it != u->newOutputDevices.end(); ++newdev_it) { |
209 | QString name = newdev_it.key(); |
210 | |
211 | // The name + index map is always written when a new device is added. |
212 | Q_ASSERT(s_outputDeviceIndexes.contains(name)); |
213 | |
214 | int index = s_outputDeviceIndexes[name]; |
215 | if (!s_outputDevices.contains(index)) { |
216 | // This is a totally new device |
217 | output_changed = true; |
218 | logMessage(QString("Brand New Output Device Found." )); |
219 | s_outputDevices.insert(index, *newdev_it); |
220 | } else if (s_outputDevices[index] != *newdev_it) { |
221 | // We have this device already, but is it different? |
222 | output_changed = true; |
223 | logMessage(QString("Change to Existing Output Device (may be Added/Removed or something else)" )); |
224 | s_outputDevices.remove(index); |
225 | s_outputDevices.insert(index, *newdev_it); |
226 | } |
227 | } |
228 | // Go through the output devices we know about and see if any are no longer mentioned in the list. |
229 | QMutableMapIterator<QString, int> output_existing_it(s_outputDeviceIndexes); |
230 | while (output_existing_it.hasNext()) { |
231 | output_existing_it.next(); |
232 | if (!u->newOutputDevices.contains(output_existing_it.key())) { |
233 | output_changed = true; |
234 | logMessage(QString("Output Device Completely Removed" )); |
235 | s_outputDevices.remove(output_existing_it.value()); |
236 | output_existing_it.remove(); |
237 | } |
238 | } |
239 | |
240 | // Check for new capture devices or things changing about known capture devices. |
241 | bool capture_changed = false; |
242 | for (newdev_it = u->newCaptureDevices.begin(); newdev_it != u->newCaptureDevices.end(); ++newdev_it) { |
243 | QString name = newdev_it.key(); |
244 | |
245 | // The name + index map is always written when a new device is added. |
246 | Q_ASSERT(s_captureDeviceIndexes.contains(name)); |
247 | |
248 | int index = s_captureDeviceIndexes[name]; |
249 | if (!s_captureDevices.contains(index)) { |
250 | // This is a totally new device |
251 | capture_changed = true; |
252 | logMessage(QString("Brand New Capture Device Found." )); |
253 | s_captureDevices.insert(index, *newdev_it); |
254 | } else if (s_captureDevices[index] != *newdev_it) { |
255 | // We have this device already, but is it different? |
256 | capture_changed = true; |
257 | logMessage(QString("Change to Existing Capture Device (may be Added/Removed or something else)" )); |
258 | s_captureDevices.remove(index); |
259 | s_captureDevices.insert(index, *newdev_it); |
260 | } |
261 | } |
262 | // Go through the capture devices we know about and see if any are no longer mentioned in the list. |
263 | QMutableMapIterator<QString, int> capture_existing_it(s_captureDeviceIndexes); |
264 | while (capture_existing_it.hasNext()) { |
265 | capture_existing_it.next(); |
266 | if (!u->newCaptureDevices.contains(capture_existing_it.key())) { |
267 | capture_changed = true; |
268 | logMessage(QString("Capture Device Completely Removed" )); |
269 | s_captureDevices.remove(capture_existing_it.value()); |
270 | capture_existing_it.remove(); |
271 | } |
272 | } |
273 | |
274 | // Just copy accross the new priority lists as we know they are valid |
275 | if (s_outputDevicePriorities != u->newOutputDevicePriorities) { |
276 | output_changed = true; |
277 | s_outputDevicePriorities = u->newOutputDevicePriorities; |
278 | } |
279 | if (s_captureDevicePriorities != u->newCaptureDevicePriorities) { |
280 | capture_changed = true; |
281 | s_captureDevicePriorities = u->newCaptureDevicePriorities; |
282 | } |
283 | |
284 | if (s_instance) { |
285 | // This wont be emitted durring the connection probe phase |
286 | // which is intensional |
287 | if (output_changed) |
288 | s_instance->emitObjectDescriptionChanged(AudioOutputDeviceType); |
289 | if (capture_changed) |
290 | s_instance->emitObjectDescriptionChanged(AudioCaptureDeviceType); |
291 | } |
292 | |
293 | // We can free the user data as we will not be called again. |
294 | delete u; |
295 | |
296 | // Some debug |
297 | logMessage(QString("Output Device Priority List:" )); |
298 | for (int i = Phonon::NoCategory; i <= Phonon::LastCategory; ++i) { |
299 | Phonon::Category cat = static_cast<Phonon::Category>(i); |
300 | if (s_outputDevicePriorities.contains(cat)) { |
301 | logMessage(QString(" Phonon Category %1" ).arg(cat)); |
302 | int count = 0; |
303 | foreach (int j, s_outputDevicePriorities[cat]) { |
304 | QHash<QByteArray, QVariant> &props = s_outputDevices[j].properties; |
305 | logMessage(QString(" %1. %2 (Available: %3)" ).arg(++count).arg(props["name" ].toString()).arg(props["available" ].toBool())); |
306 | } |
307 | } |
308 | } |
309 | logMessage(QString("Capture Device Priority List:" )); |
310 | for (int i = Phonon::NoCategory; i <= Phonon::LastCategory; ++i) { |
311 | Phonon::Category cat = static_cast<Phonon::Category>(i); |
312 | if (s_captureDevicePriorities.contains(cat)) { |
313 | logMessage(QString(" Phonon Category %1" ).arg(cat)); |
314 | int count = 0; |
315 | foreach (int j, s_captureDevicePriorities[cat]) { |
316 | QHash<QByteArray, QVariant> &props = s_captureDevices[j].properties; |
317 | logMessage(QString(" %1. %2 (Available: %3)" ).arg(++count).arg(props["name" ].toString()).arg(props["available" ].toBool())); |
318 | } |
319 | } |
320 | } |
321 | |
322 | // If this is our probe phase, exit now as we're finished reading |
323 | // our device info and can exit and reconnect |
324 | if (s_context != c) |
325 | pa_context_disconnect(c); |
326 | } |
327 | |
328 | if (!info) |
329 | return; |
330 | |
331 | Q_ASSERT(info->name); |
332 | Q_ASSERT(info->description); |
333 | Q_ASSERT(info->icon); |
334 | |
335 | // QString wrapper |
336 | QString name(info->name); |
337 | int index; |
338 | QMap<Phonon::Category, QMap<int, int> > *new_prio_map_cats; // prio, device |
339 | QMap<QString, AudioDevice> *new_devices; |
340 | |
341 | if (name.startsWith("sink:" )) { |
342 | new_devices = &u->newOutputDevices; |
343 | new_prio_map_cats = &u->newOutputDevicePriorities; |
344 | |
345 | if (s_outputDeviceIndexes.contains(name)) |
346 | index = s_outputDeviceIndexes[name]; |
347 | else |
348 | index = s_outputDeviceIndexes[name] = s_deviceIndexCounter++; |
349 | } else if (name.startsWith("source:" )) { |
350 | new_devices = &u->newCaptureDevices; |
351 | new_prio_map_cats = &u->newCaptureDevicePriorities; |
352 | |
353 | if (s_captureDeviceIndexes.contains(name)) |
354 | index = s_captureDeviceIndexes[name]; |
355 | else |
356 | index = s_captureDeviceIndexes[name] = s_deviceIndexCounter++; |
357 | } else { |
358 | // This indicates a bug in pulseaudio. |
359 | return; |
360 | } |
361 | |
362 | // Add the new device itself. |
363 | new_devices->insert(name, AudioDevice(name, info->description, info->icon, info->index)); |
364 | |
365 | // For each role in the priority, map it to a phonon category and store the order. |
366 | for (uint32_t i = 0; i < info->n_role_priorities; ++i) { |
367 | pa_ext_device_manager_role_priority_info* role_prio = &info->role_priorities[i]; |
368 | Q_ASSERT(role_prio->role); |
369 | |
370 | if (s_roleCategoryMap.contains(role_prio->role)) { |
371 | Phonon::Category cat = s_roleCategoryMap[role_prio->role]; |
372 | |
373 | (*new_prio_map_cats)[cat].insert(role_prio->priority, index); |
374 | } |
375 | } |
376 | } |
377 | |
378 | static void ext_device_manager_subscribe_cb(pa_context *c, void *) { |
379 | Q_ASSERT(c); |
380 | |
381 | pa_operation *o; |
382 | PulseUserData *u = new PulseUserData; |
383 | if (!(o = pa_ext_device_manager_read(c, ext_device_manager_read_cb, u))) { |
384 | logMessage(QString("pa_ext_device_manager_read() failed." )); |
385 | delete u; |
386 | return; |
387 | } |
388 | pa_operation_unref(o); |
389 | } |
390 | #endif |
391 | |
392 | void sink_input_cb(pa_context *c, const pa_sink_input_info *i, int eol, void *userdata) { |
393 | Q_UNUSED(userdata); |
394 | Q_ASSERT(c); |
395 | |
396 | if (eol < 0) { |
397 | if (pa_context_errno(c) == PA_ERR_NOENTITY) |
398 | return; |
399 | |
400 | logMessage(QLatin1String("Sink input callback failure" )); |
401 | return; |
402 | } |
403 | |
404 | if (eol > 0) |
405 | return; |
406 | |
407 | Q_ASSERT(i); |
408 | |
409 | // loop through (*i) and extract phonon->streamindex... |
410 | const char *t; |
411 | if ((t = pa_proplist_gets(i->proplist, "phonon.streamid" ))) { |
412 | logMessage(QString::fromLatin1("Found PulseAudio stream index %1 for Phonon Output Stream %2" ).arg(i->index).arg(QLatin1String(t))); |
413 | s_outputStreamIndexMap[QLatin1String(t)] = i->index; |
414 | |
415 | // Find the sink's phonon index and notify whoever cares... |
416 | if (PA_INVALID_INDEX != i->sink) { |
417 | bool found = false; |
418 | int device; |
419 | QMap<int, AudioDevice>::iterator it; |
420 | for (it = s_outputDevices.begin(); it != s_outputDevices.end(); ++it) { |
421 | if ((*it).pulseIndex == i->sink) { |
422 | found = true; |
423 | device = it.key(); |
424 | break; |
425 | } |
426 | } |
427 | if (found) { |
428 | // OK so we just emit our signal |
429 | logMessage(QLatin1String("Letting the rest of phonon know about this" )); |
430 | s_instance->emitUsingDevice(QLatin1String(t), device); |
431 | } |
432 | } |
433 | } |
434 | } |
435 | |
436 | void source_output_cb(pa_context *c, const pa_source_output_info *i, int eol, void *userdata) { |
437 | Q_UNUSED(userdata); |
438 | Q_ASSERT(c); |
439 | |
440 | if (eol < 0) { |
441 | if (pa_context_errno(c) == PA_ERR_NOENTITY) |
442 | return; |
443 | |
444 | logMessage(QLatin1String("Source output callback failure" )); |
445 | return; |
446 | } |
447 | |
448 | if (eol > 0) |
449 | return; |
450 | |
451 | Q_ASSERT(i); |
452 | |
453 | // loop through (*i) and extract phonon->streamindex... |
454 | const char *t; |
455 | if ((t = pa_proplist_gets(i->proplist, "phonon.streamid" ))) { |
456 | logMessage(QString::fromLatin1("Found PulseAudio stream index %1 for Phonon Capture Stream %2" ).arg(i->index).arg(QLatin1String(t))); |
457 | s_captureStreamIndexMap[QLatin1String(t)] = i->index; |
458 | |
459 | // Find the source's phonon index and notify whoever cares... |
460 | if (PA_INVALID_INDEX != i->source) { |
461 | bool found = false; |
462 | int device; |
463 | QMap<int, AudioDevice>::iterator it; |
464 | for (it = s_captureDevices.begin(); it != s_captureDevices.end(); ++it) { |
465 | if ((*it).pulseIndex == i->source) { |
466 | found = true; |
467 | device = it.key(); |
468 | break; |
469 | } |
470 | } |
471 | if (found) { |
472 | // OK so we just emit our signal |
473 | logMessage(QLatin1String("Letting the rest of phonon know about this" )); |
474 | s_instance->emitUsingDevice(QLatin1String(t), device); |
475 | } |
476 | } |
477 | } |
478 | } |
479 | |
480 | static void subscribe_cb(pa_context *c, pa_subscription_event_type_t t, uint32_t index, void *userdata) { |
481 | Q_UNUSED(userdata); |
482 | |
483 | switch (t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) { |
484 | case PA_SUBSCRIPTION_EVENT_SINK_INPUT: |
485 | if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { |
486 | QString phononid = s_outputStreamIndexMap.key(index); |
487 | if (!phononid.isEmpty()) { |
488 | if (s_outputStreamIndexMap.contains(phononid)) { |
489 | 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(phononid)); |
490 | s_outputStreamIndexMap[phononid] = PA_INVALID_INDEX; |
491 | } else { |
492 | logMessage(QString::fromLatin1("Removing Phonon Output Stream %1 (it's gone!)" ).arg(phononid)); |
493 | s_outputStreamIndexMap.remove(phononid); |
494 | } |
495 | } |
496 | } else { |
497 | pa_operation *o; |
498 | if (!(o = pa_context_get_sink_input_info(c, index, sink_input_cb, NULL))) { |
499 | logMessage(QLatin1String("pa_context_get_sink_input_info() failed" )); |
500 | return; |
501 | } |
502 | pa_operation_unref(o); |
503 | } |
504 | break; |
505 | |
506 | case PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT: |
507 | if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { |
508 | QString phononid = s_captureStreamIndexMap.key(index); |
509 | if (!phononid.isEmpty()) { |
510 | if (s_captureStreamIndexMap.contains(phononid)) { |
511 | 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(phononid)); |
512 | s_captureStreamIndexMap[phononid] = PA_INVALID_INDEX; |
513 | } else { |
514 | logMessage(QString::fromLatin1("Removing Phonon Capture Stream %1 (it's gone!)" ).arg(phononid)); |
515 | s_captureStreamIndexMap.remove(phononid); |
516 | } |
517 | } |
518 | } else { |
519 | pa_operation *o; |
520 | if (!(o = pa_context_get_source_output_info(c, index, source_output_cb, NULL))) { |
521 | logMessage(QLatin1String("pa_context_get_sink_input_info() failed" )); |
522 | return; |
523 | } |
524 | pa_operation_unref(o); |
525 | } |
526 | break; |
527 | } |
528 | } |
529 | |
530 | |
531 | static QString statename(pa_context_state_t state) |
532 | { |
533 | switch (state) |
534 | { |
535 | case PA_CONTEXT_UNCONNECTED: return QLatin1String("Unconnected" ); |
536 | case PA_CONTEXT_CONNECTING: return QLatin1String("Connecting" ); |
537 | case PA_CONTEXT_AUTHORIZING: return QLatin1String("Authorizing" ); |
538 | case PA_CONTEXT_SETTING_NAME: return QLatin1String("Setting Name" ); |
539 | case PA_CONTEXT_READY: return QLatin1String("Ready" ); |
540 | case PA_CONTEXT_FAILED: return QLatin1String("Failed" ); |
541 | case PA_CONTEXT_TERMINATED: return QLatin1String("Terminated" ); |
542 | } |
543 | |
544 | return QString::fromLatin1("Unknown state: %0" ).arg(state); |
545 | } |
546 | |
547 | static void context_state_callback(pa_context *c, void *) |
548 | { |
549 | Q_ASSERT(c); |
550 | |
551 | logMessage(QString::fromLatin1("context_state_callback %1" ).arg(statename(pa_context_get_state(c)))); |
552 | pa_context_state_t state = pa_context_get_state(c); |
553 | if (state == PA_CONTEXT_READY) { |
554 | // We've connected to PA, so it is active |
555 | s_pulseActive = true; |
556 | |
557 | // Attempt to load things up |
558 | pa_operation *o; |
559 | |
560 | // 1. Register for the stream changes (except during probe) |
561 | if (s_context == c) { |
562 | pa_context_set_subscribe_callback(c, subscribe_cb, NULL); |
563 | |
564 | if (!(o = pa_context_subscribe(c, (pa_subscription_mask_t) |
565 | (PA_SUBSCRIPTION_MASK_SINK_INPUT| |
566 | PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT), NULL, NULL))) { |
567 | logMessage(QLatin1String("pa_context_subscribe() failed" )); |
568 | return; |
569 | } |
570 | pa_operation_unref(o); |
571 | } |
572 | |
573 | #ifdef HAVE_PULSEAUDIO_DEVICE_MANAGER |
574 | // 2a. Attempt to initialise Device Manager info (except during probe) |
575 | if (s_context == c) { |
576 | pa_ext_device_manager_set_subscribe_cb(c, ext_device_manager_subscribe_cb, NULL); |
577 | if (!(o = pa_ext_device_manager_subscribe(c, 1, NULL, NULL))) { |
578 | logMessage(QString("pa_ext_device_manager_subscribe() failed" )); |
579 | return; |
580 | } |
581 | pa_operation_unref(o); |
582 | } |
583 | |
584 | // 3. Attempt to read info from Device Manager |
585 | PulseUserData *u = new PulseUserData; |
586 | if (!(o = pa_ext_device_manager_read(c, ext_device_manager_read_cb, u))) { |
587 | logMessage(QString("pa_ext_device_manager_read() failed. Attempting to continue without device manager support" )); |
588 | createGenericDevices(); |
589 | delete u; |
590 | |
591 | // If this is our probe phase, exit immediately |
592 | if (s_context != c) |
593 | pa_context_disconnect(c); |
594 | |
595 | return; |
596 | } |
597 | pa_operation_unref(o); |
598 | |
599 | #else |
600 | // If we know do not have Device Manager support, we just create our dummy devices now |
601 | createGenericDevices(); |
602 | |
603 | // If this is our probe phase, exit immediately |
604 | if (s_context != c) |
605 | pa_context_disconnect(c); |
606 | #endif |
607 | } else if (!PA_CONTEXT_IS_GOOD(state)) { |
608 | /// @todo Deal with reconnection... |
609 | //logMessage("Connection to PulseAudio lost"); |
610 | |
611 | // If this is our probe phase, exit our context immediately |
612 | if (s_context != c) |
613 | pa_context_disconnect(c); |
614 | } |
615 | } |
616 | #endif // HAVE_PULSEAUDIO |
617 | |
618 | |
619 | PulseSupport* PulseSupport::getInstance() |
620 | { |
621 | if (NULL == s_instance) { |
622 | s_instance = new PulseSupport(); |
623 | } |
624 | return s_instance; |
625 | } |
626 | |
627 | void PulseSupport::shutdown() |
628 | { |
629 | if (NULL != s_instance) { |
630 | delete s_instance; |
631 | s_instance = NULL; |
632 | } |
633 | } |
634 | |
635 | PulseSupport::PulseSupport() |
636 | : QObject(), mEnabled(false) |
637 | { |
638 | #ifdef HAVE_PULSEAUDIO |
639 | // Initialise our map (is there a better way to do this?) |
640 | s_roleCategoryMap[QLatin1String("none" )] = Phonon::NoCategory; |
641 | s_roleCategoryMap[QLatin1String("video" )] = Phonon::VideoCategory; |
642 | s_roleCategoryMap[QLatin1String("music" )] = Phonon::MusicCategory; |
643 | s_roleCategoryMap[QLatin1String("game" )] = Phonon::GameCategory; |
644 | s_roleCategoryMap[QLatin1String("event" )] = Phonon::NotificationCategory; |
645 | s_roleCategoryMap[QLatin1String("phone" )] = Phonon::CommunicationCategory; |
646 | //s_roleCategoryMap[QLatin1String("animation")]; // No Mapping |
647 | //s_roleCategoryMap[QLatin1String("production")]; // No Mapping |
648 | s_roleCategoryMap[QLatin1String("a11y" )] = Phonon::AccessibilityCategory; |
649 | |
650 | // To allow for easy debugging, give an easy way to disable this pulseaudio check |
651 | QByteArray pulseenv = qgetenv("PHONON_PULSEAUDIO_DISABLE" ); |
652 | if (pulseenv.toInt()) { |
653 | logMessage(QLatin1String("PulseAudio support disabled: PHONON_PULSEAUDIO_DISABLE is set" )); |
654 | return; |
655 | } |
656 | |
657 | // We require a glib event loop |
658 | if (strcmp(QAbstractEventDispatcher::instance()->metaObject()->className(), |
659 | "QGuiEventDispatcherGlib" ) != 0) { |
660 | logMessage(QLatin1String("Disabling PulseAudio integration for lack of GLib event loop." )); |
661 | return; |
662 | } |
663 | |
664 | // First of all conenct to PA via simple/blocking means and if that succeeds, |
665 | // use a fully async integrated mainloop method to connect and get proper support. |
666 | pa_mainloop *p_test_mainloop; |
667 | if (!(p_test_mainloop = pa_mainloop_new())) { |
668 | logMessage(QLatin1String("PulseAudio support disabled: Unable to create mainloop" )); |
669 | return; |
670 | } |
671 | |
672 | pa_context *p_test_context; |
673 | if (!(p_test_context = pa_context_new(pa_mainloop_get_api(p_test_mainloop), "libphonon-probe" ))) { |
674 | logMessage(QLatin1String("PulseAudio support disabled: Unable to create context" )); |
675 | pa_mainloop_free(p_test_mainloop); |
676 | return; |
677 | } |
678 | |
679 | logMessage(QLatin1String("Probing for PulseAudio..." )); |
680 | // (cg) Convert to PA_CONTEXT_NOFLAGS when PulseAudio 0.9.19 is required |
681 | if (pa_context_connect(p_test_context, NULL, static_cast<pa_context_flags_t>(0), NULL) < 0) { |
682 | logMessage(QString::fromLatin1("PulseAudio support disabled: %1" ).arg(QString::fromLocal8Bit(pa_strerror(pa_context_errno(p_test_context))))); |
683 | pa_context_disconnect(p_test_context); |
684 | pa_context_unref(p_test_context); |
685 | pa_mainloop_free(p_test_mainloop); |
686 | return; |
687 | } |
688 | |
689 | pa_context_set_state_callback(p_test_context, &context_state_callback, NULL); |
690 | for (;;) { |
691 | pa_mainloop_iterate(p_test_mainloop, 1, NULL); |
692 | |
693 | if (!PA_CONTEXT_IS_GOOD(pa_context_get_state(p_test_context))) { |
694 | logMessage(QLatin1String("PulseAudio probe complete." )); |
695 | break; |
696 | } |
697 | } |
698 | pa_context_disconnect(p_test_context); |
699 | pa_context_unref(p_test_context); |
700 | pa_mainloop_free(p_test_mainloop); |
701 | |
702 | if (!s_pulseActive) { |
703 | logMessage(QLatin1String("PulseAudio support is not available." )); |
704 | return; |
705 | } |
706 | |
707 | // If we're still here, PA is available. |
708 | logMessage(QLatin1String("PulseAudio support enabled" )); |
709 | |
710 | // Now we connect for real using a proper main loop that we can forget |
711 | // all about processing. |
712 | s_mainloop = pa_glib_mainloop_new(NULL); |
713 | Q_ASSERT(s_mainloop); |
714 | pa_mainloop_api *api = pa_glib_mainloop_get_api(s_mainloop); |
715 | |
716 | s_context = pa_context_new(api, "libphonon" ); |
717 | // (cg) Convert to PA_CONTEXT_NOFLAGS when PulseAudio 0.9.19 is required |
718 | if (pa_context_connect(s_context, NULL, static_cast<pa_context_flags_t>(0), 0) >= 0) |
719 | pa_context_set_state_callback(s_context, &context_state_callback, NULL); |
720 | #endif |
721 | } |
722 | |
723 | PulseSupport::~PulseSupport() |
724 | { |
725 | #ifdef HAVE_PULSEAUDIO |
726 | if (s_context) { |
727 | pa_context_disconnect(s_context); |
728 | s_context = NULL; |
729 | } |
730 | |
731 | if (s_mainloop) { |
732 | pa_glib_mainloop_free(s_mainloop); |
733 | s_mainloop = NULL; |
734 | } |
735 | #endif |
736 | } |
737 | |
738 | bool PulseSupport::isActive() |
739 | { |
740 | #ifdef HAVE_PULSEAUDIO |
741 | return mEnabled && s_pulseActive; |
742 | #else |
743 | return false; |
744 | #endif |
745 | } |
746 | |
747 | void PulseSupport::enable(bool enabled) |
748 | { |
749 | mEnabled = enabled; |
750 | } |
751 | |
752 | QList<int> PulseSupport::objectDescriptionIndexes(ObjectDescriptionType type) const |
753 | { |
754 | QList<int> list; |
755 | |
756 | if (type != AudioOutputDeviceType && type != AudioCaptureDeviceType) |
757 | return list; |
758 | |
759 | #ifdef HAVE_PULSEAUDIO |
760 | if (s_pulseActive) { |
761 | switch (type) { |
762 | |
763 | case AudioOutputDeviceType: { |
764 | QMap<QString, int>::iterator it; |
765 | for (it = s_outputDeviceIndexes.begin(); it != s_outputDeviceIndexes.end(); ++it) { |
766 | list.append(*it); |
767 | } |
768 | break; |
769 | } |
770 | case AudioCaptureDeviceType: { |
771 | QMap<QString, int>::iterator it; |
772 | for (it = s_captureDeviceIndexes.begin(); it != s_captureDeviceIndexes.end(); ++it) { |
773 | list.append(*it); |
774 | } |
775 | break; |
776 | } |
777 | default: |
778 | break; |
779 | } |
780 | } |
781 | #endif |
782 | |
783 | return list; |
784 | } |
785 | |
786 | QHash<QByteArray, QVariant> PulseSupport::objectDescriptionProperties(ObjectDescriptionType type, int index) const |
787 | { |
788 | QHash<QByteArray, QVariant> ret; |
789 | |
790 | if (type != AudioOutputDeviceType && type != AudioCaptureDeviceType) |
791 | return ret; |
792 | |
793 | #ifndef HAVE_PULSEAUDIO |
794 | Q_UNUSED(index); |
795 | #else |
796 | if (s_pulseActive) { |
797 | switch (type) { |
798 | |
799 | case AudioOutputDeviceType: |
800 | Q_ASSERT(s_outputDevices.contains(index)); |
801 | ret = s_outputDevices[index].properties; |
802 | break; |
803 | |
804 | case AudioCaptureDeviceType: |
805 | Q_ASSERT(s_captureDevices.contains(index)); |
806 | ret = s_captureDevices[index].properties; |
807 | break; |
808 | |
809 | default: |
810 | break; |
811 | } |
812 | } |
813 | #endif |
814 | |
815 | return ret; |
816 | } |
817 | |
818 | QList<int> PulseSupport::objectIndexesByCategory(ObjectDescriptionType type, Category category) const |
819 | { |
820 | QList<int> ret; |
821 | |
822 | if (type != AudioOutputDeviceType && type != AudioCaptureDeviceType) |
823 | return ret; |
824 | |
825 | #ifndef HAVE_PULSEAUDIO |
826 | Q_UNUSED(category); |
827 | #else |
828 | if (s_pulseActive) { |
829 | switch (type) { |
830 | |
831 | case AudioOutputDeviceType: |
832 | if (s_outputDevicePriorities.contains(category)) |
833 | ret = s_outputDevicePriorities[category].values(); |
834 | break; |
835 | |
836 | case AudioCaptureDeviceType: |
837 | if (s_captureDevicePriorities.contains(category)) |
838 | ret = s_captureDevicePriorities[category].values(); |
839 | break; |
840 | |
841 | default: |
842 | break; |
843 | } |
844 | } |
845 | #endif |
846 | |
847 | return ret; |
848 | } |
849 | |
850 | #ifdef HAVE_PULSEAUDIO |
851 | static void setDevicePriority(Category category, QStringList list) |
852 | { |
853 | QString role = s_roleCategoryMap.key(category); |
854 | if (role.isEmpty()) |
855 | return; |
856 | |
857 | logMessage(QString::fromLatin1("Reindexing %1: %2" ).arg(role).arg(list.join(QLatin1String(", " )))); |
858 | |
859 | char **devices; |
860 | devices = pa_xnew(char *, list.size()+1); |
861 | int i = 0; |
862 | foreach (QString str, list) { |
863 | devices[i++] = pa_xstrdup(str.toUtf8().constData()); |
864 | } |
865 | devices[list.size()] = NULL; |
866 | |
867 | #ifdef HAVE_PULSEAUDIO_DEVICE_MANAGER |
868 | pa_operation *o; |
869 | if (!(o = pa_ext_device_manager_reorder_devices_for_role(s_context, role.toUtf8().constData(), (const char**)devices, NULL, NULL))) |
870 | logMessage(QString("pa_ext_device_manager_reorder_devices_for_role() failed" )); |
871 | else |
872 | pa_operation_unref(o); |
873 | #endif |
874 | |
875 | for (i = 0; i < list.size(); ++i) |
876 | pa_xfree(devices[i]); |
877 | pa_xfree(devices); |
878 | } |
879 | #endif |
880 | |
881 | void PulseSupport::setOutputDevicePriorityForCategory(Category category, QList<int> order) |
882 | { |
883 | #ifndef HAVE_PULSEAUDIO |
884 | Q_UNUSED(category); |
885 | Q_UNUSED(order); |
886 | #else |
887 | QStringList list; |
888 | QList<int>::iterator it; |
889 | |
890 | for (it = order.begin(); it != order.end(); ++it) { |
891 | if (s_outputDevices.contains(*it)) { |
892 | list << s_outputDeviceIndexes.key(*it); |
893 | } |
894 | } |
895 | setDevicePriority(category, list); |
896 | #endif |
897 | } |
898 | |
899 | void PulseSupport::setCaptureDevicePriorityForCategory(Category category, QList<int> order) |
900 | { |
901 | #ifndef HAVE_PULSEAUDIO |
902 | Q_UNUSED(category); |
903 | Q_UNUSED(order); |
904 | #else |
905 | QStringList list; |
906 | QList<int>::iterator it; |
907 | |
908 | for (it = order.begin(); it != order.end(); ++it) { |
909 | if (s_captureDevices.contains(*it)) { |
910 | list << s_captureDeviceIndexes.key(*it); |
911 | } |
912 | } |
913 | setDevicePriority(category, list); |
914 | #endif |
915 | } |
916 | |
917 | void PulseSupport::setStreamPropList(Category category, QString streamUuid) |
918 | { |
919 | #ifndef HAVE_PULSEAUDIO |
920 | Q_UNUSED(category); |
921 | Q_UNUSED(streamUuid); |
922 | #else |
923 | QString role = s_roleCategoryMap.key(category); |
924 | if (role.isEmpty()) |
925 | return; |
926 | |
927 | logMessage(QString::fromLatin1("Setting role to %1 for streamindex %2" ).arg(role).arg(streamUuid)); |
928 | setenv("PULSE_PROP_media.role" , role.toLatin1().constData(), 1); |
929 | setenv("PULSE_PROP_phonon.streamid" , streamUuid.toLatin1().constData(), 1); |
930 | #endif |
931 | } |
932 | |
933 | void PulseSupport::emitObjectDescriptionChanged(ObjectDescriptionType type) |
934 | { |
935 | emit objectDescriptionChanged(type); |
936 | } |
937 | |
938 | void PulseSupport::emitUsingDevice(QString streamUuid, int device) |
939 | { |
940 | emit usingDevice(streamUuid, device); |
941 | } |
942 | |
943 | bool PulseSupport::setOutputDevice(QString streamUuid, int device) { |
944 | #ifndef HAVE_PULSEAUDIO |
945 | Q_UNUSED(streamUuid); |
946 | Q_UNUSED(device); |
947 | return false; |
948 | #else |
949 | if (s_outputDevices.size() < 2) |
950 | return true; |
951 | |
952 | if (!s_outputDevices.contains(device)) { |
953 | logMessage(QString::fromLatin1("Attempting to set Output Device for invalid device id %1." ).arg(device)); |
954 | return false; |
955 | } |
956 | const QVariant var = s_outputDevices[device].properties["name" ]; |
957 | logMessage(QString::fromLatin1("Attempting to set Output Device to '%1' for Output Stream %2" ).arg(var.toString()).arg(streamUuid)); |
958 | |
959 | // Attempt to look up the pulse stream index. |
960 | if (s_outputStreamIndexMap.contains(streamUuid) && s_outputStreamIndexMap[streamUuid] != PA_INVALID_INDEX) { |
961 | logMessage(QLatin1String("... Found in map. Moving now" )); |
962 | |
963 | uint32_t pulse_device_index = s_outputDevices[device].pulseIndex; |
964 | uint32_t pulse_stream_index = s_outputStreamIndexMap[streamUuid]; |
965 | |
966 | logMessage(QString::fromLatin1("Moving Pulse Sink Input %1 to '%2' (Pulse Sink %3)" ).arg(pulse_stream_index).arg(var.toString()).arg(pulse_device_index)); |
967 | |
968 | /// @todo Find a way to move the stream without saving it... We don't want to pollute the stream restore db. |
969 | pa_operation* o; |
970 | if (!(o = pa_context_move_sink_input_by_index(s_context, pulse_stream_index, pulse_device_index, NULL, NULL))) { |
971 | logMessage(QLatin1String("pa_context_move_sink_input_by_index() failed" )); |
972 | return false; |
973 | } |
974 | pa_operation_unref(o); |
975 | } else { |
976 | logMessage(QLatin1String("... Not found in map. We will be notified of the device when the stream appears and we can process any moves needed then" )); |
977 | } |
978 | return true; |
979 | #endif |
980 | } |
981 | |
982 | bool PulseSupport::setCaptureDevice(QString streamUuid, int device) { |
983 | #ifndef HAVE_PULSEAUDIO |
984 | Q_UNUSED(streamUuid); |
985 | Q_UNUSED(device); |
986 | return false; |
987 | #else |
988 | if (s_captureDevices.size() < 2) |
989 | return true; |
990 | |
991 | if (!s_captureDevices.contains(device)) { |
992 | logMessage(QString::fromLatin1("Attempting to set Capture Device for invalid device id %1." ).arg(device)); |
993 | return false; |
994 | } |
995 | const QVariant var = s_captureDevices[device].properties["name" ]; |
996 | logMessage(QString::fromLatin1("Attempting to set Capture Device to '%1' for Capture Stream %2" ).arg(var.toString()).arg(streamUuid)); |
997 | |
998 | // Attempt to look up the pulse stream index. |
999 | if (s_captureStreamIndexMap.contains(streamUuid) && s_captureStreamIndexMap[streamUuid] == PA_INVALID_INDEX) { |
1000 | logMessage(QString::fromLatin1("... Found in map. Moving now" )); |
1001 | |
1002 | uint32_t pulse_device_index = s_captureDevices[device].pulseIndex; |
1003 | uint32_t pulse_stream_index = s_captureStreamIndexMap[streamUuid]; |
1004 | |
1005 | logMessage(QString::fromLatin1("Moving Pulse Source Output %1 to '%2' (Pulse Sink %3)" ).arg(pulse_stream_index).arg(var.toString()).arg(pulse_device_index)); |
1006 | |
1007 | /// @todo Find a way to move the stream without saving it... We don't want to pollute the stream restore db. |
1008 | pa_operation* o; |
1009 | if (!(o = pa_context_move_source_output_by_index(s_context, pulse_stream_index, pulse_device_index, NULL, NULL))) { |
1010 | logMessage(QString::fromLatin1("pa_context_move_source_output_by_index() failed" )); |
1011 | return false; |
1012 | } |
1013 | pa_operation_unref(o); |
1014 | } else { |
1015 | 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" )); |
1016 | } |
1017 | return true; |
1018 | #endif |
1019 | } |
1020 | |
1021 | void PulseSupport::clearStreamCache(QString streamUuid) { |
1022 | #ifndef HAVE_PULSEAUDIO |
1023 | Q_UNUSED(streamUuid); |
1024 | return; |
1025 | #else |
1026 | logMessage(QString::fromLatin1("Clearing stream cache for stream %1" ).arg(streamUuid)); |
1027 | s_outputStreamIndexMap.remove(streamUuid); |
1028 | s_captureStreamIndexMap.remove(streamUuid); |
1029 | #endif |
1030 | } |
1031 | |
1032 | } // namespace Phonon |
1033 | |
1034 | QT_END_NAMESPACE |
1035 | |
1036 | #include "moc_pulsesupport.cpp" |
1037 | |
1038 | // vim: sw=4 ts=4 |
1039 | |