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