File indexing completed on 2024-04-21 04:56:56

0001 /**
0002  * SPDX-FileCopyrightText: 2018 Jun Bo Bi <jambonmcyeah@gmail.com>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005  */
0006 
0007 #include "systemvolumeplugin-win.h"
0008 
0009 #include <QDebug>
0010 #include <QJsonArray>
0011 #include <QJsonDocument>
0012 #include <QJsonObject>
0013 
0014 #include <KPluginFactory>
0015 
0016 #include <Functiondiscoverykeys_devpkey.h>
0017 
0018 #include <core/device.h>
0019 
0020 #include "PolicyConfig.h"
0021 #include "plugin_systemvolume_debug.h"
0022 
0023 K_PLUGIN_CLASS_WITH_JSON(SystemvolumePlugin, "kdeconnect_systemvolume.json")
0024 
0025 // Private classes of SystemvolumePlugin
0026 
0027 class SystemvolumePlugin::CAudioEndpointVolumeCallback : public IAudioEndpointVolumeCallback
0028 {
0029     LONG _cRef;
0030 
0031 public:
0032     CAudioEndpointVolumeCallback(SystemvolumePlugin &x, QString sinkName)
0033         : enclosing(x)
0034         , name(std::move(sinkName))
0035         , _cRef(1)
0036     {
0037     }
0038     ~CAudioEndpointVolumeCallback(){};
0039 
0040     // IUnknown methods -- AddRef, Release, and QueryInterface
0041 
0042     ULONG STDMETHODCALLTYPE AddRef() override
0043     {
0044         return InterlockedIncrement(&_cRef);
0045     }
0046 
0047     ULONG STDMETHODCALLTYPE Release() override
0048     {
0049         ULONG ulRef = InterlockedDecrement(&_cRef);
0050         if (ulRef == 0) {
0051             delete this;
0052         }
0053         return ulRef;
0054     }
0055 
0056     HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, VOID **ppvInterface) override
0057     {
0058         if (IID_IUnknown == riid) {
0059             AddRef();
0060             *ppvInterface = (IUnknown *)this;
0061         } else if (__uuidof(IMMNotificationClient) == riid) {
0062             AddRef();
0063             *ppvInterface = (IMMNotificationClient *)this;
0064         } else {
0065             *ppvInterface = NULL;
0066             return E_NOINTERFACE;
0067         }
0068         return S_OK;
0069     }
0070 
0071     // Callback method for endpoint-volume-change notifications.
0072 
0073     HRESULT STDMETHODCALLTYPE OnNotify(PAUDIO_VOLUME_NOTIFICATION_DATA pNotify) override
0074     {
0075         NetworkPacket np(PACKET_TYPE_SYSTEMVOLUME);
0076         np.set<int>(QStringLiteral("volume"), (int)(pNotify->fMasterVolume * 100));
0077         np.set<bool>(QStringLiteral("muted"), pNotify->bMuted);
0078         np.set<QString>(QStringLiteral("name"), name);
0079         enclosing.sendPacket(np);
0080 
0081         return S_OK;
0082     }
0083 
0084 private:
0085     SystemvolumePlugin &enclosing;
0086     QString name;
0087 };
0088 
0089 class SystemvolumePlugin::CMMNotificationClient : public IMMNotificationClient
0090 {
0091 public:
0092     CMMNotificationClient(SystemvolumePlugin &x)
0093         : enclosing(x)
0094         , _cRef(1){};
0095 
0096     ~CMMNotificationClient(){};
0097 
0098     // IUnknown methods -- AddRef, Release, and QueryInterface
0099 
0100     ULONG STDMETHODCALLTYPE AddRef() override
0101     {
0102         return InterlockedIncrement(&_cRef);
0103     }
0104 
0105     ULONG STDMETHODCALLTYPE Release() override
0106     {
0107         ULONG ulRef = InterlockedDecrement(&_cRef);
0108         if (ulRef == 0) {
0109             delete this;
0110         }
0111         return ulRef;
0112     }
0113 
0114     HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, VOID **ppvInterface) override
0115     {
0116         if (IID_IUnknown == riid) {
0117             AddRef();
0118             *ppvInterface = (IUnknown *)this;
0119         } else if (__uuidof(IMMNotificationClient) == riid) {
0120             AddRef();
0121             *ppvInterface = (IMMNotificationClient *)this;
0122         } else {
0123             *ppvInterface = NULL;
0124             return E_NOINTERFACE;
0125         }
0126         return S_OK;
0127     }
0128 
0129     // Callback methods for device-event notifications.
0130 
0131     HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId) override
0132     {
0133         if (flow == eRender) {
0134             enclosing.sendSinkList();
0135         }
0136         return S_OK;
0137     }
0138 
0139     HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR pwstrDeviceId) override
0140     {
0141         enclosing.sendSinkList();
0142         return S_OK;
0143     }
0144 
0145     struct RemovedDeviceThreadData {
0146         QString qDeviceId;
0147         SystemvolumePlugin *plugin;
0148     };
0149 
0150     static DWORD WINAPI releaseRemovedDevice(_In_ LPVOID lpParameter)
0151     {
0152         auto *data = static_cast<RemovedDeviceThreadData *>(lpParameter);
0153 
0154         if (!data->plugin->sinkList.empty()) {
0155             auto idToNameIterator = data->plugin->idToNameMap.find(data->qDeviceId);
0156             if (idToNameIterator == data->plugin->idToNameMap.end())
0157                 return 0;
0158 
0159             QString &sinkName = idToNameIterator.value();
0160 
0161             auto sinkListIterator = data->plugin->sinkList.find(sinkName);
0162             if (sinkListIterator == data->plugin->sinkList.end())
0163                 return 0;
0164 
0165             auto &sink = sinkListIterator.value();
0166 
0167             sink.first->UnregisterControlChangeNotify(sink.second);
0168             sink.first->Release();
0169             sink.second->Release();
0170 
0171             data->plugin->idToNameMap.remove(data->qDeviceId);
0172             data->plugin->sinkList.remove(sinkName);
0173         }
0174 
0175         data->plugin->sendSinkList();
0176 
0177         return 0;
0178     }
0179 
0180     HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR pwstrDeviceId) override
0181     {
0182         static RemovedDeviceThreadData data{};
0183         data.qDeviceId = QString::fromWCharArray(pwstrDeviceId);
0184         data.plugin = &enclosing;
0185 
0186         DWORD threadId;
0187         HANDLE threadHandle = CreateThread(NULL, 0, releaseRemovedDevice, &data, 0, &threadId);
0188         CloseHandle(threadHandle);
0189 
0190         enclosing.sendSinkList();
0191 
0192         return S_OK;
0193     }
0194 
0195     HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(LPCWSTR pwstrDeviceId, DWORD dwNewState) override
0196     {
0197         if (dwNewState == DEVICE_STATE_UNPLUGGED)
0198             return OnDeviceRemoved(pwstrDeviceId);
0199         return S_OK;
0200     }
0201 
0202     HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(LPCWSTR pwstrDeviceId, const PROPERTYKEY key) override
0203     {
0204         // This callback is supper spammy. Care only about name and description changes.
0205         if (IsEqualPropertyKey(key, PKEY_Device_FriendlyName)) {
0206             enclosing.sendSinkList();
0207         }
0208 #ifndef __MINGW32__
0209         else if (IsEqualPropertyKey(key, PKEY_Device_DeviceDesc)) {
0210             enclosing.sendSinkList();
0211         }
0212 #endif
0213         return S_OK;
0214     }
0215 
0216 private:
0217     LONG _cRef;
0218     SystemvolumePlugin &enclosing;
0219 };
0220 
0221 SystemvolumePlugin::SystemvolumePlugin(QObject *parent, const QVariantList &args)
0222     : KdeConnectPlugin(parent, args)
0223     , sinkList()
0224 {
0225     CoInitialize(nullptr);
0226     deviceEnumerator = nullptr;
0227     HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER, __uuidof(IMMDeviceEnumerator), (LPVOID *)&(deviceEnumerator));
0228     valid = (hr == S_OK);
0229     if (!valid) {
0230         qWarning("Initialization failed: Failed to create MMDeviceEnumerator");
0231         qWarning("Error Code: %lx", hr);
0232     }
0233 }
0234 
0235 SystemvolumePlugin::~SystemvolumePlugin()
0236 {
0237     if (valid) {
0238         for (auto &entry : sinkList) {
0239             entry.first->UnregisterControlChangeNotify(entry.second);
0240             entry.second->Release();
0241             entry.first->Release();
0242         }
0243         deviceEnumerator->UnregisterEndpointNotificationCallback(deviceCallback);
0244         deviceEnumerator->Release();
0245         deviceEnumerator = nullptr;
0246     }
0247 }
0248 
0249 bool SystemvolumePlugin::sendSinkList()
0250 {
0251     if (!valid)
0252         return false;
0253 
0254     QJsonDocument document;
0255     QJsonArray array;
0256 
0257     IMMDeviceCollection *devices = nullptr;
0258     HRESULT hr = deviceEnumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &devices);
0259 
0260     if (hr != S_OK) {
0261         qWarning("Failed to Enumumerate AudioEndpoints");
0262         qWarning("Error Code: %lx", hr);
0263         return false;
0264     }
0265     unsigned int deviceCount;
0266     devices->GetCount(&deviceCount);
0267 
0268     if (!deviceCount) {
0269         qWarning("No audio devices detected");
0270         return false;
0271     }
0272 
0273     IMMDevice *defaultDevice = nullptr;
0274     deviceEnumerator->GetDefaultAudioEndpoint(eRender, eMultimedia, &defaultDevice);
0275 
0276     if (!defaultDevice) {
0277         qWarning("No default audio device detected");
0278         return false;
0279     }
0280 
0281     LPWSTR defaultId = NULL;
0282     defaultDevice->GetId(&defaultId);
0283 
0284     for (unsigned int i = 0; i < deviceCount; i++) {
0285         IMMDevice *device = nullptr;
0286 
0287         IPropertyStore *deviceProperties = nullptr;
0288         PROPVARIANT deviceProperty;
0289         QString name;
0290         QString desc;
0291         LPWSTR deviceId;
0292         float volume;
0293         BOOL muted;
0294 
0295         IAudioEndpointVolume *endpoint = nullptr;
0296         CAudioEndpointVolumeCallback *callback;
0297 
0298         // Get Properties
0299         devices->Item(i, &device);
0300         device->OpenPropertyStore(STGM_READ, &deviceProperties);
0301 
0302         deviceProperties->GetValue(PKEY_Device_FriendlyName, &deviceProperty);
0303         name = QString::fromWCharArray(deviceProperty.pwszVal);
0304         PropVariantClear(&deviceProperty);
0305 
0306 #ifndef __MINGW32__
0307         deviceProperties->GetValue(PKEY_Device_DeviceDesc, &deviceProperty);
0308         desc = QString::fromWCharArray(deviceProperty.pwszVal);
0309         PropVariantClear(&deviceProperty);
0310 #else
0311         desc = name;
0312 #endif
0313 
0314         QJsonObject sinkObject;
0315         sinkObject.insert(QStringLiteral("name"), name);
0316         sinkObject.insert(QStringLiteral("description"), desc);
0317 
0318         hr = device->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_ALL, NULL, (void **)&endpoint);
0319         if (hr != S_OK) {
0320             qWarning() << "Failed to create IAudioEndpointVolume for device:" << name;
0321             qWarning("Error Code: %lx", hr);
0322 
0323             device->Release();
0324             continue;
0325         }
0326 
0327         device->GetId(&deviceId);
0328         endpoint->GetMasterVolumeLevelScalar(&volume);
0329         endpoint->GetMute(&muted);
0330 
0331         sinkObject.insert(QStringLiteral("muted"), (bool)muted);
0332         sinkObject.insert(QStringLiteral("volume"), (qint64)(volume * 100));
0333         sinkObject.insert(QStringLiteral("maxVolume"), (qint64)100);
0334         sinkObject.insert(QStringLiteral("enabled"), lstrcmpW(deviceId, defaultId) == 0);
0335 
0336         // Register Callback
0337         if (!sinkList.contains(name)) {
0338             callback = new CAudioEndpointVolumeCallback(*this, name);
0339             endpoint->RegisterControlChangeNotify(callback);
0340             sinkList[name] = qMakePair(endpoint, callback);
0341         }
0342 
0343         QString qDeviceId = QString::fromWCharArray(deviceId);
0344         idToNameMap[qDeviceId] = name;
0345 
0346         device->Release();
0347         array.append(sinkObject);
0348     }
0349     devices->Release();
0350     defaultDevice->Release();
0351 
0352     document.setArray(array);
0353 
0354     NetworkPacket np(PACKET_TYPE_SYSTEMVOLUME);
0355     np.set<QJsonDocument>(QStringLiteral("sinkList"), document);
0356     sendPacket(np);
0357     return true;
0358 }
0359 
0360 void SystemvolumePlugin::connected()
0361 {
0362     if (!valid)
0363         return;
0364 
0365     deviceCallback = new CMMNotificationClient(*this);
0366     deviceEnumerator->RegisterEndpointNotificationCallback(deviceCallback);
0367     sendSinkList();
0368 }
0369 
0370 static HRESULT setDefaultAudioPlaybackDevice(PCWSTR deviceId)
0371 {
0372     if (deviceId == nullptr)
0373         return ERROR_BAD_UNIT;
0374 
0375     IPolicyConfigVista *pPolicyConfig;
0376     HRESULT hr = CoCreateInstance(__uuidof(CPolicyConfigVistaClient), NULL, CLSCTX_ALL, __uuidof(IPolicyConfigVista), (LPVOID *)&pPolicyConfig);
0377 
0378     if (SUCCEEDED(hr)) {
0379         hr = pPolicyConfig->SetDefaultEndpoint(deviceId, eMultimedia);
0380         pPolicyConfig->Release();
0381     }
0382 
0383     return hr;
0384 }
0385 
0386 HRESULT SystemvolumePlugin::setDefaultAudioPlaybackDevice(QString &name, bool enabled)
0387 {
0388     if (!enabled)
0389         return S_OK;
0390 
0391     PWSTR deviceId = nullptr;
0392     for (auto &entry : idToNameMap.toStdMap()) {
0393         if (entry.second == name) {
0394             deviceId = new WCHAR[entry.first.length()];
0395             wcscpy(deviceId, entry.first.toStdWString().data());
0396             break;
0397         }
0398     }
0399 
0400     if (deviceId == nullptr)
0401         return ERROR_BAD_UNIT;
0402 
0403     HRESULT hr = ::setDefaultAudioPlaybackDevice(deviceId);
0404 
0405     delete[] deviceId;
0406 
0407     return hr;
0408 }
0409 
0410 void SystemvolumePlugin::receivePacket(const NetworkPacket &np)
0411 {
0412     if (!valid)
0413         return;
0414 
0415     if (np.has(QStringLiteral("requestSinks"))) {
0416         sendSinkList();
0417     } else {
0418         QString name = np.get<QString>(QStringLiteral("name"));
0419 
0420         auto sinkListIterator = this->sinkList.find(name);
0421         if (sinkListIterator != this->sinkList.end()) {
0422             auto &sink = sinkListIterator.value();
0423 
0424             if (np.has(QStringLiteral("volume"))) {
0425                 float requestedVolume = (float)np.get<int>(QStringLiteral("volume"), 100) / 100;
0426                 sinkList[name].first->SetMasterVolumeLevelScalar(requestedVolume, NULL);
0427             }
0428 
0429             if (np.has(QStringLiteral("muted"))) {
0430                 BOOL requestedMuteStatus = np.get<bool>(QStringLiteral("muted"), false);
0431                 sinkList[name].first->SetMute(requestedMuteStatus, NULL);
0432             }
0433 
0434             if (np.has(QStringLiteral("enabled"))) {
0435                 // get the current default device ID
0436                 IMMDevice *defaultDevice = nullptr;
0437                 deviceEnumerator->GetDefaultAudioEndpoint(eRender, eMultimedia, &defaultDevice);
0438                 LPWSTR defaultId = NULL;
0439                 defaultDevice->GetId(&defaultId);
0440                 defaultDevice->Release();
0441                 // get current sink's device ID
0442                 QString qDefaultId = QString::fromWCharArray(defaultId);
0443                 QString currentDeviceId = idToNameMap.key(name);
0444                 if (qDefaultId != currentDeviceId) {
0445                     setDefaultAudioPlaybackDevice(name, np.get<bool>(QStringLiteral("enabled")));
0446                 }
0447             }
0448         }
0449     }
0450 }
0451 
0452 #include "moc_systemvolumeplugin-win.cpp"
0453 #include "systemvolumeplugin-win.moc"