systemvolumeplugin-win.cpp 13.4 KB
Newer Older
Nicolas Fella's avatar
Nicolas Fella committed
1
/**
2
 * SPDX-FileCopyrightText: 2018 Jun Bo Bi <jambonmcyeah@gmail.com>
Nicolas Fella's avatar
Nicolas Fella committed
3
 *
4
 * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
Nicolas Fella's avatar
Nicolas Fella committed
5
 */
6

Nicolas Fella's avatar
Nicolas Fella committed
7
#include "systemvolumeplugin-win.h"
8
9
10
11
12
13

#include <QDebug>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>

Nicolas Fella's avatar
Nicolas Fella committed
14
15
#include <KPluginFactory>

16
17
#include <Functiondiscoverykeys_devpkey.h>

Nicolas Fella's avatar
Nicolas Fella committed
18
19
#include <core/device.h>

20
#include "plugin_systemvolume_debug.h"
21
#include "PolicyConfig.h"
22

23
K_PLUGIN_CLASS_WITH_JSON(SystemvolumePlugin, "kdeconnect_systemvolume.json")
24
25
26

// Private classes of SystemvolumePlugin

27
class SystemvolumePlugin::CAudioEndpointVolumeCallback : public IAudioEndpointVolumeCallback
28
{
29
    LONG _cRef;
30

31
32
33
public:
    CAudioEndpointVolumeCallback(SystemvolumePlugin &x, QString sinkName) : enclosing(x), name(std::move(sinkName)), _cRef(1) {}
    ~CAudioEndpointVolumeCallback(){};
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

    // IUnknown methods -- AddRef, Release, and QueryInterface

    ULONG STDMETHODCALLTYPE AddRef() override
    {
        return InterlockedIncrement(&_cRef);
    }

    ULONG STDMETHODCALLTYPE Release() override
    {
        ULONG ulRef = InterlockedDecrement(&_cRef);
        if (ulRef == 0)
        {
            delete this;
        }
        return ulRef;
    }

    HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, VOID **ppvInterface) override
    {
        if (IID_IUnknown == riid)
        {
            AddRef();
            *ppvInterface = (IUnknown *)this;
        }
        else if (__uuidof(IMMNotificationClient) == riid)
        {
            AddRef();
            *ppvInterface = (IMMNotificationClient *)this;
        }
        else
        {
            *ppvInterface = NULL;
            return E_NOINTERFACE;
        }
        return S_OK;
    }

72
    // Callback method for endpoint-volume-change notifications.
73

74
    HRESULT STDMETHODCALLTYPE OnNotify(PAUDIO_VOLUME_NOTIFICATION_DATA pNotify) override
75
    {
76
77
78
79
80
        NetworkPacket np(PACKET_TYPE_SYSTEMVOLUME);
        np.set<int>(QStringLiteral("volume"), (int)(pNotify->fMasterVolume * 100));
        np.set<bool>(QStringLiteral("muted"), pNotify->bMuted);
        np.set<QString>(QStringLiteral("name"), name);
        enclosing.sendPacket(np);
81
82
83
84

        return S_OK;
    }

85
private:
86
    SystemvolumePlugin &enclosing;
87
    QString name;
88
89
};

90
class SystemvolumePlugin::CMMNotificationClient : public IMMNotificationClient
91
92
{

93
94
95
96
public:
    CMMNotificationClient(SystemvolumePlugin &x) : enclosing(x), _cRef(1){};

    ~CMMNotificationClient(){};
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134

    // IUnknown methods -- AddRef, Release, and QueryInterface

    ULONG STDMETHODCALLTYPE AddRef() override
    {
        return InterlockedIncrement(&_cRef);
    }

    ULONG STDMETHODCALLTYPE Release() override
    {
        ULONG ulRef = InterlockedDecrement(&_cRef);
        if (ulRef == 0)
        {
            delete this;
        }
        return ulRef;
    }

    HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, VOID **ppvInterface) override
    {
        if (IID_IUnknown == riid)
        {
            AddRef();
            *ppvInterface = (IUnknown *)this;
        }
        else if (__uuidof(IMMNotificationClient) == riid)
        {
            AddRef();
            *ppvInterface = (IMMNotificationClient *)this;
        }
        else
        {
            *ppvInterface = NULL;
            return E_NOINTERFACE;
        }
        return S_OK;
    }

