diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -40,7 +40,7 @@ add_subdirectory(remotesystemvolume) endif() -if(KF5PulseAudioQt_FOUND) +if(KF5PulseAudioQt_FOUND OR WIN32) add_subdirectory(systemvolume) endif() diff --git a/plugins/systemvolume/CMakeLists.txt b/plugins/systemvolume/CMakeLists.txt --- a/plugins/systemvolume/CMakeLists.txt +++ b/plugins/systemvolume/CMakeLists.txt @@ -1,11 +1,25 @@ -set(kdeconnect_systemvolume_SRCS - systemvolumeplugin.cpp -) +if(WIN32) + set(kdeconnect_systemvolume_SRCS + systemvolumeplugin-win.cpp + ) +else() + set(kdeconnect_systemvolume_SRCS + systemvolumeplugin-pulse.cpp + ) +endif() kdeconnect_add_plugin(kdeconnect_systemvolume JSON kdeconnect_systemvolume.json SOURCES ${kdeconnect_systemvolume_SRCS}) -target_link_libraries(kdeconnect_systemvolume - kdeconnectcore - Qt5::Core - KF5::PulseAudioQt -) +if(WIN32) + target_link_libraries(kdeconnect_systemvolume + kdeconnectcore + Qt5::Core + ole32 + ) +else() + target_link_libraries(kdeconnect_systemvolume + kdeconnectcore + Qt5::Core + KF5::PulseAudioQt + ) +endif() diff --git a/plugins/systemvolume/systemvolumeplugin.h b/plugins/systemvolume/systemvolumeplugin-pulse.h rename from plugins/systemvolume/systemvolumeplugin.h rename to plugins/systemvolume/systemvolumeplugin-pulse.h --- a/plugins/systemvolume/systemvolumeplugin.h +++ b/plugins/systemvolume/systemvolumeplugin-pulse.h @@ -18,8 +18,8 @@ * along with this program. If not, see . */ -#ifndef SYSTEMVOLUMEPLUGIN_H -#define SYSTEMVOLUMEPLUGIN_H +#ifndef SYSTEMVOLUMEPLUGINPULSE_H +#define SYSTEMVOLUMEPLUGINPULSE_H #include #include diff --git a/plugins/systemvolume/systemvolumeplugin.cpp b/plugins/systemvolume/systemvolumeplugin-pulse.cpp rename from plugins/systemvolume/systemvolumeplugin.cpp rename to plugins/systemvolume/systemvolumeplugin-pulse.cpp --- a/plugins/systemvolume/systemvolumeplugin.cpp +++ b/plugins/systemvolume/systemvolumeplugin-pulse.cpp @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -#include "systemvolumeplugin.h" +#include "systemvolumeplugin-pulse.h" #include @@ -123,5 +123,5 @@ } } -#include "systemvolumeplugin.moc" +#include "systemvolumeplugin-pulse.moc" diff --git a/plugins/systemvolume/systemvolumeplugin-win.h b/plugins/systemvolume/systemvolumeplugin-win.h new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/systemvolumeplugin-win.h @@ -0,0 +1,38 @@ +#ifndef SYSTEMVOLUMEPLUGINWIN_H +#define SYSTEMVOLUMEPLUGINWIN_H + +#include +#include + +#include + +#include +#include +#include + +#define PACKET_TYPE_SYSTEMVOLUME QStringLiteral("kdeconnect.systemvolume") +#define PACKET_TYPE_SYSTEMVOLUME_REQUEST QStringLiteral("kdeconnect.systemvolume.request") + +class Q_DECL_EXPORT SystemvolumePlugin : public KdeConnectPlugin +{ + Q_OBJECT + + public: + explicit SystemvolumePlugin(QObject *parent, const QVariantList &args); + ~SystemvolumePlugin(); + bool receivePacket(const NetworkPacket& np) override; + void connected() override; + + private: + class CMMNotificationClient; + class CAudioEndpointVolumeCallback; + + bool valid; + IMMDeviceEnumerator* deviceEnumerator; + CMMNotificationClient* deviceCallback; + QMap> sinkList; + + bool sendSinkList(); +}; + +#endif // SYSTEMVOLUMEPLUGINWIN_H diff --git a/plugins/systemvolume/systemvolumeplugin-win.cpp b/plugins/systemvolume/systemvolumeplugin-win.cpp new file mode 100644 --- /dev/null +++ b/plugins/systemvolume/systemvolumeplugin-win.cpp @@ -0,0 +1,325 @@ +#include "systemvolumeplugin-win.h" +#include + +#include + +#include +#include +#include +#include +#include + +#include + +K_PLUGIN_FACTORY_WITH_JSON(KdeConnectPluginFactory, "kdeconnect_systemvolume.json", registerPlugin();) + +Q_LOGGING_CATEGORY(KDECONNECT_PLUGIN_SYSTEMVOLUME, "kdeconnect.plugin.systemvolume") + +// Private classes of SystemvolumePlugin + +class SystemvolumePlugin::CMMNotificationClient : public IMMNotificationClient +{ + + public: + CMMNotificationClient(SystemvolumePlugin &x) : enclosing(x), _cRef(1){}; + + ~CMMNotificationClient(){}; + + // 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; + } + + // Callback methods for device-event notifications. + + HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId) override + { + if (flow == eRender) + { + enclosing.sendSinkList(); + } + return S_OK; + } + + HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR pwstrDeviceId) override + { + enclosing.sendSinkList(); + return S_OK; + } + + HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR pwstrDeviceId) override + { + enclosing.sendSinkList(); + return S_OK; + } + + HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(LPCWSTR pwstrDeviceId, DWORD dwNewState) override + { + enclosing.sendSinkList(); + return S_OK; + } + + HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(LPCWSTR pwstrDeviceId, const PROPERTYKEY key) override + { + enclosing.sendSinkList(); + return S_OK; + } + + private: + LONG _cRef; + SystemvolumePlugin &enclosing; +}; + +class SystemvolumePlugin::CAudioEndpointVolumeCallback : public IAudioEndpointVolumeCallback +{ + LONG _cRef; + + public: + CAudioEndpointVolumeCallback(SystemvolumePlugin &x, QString sinkName) : enclosing(x), name(sinkName), _cRef(1) {} + ~CAudioEndpointVolumeCallback(){}; + + // 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; + } + + // Callback method for endpoint-volume-change notifications. + + HRESULT STDMETHODCALLTYPE OnNotify(PAUDIO_VOLUME_NOTIFICATION_DATA pNotify) override + { + NetworkPacket np(PACKET_TYPE_SYSTEMVOLUME); + np.set(QStringLiteral("volume"), (int)(pNotify->fMasterVolume * 100)); + np.set(QStringLiteral("muted"), pNotify->bMuted); + np.set(QStringLiteral("name"), name); + enclosing.sendPacket(np); + + return S_OK; + } + + private: + SystemvolumePlugin &enclosing; + QString name; +}; + +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; + + HRESULT hr; + if (!sinkList.empty()) + { + for (auto const &sink : sinkList) + { + sink.first->UnregisterControlChangeNotify(sink.second); + sink.first->Release(); + sink.second->Release(); + } + sinkList.clear(); + } + IMMDeviceCollection *devices = nullptr; + hr = deviceEnumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &devices); + + if (hr != S_OK) + { + qWarning("Failed to Enumumerate AudioEndpoints"); + qWarning("Error Code: %lx", hr); + return false; + } + unsigned int deviceCount; + devices->GetCount(&deviceCount); + for (unsigned int i = 0; i < deviceCount; i++) + { + IMMDevice *device = nullptr; + + IPropertyStore *deviceProperties = nullptr; + PROPVARIANT deviceProperty; + QString name; + QString desc; + 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); + + deviceProperties->GetValue(PKEY_Device_DeviceDesc, &deviceProperty); + desc = QString::fromWCharArray(deviceProperty.pwszVal); + //PropVariantClear(&deviceProperty); + + QJsonObject sinkObject; + sinkObject.insert("name", name); + sinkObject.insert("description", desc); + + 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; + } + endpoint->GetMasterVolumeLevelScalar(&volume); + endpoint->GetMute(&muted); + + sinkObject.insert("muted", (bool)muted); + sinkObject.insert("volume", (qint64)(volume * 100)); + sinkObject.insert("maxVolume", (qint64)100); + + // Register Callback + callback = new CAudioEndpointVolumeCallback(*this, name); + sinkList[name] = qMakePair(endpoint, callback); + endpoint->RegisterControlChangeNotify(callback); + + device->Release(); + array.append(sinkObject); + } + devices->Release(); + + document.setArray(array); + + NetworkPacket np(PACKET_TYPE_SYSTEMVOLUME); + np.set(QStringLiteral("sinkList"), document); + sendPacket(np); + return true; +} + +void SystemvolumePlugin::connected() +{ + if (!valid) + return; + + deviceCallback = new CMMNotificationClient(*this); + deviceEnumerator->RegisterEndpointNotificationCallback(deviceCallback); + sendSinkList(); +} + +bool SystemvolumePlugin::receivePacket(const NetworkPacket &np) +{ + if (!valid) + return false; + + if (np.has(QStringLiteral("requestSinks"))) + { + return sendSinkList(); + } + else + { + QString name = np.get(QStringLiteral("name")); + + if (sinkList.contains(name)) + { + if (np.has(QStringLiteral("volume"))) + { + sinkList[name].first->SetMasterVolumeLevelScalar((float)np.get(QStringLiteral("volume")) / 100, NULL); + } + if (np.has(QStringLiteral("muted"))) + { + sinkList[name].first->SetMute(np.get(QStringLiteral("muted")), NULL); + } + } + } + return true; +} + +#include "systemvolumeplugin-win.moc" \ No newline at end of file