File indexing completed on 2025-04-27 04:36:17
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"