135
    // Callback methods for device-event notifications.
136

137
    HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId) override
138
    {
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
        if (flow == eRender)
        {
            enclosing.sendSinkList();
        }
        return S_OK;
    }

    HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR pwstrDeviceId) override
    {
        enclosing.sendSinkList();
        return S_OK;
    }

    struct RemovedDeviceThreadData {

        QString qDeviceId;
        SystemvolumePlugin* plugin;

    };

    static DWORD WINAPI releaseRemovedDevice(_In_ LPVOID lpParameter) {

        auto* data = static_cast<RemovedDeviceThreadData*>(lpParameter);

        if (!data->plugin->sinkList.empty())
        {
            auto idToNameIterator = data->plugin->idToNameMap.find(data->qDeviceId);
            if (idToNameIterator == data->plugin->idToNameMap.end()) return 0;

            QString& sinkName = idToNameIterator.value();

            auto sinkListIterator = data->plugin->sinkList.find(sinkName);
            if (sinkListIterator == data->plugin->sinkList.end()) return 0;

            auto& sink = sinkListIterator.value();

            sink.first->UnregisterControlChangeNotify(sink.second);
            sink.first->Release();
            sink.second->Release();

            data->plugin->idToNameMap.remove(data->qDeviceId);
            data->plugin->sinkList.remove(sinkName);
        }

        data->plugin->sendSinkList();

        return 0;
    }

    HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR pwstrDeviceId) override
    {
        static RemovedDeviceThreadData data {};
        data.qDeviceId = QString::fromWCharArray(pwstrDeviceId);
        data.plugin = &enclosing;

        DWORD threadId;
        HANDLE threadHandle = CreateThread(NULL, 0, releaseRemovedDevice, &data, 0, &threadId);
        CloseHandle(threadHandle);

        return S_OK;
    }

    HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(LPCWSTR pwstrDeviceId, DWORD dwNewState) override
    {
        if (dwNewState == DEVICE_STATE_UNPLUGGED) return OnDeviceRemoved(pwstrDeviceId);

        enclosing.sendSinkList();
        return S_OK;
    }
208

209
210
211
    HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(LPCWSTR pwstrDeviceId, const PROPERTYKEY key) override
    {
        enclosing.sendSinkList();
212
213
214
        return S_OK;
    }

215
216
private:
    LONG _cRef;
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
    SystemvolumePlugin &enclosing;
};

SystemvolumePlugin::SystemvolumePlugin(QObject *parent, const QVariantList &args)
    : KdeConnectPlugin(parent, args),
      sinkList()
{
    CoInitialize(nullptr);
    deviceEnumerator = nullptr;
    HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER, __uuidof(IMMDeviceEnumerator), (LPVOID *)&(deviceEnumerator));
    valid = (hr == S_OK);
    if (!valid)
    {
        qWarning("Initialization failed: Failed to create MMDeviceEnumerator");
        qWarning("Error Code: %lx", hr);
    }
}

SystemvolumePlugin::~SystemvolumePlugin()
{
    if (valid)
    {
        deviceEnumerator->UnregisterEndpointNotificationCallback(deviceCallback);
        deviceEnumerator->Release();
        deviceEnumerator = nullptr;
    }
}

bool SystemvolumePlugin::sendSinkList()
{
    if (!valid)
        return false;

    QJsonDocument document;
    QJsonArray array;

    IMMDeviceCollection *devices = nullptr;
254
    HRESULT hr = deviceEnumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &devices);
