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;
HRESULT 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);
if (!deviceCount) {
qWarning("No audio devices detected");
return false;
}
IMMDevice *defaultDevice = nullptr;
deviceEnumerator->GetDefaultAudioEndpoint(eRender, eMultimedia, &defaultDevice);
if (!defaultDevice) {
qWarning("No default audio device detected");
return false;
}
LPWSTR defaultId = NULL;
defaultDevice->GetId(&defaultId);
for (unsigned int i = 0; i < deviceCount; i++) {
IMMDevice *device = nullptr;
IPropertyStore *deviceProperties = nullptr;
PROPVARIANT deviceProperty;
QString name;
QString desc;
LPWSTR deviceId;
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);
#ifndef __MINGW32__
deviceProperties->GetValue(PKEY_Device_DeviceDesc, &deviceProperty);
desc = QString::fromWCharArray(deviceProperty.pwszVal);
// PropVariantClear(&deviceProperty);
#else
desc = name;
#endif
QJsonObject sinkObject;
sinkObject.insert(QStringLiteral("name"), name);
sinkObject.insert(QStringLiteral("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;
}
device->GetId(&deviceId);
endpoint->GetMasterVolumeLevelScalar(&volume);
endpoint->GetMute(&muted);
sinkObject.insert(QStringLiteral("muted"), (bool)muted);
sinkObject.insert(QStringLiteral("volume"), (qint64)(volume * 100));
sinkObject.insert(QStringLiteral("maxVolume"), (qint64)100);
sinkObject.insert(QStringLiteral("enabled"), lstrcmpW(deviceId, defaultId) == 0);
// Register Callback
if (!sinkList.contains(name)) {
callback = new CAudioEndpointVolumeCallback(*this, name);
endpoint->RegisterControlChangeNotify(callback);
sinkList[name] = qMakePair(endpoint, callback);
}
QString qDeviceId = QString::fromWCharArray(deviceId);
idToNameMap[qDeviceId] = name;
device->Release();
array.append(sinkObject);
}
devices->Release();
defaultDevice->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();
}
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;
}
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)) {
// 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);
}
if (np.has(QStringLiteral("volume"))) {
float currentVolume;
sink.first->GetMasterVolumeLevelScalar(¤tVolume);
float requestedVolume = (float)np.get(QStringLiteral("volume"), 100) / 100;
if (currentVolume != requestedVolume) {
sinkList[name].first->SetMasterVolumeLevelScalar(requestedVolume, NULL);
}
}
if (np.has(QStringLiteral("muted"))) {
BOOL currentMuteStatus;
sink.first->GetMute(¤tMuteStatus);
BOOL requestedMuteStatus = np.get(QStringLiteral("muted"), false);
if (currentMuteStatus != requestedMuteStatus) {
sinkList[name].first->SetMute(requestedMuteStatus, NULL);
}
}
if (np.has(QStringLiteral("enabled"))) {
// 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(QStringLiteral("enabled")));
}
}
// re-register ControlChangeNotify in case we unregistered it
if (unregisterSuccess == S_OK) {
sink.first->RegisterControlChangeNotify(sink.second);
}
}
}
return true;
}