255
256
257
258
259
260
261
262
263

    if (hr != S_OK)
    {
        qWarning("Failed to Enumumerate AudioEndpoints");
        qWarning("Error Code: %lx", hr);
        return false;
    }
    unsigned int deviceCount;
    devices->GetCount(&deviceCount);
264
265
266
267
268
269
270

    IMMDevice *defaultDevice = nullptr;
    deviceEnumerator->GetDefaultAudioEndpoint(eRender, eMultimedia, &defaultDevice);

    LPWSTR defaultId = NULL;
    defaultDevice->GetId(&defaultId);

271
272
273
274
275
276
277
278
    for (unsigned int i = 0; i < deviceCount; i++)
    {
        IMMDevice *device = nullptr;

        IPropertyStore *deviceProperties = nullptr;
        PROPVARIANT deviceProperty;
        QString name;
        QString desc;
279
        LPWSTR deviceId;
280
281
282
283
284
285
286
287
288
289
290
291
292
293
        float volume;
        BOOL muted;

        IAudioEndpointVolume *endpoint = nullptr;
        CAudioEndpointVolumeCallback *callback;

        // Get Properties
        devices->Item(i, &device);
        device->OpenPropertyStore(STGM_READ, &deviceProperties);

        deviceProperties->GetValue(PKEY_Device_FriendlyName, &deviceProperty);
        name = QString::fromWCharArray(deviceProperty.pwszVal);
        //PropVariantClear(&deviceProperty);

294
#ifndef __MINGW32__
295
296
297
        deviceProperties->GetValue(PKEY_Device_DeviceDesc, &deviceProperty);
        desc = QString::fromWCharArray(deviceProperty.pwszVal);
        //PropVariantClear(&deviceProperty);
298
#endif
299
300

        QJsonObject sinkObject;
301
302
        sinkObject.insert(QStringLiteral("name"), name);
        sinkObject.insert(QStringLiteral("description"), desc);
303
304
305
306
307
308
309
310
311
312

        hr = device->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_ALL, NULL, (void **)&endpoint);
        if (hr != S_OK)
        {
            qWarning() << "Failed to create IAudioEndpointVolume for device:" << name;
            qWarning("Error Code: %lx", hr);

            device->Release();
            continue;
        }
313
314

        device->GetId(&deviceId);
315
316
317
        endpoint->GetMasterVolumeLevelScalar(&volume);
        endpoint->GetMute(&muted);

318
319
320
        sinkObject.insert(QStringLiteral("muted"), (bool)muted);
        sinkObject.insert(QStringLiteral("volume"), (qint64)(volume * 100));
        sinkObject.insert(QStringLiteral("maxVolume"), (qint64)100);
321
        sinkObject.insert(QStringLiteral("enabled"), lstrcmpW(deviceId, defaultId) == 0);
322
323

        // Register Callback
324
        if (!sinkList.contains(name)) {
325
326
327
328
329
            callback = new CAudioEndpointVolumeCallback(*this, name);
            endpoint->RegisterControlChangeNotify(callback);
            sinkList[name] = qMakePair(endpoint, callback);
        }

330
        QString qDeviceId = QString::fromWCharArray(deviceId);
331
        idToNameMap[qDeviceId] = name;
332
333
334
335
336

        device->Release();
        array.append(sinkObject);
    }
    devices->Release();
337
    defaultDevice->Release();
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356

    document.setArray(array);

    NetworkPacket np(PACKET_TYPE_SYSTEMVOLUME);
    np.set<QJsonDocument>(QStringLiteral("sinkList"), document);
    sendPacket(np);
    return true;
}

void SystemvolumePlugin::connected()
{
    if (!valid)
        return;

    deviceCallback = new CMMNotificationClient(*this);
    deviceEnumerator->RegisterEndpointNotificationCallback(deviceCallback);
    sendSinkList();
}

357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
static HRESULT setDefaultAudioPlaybackDevice(PCWSTR deviceId) {
    if (deviceId == nullptr) return ERROR_BAD_UNIT;

    IPolicyConfigVista *pPolicyConfig;
    HRESULT hr = CoCreateInstance(
            __uuidof(CPolicyConfigVistaClient),
            NULL,
            CLSCTX_ALL,
            __uuidof(IPolicyConfigVista),
            (LPVOID *)&pPolicyConfig
    );

    if (SUCCEEDED(hr))
    {
        hr = pPolicyConfig->SetDefaultEndpoint(deviceId, eMultimedia);
        pPolicyConfig->Release();
    }

    return hr;
}

HRESULT SystemvolumePlugin::setDefaultAudioPlaybackDevice(QString& name, bool enabled)
{
    if (!enabled) return S_OK;

    PWSTR deviceId = nullptr;
    for (auto& entry : idToNameMap.toStdMap()) {
        if (entry.second == name) {

            deviceId = new WCHAR[entry.first.length()];
            wcscpy(deviceId, entry.first.toStdWString().data());
            break;
        }
    }

    if (deviceId == nullptr) return ERROR_BAD_UNIT;

    HRESULT hr = ::setDefaultAudioPlaybackDevice(deviceId);

    delete[] deviceId;

    return hr;
}

401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
bool SystemvolumePlugin::receivePacket(const NetworkPacket &np)
{
    if (!valid)
        return false;

    if (np.has(QStringLiteral("requestSinks")))
    {
        return sendSinkList();
    }
    else
    {
        QString name = np.get<QString>(QStringLiteral("name"));

        if (sinkList.contains(name))
        {
416
417
418
419
420
421
422
423
            // unregister ControlChangeNotify before doing any changes to a sink
            HRESULT unregisterSuccess = E_POINTER;
            auto sinkListIterator = this->sinkList.find(name);
            auto &sink = sinkListIterator.value();
            if (!(sinkListIterator == this->sinkList.end())) {
                unregisterSuccess = sink.first->UnregisterControlChangeNotify(sink.second);
            }

424
425
            if (np.has(QStringLiteral("volume")))
            {
426
427
428
429
430
431
                float currentVolume;
                sink.first->GetMasterVolumeLevelScalar(&currentVolume);
                float requestedVolume = (float)np.get<int>(QStringLiteral("volume"), 100) / 100;
                if (currentVolume != requestedVolume) {
                    sinkList[name].first->SetMasterVolumeLevelScalar(requestedVolume, NULL);
                }
432
            }
433

434
435
            if (np.has(QStringLiteral("muted")))
            {
436
437
438
439
440
441
                BOOL currentMuteStatus;
                sink.first->GetMute(&currentMuteStatus);
                BOOL requestedMuteStatus = np.get<bool>(QStringLiteral("muted"), false);
                if(currentMuteStatus != requestedMuteStatus) {
                    sinkList[name].first->SetMute(requestedMuteStatus, NULL);
                }
442
            }
443

444
445
            if (np.has(QStringLiteral("enabled")))
            {
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
                // get the current default device ID
                IMMDevice *defaultDevice = nullptr;
                deviceEnumerator->GetDefaultAudioEndpoint(eRender, eMultimedia, &defaultDevice);
                LPWSTR defaultId = NULL;
                defaultDevice->GetId(&defaultId);
                defaultDevice->Release();
                // get current sink's device ID
                QString qDefaultId = QString::fromWCharArray(defaultId);
                QString currentDeviceId = idToNameMap.key(name);

                if ((bool)qDefaultId.compare(currentDeviceId)) {
                    setDefaultAudioPlaybackDevice(name, np.get<bool>(QStringLiteral("enabled")));
                }
            }

            // re-register ControlChangeNotify in case we unregistered it
            if (unregisterSuccess == S_OK) {
                sink.first->RegisterControlChangeNotify(sink.second);
464
            }
465
466
467
468
469
        }
    }
    return true;
}

Nicolas Fella's avatar
Nicolas Fella committed
470
#include "systemvolumeplugin-win.moc"