Ring Daemon
Loading...
Searching...
No Matches
audio_device_monitor.h
Go to the documentation of this file.
1/*
2 * Copyright (C) 2025-2026 Savoir-faire Linux Inc.
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 */
17
18#include "string_utils.h"
19#include "logger.h"
20
21#include <atlbase.h>
22#include <conio.h>
23#include <mmdeviceapi.h>
24#include <mmreg.h>
25#include <string>
26#include <windows.h>
27#include <Functiondiscoverykeys_devpkey.h>
28
29#include <functional>
30#include <mutex>
31#include <unordered_set>
32
33// Note: the granularity of these events is not currently required because we end up
34// restarting the audio layer regardless of the event type. However, let's keep it like
35// this for now in case a future PortAudio bump or layer improvement can make use of it
36// to avoid unnecessary stream restarts.
38inline std::string
40{
41 switch (type) {
43 return "BecameActive";
45 return "BecameInactive";
47 return "DefaultChanged";
49 return "PropertyChanged";
50 }
51 return "Unknown";
52}
53using DeviceEventCallback = std::function<void(const std::string& deviceName, DeviceEventType)>;
54
55std::string
56GetFriendlyNameFromIMMDeviceId(CComPtr<IMMDeviceEnumerator> enumerator, LPCWSTR deviceId)
57{
58 if (!enumerator || !deviceId)
59 return {};
60
61 CComPtr<IMMDevice> device;
62 CComPtr<IPropertyStore> props;
63
64 if (FAILED(enumerator->GetDevice(deviceId, &device)))
65 return {};
66
67 if (FAILED(device->OpenPropertyStore(STGM_READ, &props)))
68 return {};
69
70 PROPVARIANT varName;
71 PropVariantInit(&varName);
72 HRESULT hr = props->GetValue(PKEY_Device_FriendlyName, &varName);
73
74 std::string result;
75 if (SUCCEEDED(hr) && varName.vt == VT_LPWSTR && varName.pwszVal) {
76 result = jami::to_string(varName.pwszVal);
77 }
78 PropVariantClear(&varName);
79 return result;
80}
81
82class AudioDeviceNotificationClient : public IMMNotificationClient
83{
84 LONG _refCount;
85
86public:
88 : _refCount(1)
89 {
90 if (FAILED(CoCreateInstance(__uuidof(MMDeviceEnumerator),
91 nullptr,
92 CLSCTX_ALL,
93 __uuidof(IMMDeviceEnumerator),
94 (void**) &deviceEnumerator_))) {
95 JAMI_ERR("Failed to create device enumerator");
96 } else {
97 // Fill our list of active devices (render + capture)
98 enumerateDevices();
99 }
100 }
101
103 {
104 if (deviceEnumerator_) {
105 deviceEnumerator_->UnregisterEndpointNotificationCallback(this);
106 }
107 }
108
110 {
111 if (!deviceEnumerator_) {
112 JAMI_ERR("Device enumerator not initialized");
113 return;
114 }
115
116 {
117 std::lock_guard<std::mutex> lock(mutex_);
118 deviceEventCallback_ = callback;
119 }
120
121 // Now we can start monitoring
122 deviceEnumerator_->RegisterEndpointNotificationCallback(this);
123 }
124
125 // // IUnknown methods
126 ULONG STDMETHODCALLTYPE AddRef() override
127 {
128 auto count = InterlockedIncrement(&_refCount);
129 return count;
130 }
131
132 ULONG STDMETHODCALLTYPE Release() override
133 {
134 auto count = InterlockedDecrement(&_refCount);
135 if (count == 0) {
136 delete this;
137 }
138 return count;
139 }
140
141 HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) override
142 {
143 if (riid == __uuidof(IUnknown) || riid == __uuidof(IMMNotificationClient)) {
144 *ppv = static_cast<IMMNotificationClient*>(this);
145 AddRef();
146 return S_OK;
147 }
148 *ppv = nullptr;
149 return E_NOINTERFACE;
150 }
151
152 // IMMNotificationClient methods
153 HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(LPCWSTR deviceId, DWORD newState) override
154 {
155 handleStateChanged(deviceId, newState);
156 return S_OK;
157 }
158
159 HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR deviceId) override
160 {
161 // Treat addition as transition to active - we'll validate the state in handler
162 handleStateChanged(deviceId, DEVICE_STATE_ACTIVE);
163 return S_OK;
164 }
165
166 HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR deviceId) override
167 {
168 // Removed devices should be treated as inactive/unavailable
169 handleStateChanged(deviceId, DEVICE_STATE_NOTPRESENT);
170 return S_OK;
171 }
172
173 HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR deviceId) override
174 {
175 // If the default communication device changes, we should restart the layer
176 // to ensure the new device is used. We only care about the default communication
177 // device, so we ignore other roles.
178 if (role == eCommunications && deviceId && deviceEventCallback_) {
179 std::string friendlyName = GetFriendlyNameFromIMMDeviceId(deviceEnumerator_, deviceId);
180 deviceEventCallback_(friendlyName, DeviceEventType::DefaultChanged);
181 }
182 return S_OK;
183 }
184
185 HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(LPCWSTR deviceId, const PROPERTYKEY key) override
186 {
187 // Limit this to samplerate changes
188 if (key == PKEY_AudioEngine_DeviceFormat) {
189 // Fetch updated sample rate
190 IMMDevice* pDevice;
191 deviceEnumerator_->GetDevice(deviceId, &pDevice);
192
193 IPropertyStore* pProps;
194 pDevice->OpenPropertyStore(STGM_READ, &pProps);
195
196 PROPVARIANT var;
197 PropVariantInit(&var);
198 pProps->GetValue(PKEY_AudioEngine_DeviceFormat, &var);
199
200 if (var.vt == VT_BLOB) {
201 auto* pWaveFormat = reinterpret_cast<WAVEFORMATEXTENSIBLE*>(var.blob.pBlobData);
202 UINT sampleRate = pWaveFormat->Format.nSamplesPerSec;
203 JAMI_DBG("Sample rate changed to %u", sampleRate);
204 std::string friendlyName = GetFriendlyNameFromIMMDeviceId(deviceEnumerator_, deviceId);
205 deviceEventCallback_(friendlyName, DeviceEventType::PropertyChanged);
206 }
207
208 PropVariantClear(&var);
209 pProps->Release();
210 pDevice->Release();
211 }
212 return S_OK;
213 }
214
215private:
216 CComPtr<IMMDeviceEnumerator> deviceEnumerator_;
217
218 DeviceEventCallback deviceEventCallback_;
219 std::unordered_set<std::wstring> activeDevices_;
220
221 // Notifications are invoked on system-managed threads, so we need a lock
222 // to protect deviceEventCallback_ and activeDevices_
223 std::mutex mutex_;
224
225 // Enumerates both render (playback) and capture (recording) devices
226 void enumerateDevices()
227 {
228 if (!deviceEnumerator_)
229 return;
230
231 std::lock_guard<std::mutex> lock(mutex_);
232 activeDevices_.clear();
233
234 enumerateDevicesOfType(eRender);
235 enumerateDevicesOfType(eCapture);
236 }
237
238 void enumerateDevicesOfType(EDataFlow flow)
239 {
240 CComPtr<IMMDeviceCollection> devices;
241 CComPtr<IMMDevice> device;
242 UINT deviceCount = 0;
243
244 if (FAILED(deviceEnumerator_->EnumAudioEndpoints(flow, DEVICE_STATE_ACTIVE, &devices))) {
245 JAMI_ERR("Failed to enumerate devices");
246 return;
247 }
248
249 if (FAILED(devices->GetCount(&deviceCount))) {
250 JAMI_ERR("Failed to get device count");
251 return;
252 }
253
254 for (UINT i = 0; i < deviceCount; ++i) {
255 if (FAILED(devices->Item(i, &device)))
256 continue;
257
258 LPWSTR deviceId = nullptr;
259 if (FAILED(device->GetId(&deviceId)))
260 continue;
261
262 DWORD deviceState = 0;
263 if (SUCCEEDED(device->GetState(&deviceState)) && deviceState == DEVICE_STATE_ACTIVE) {
264 activeDevices_.insert(deviceId);
265 }
266
267 CoTaskMemFree(deviceId);
268 }
269 }
270
271 void handleStateChanged(LPCWSTR deviceId, DWORD newState)
272 {
273 if (!deviceId || !deviceEnumerator_ || !deviceEventCallback_)
274 return;
275
276 std::lock_guard<std::mutex> lock(mutex_);
277
278 std::wstring wsId(deviceId);
279 std::string friendlyName = GetFriendlyNameFromIMMDeviceId(deviceEnumerator_, deviceId);
280
281 // We only care if a device is entering or leaving the active state
282 bool isActive = (newState == DEVICE_STATE_ACTIVE);
283 auto it = activeDevices_.find(wsId);
284
285 if (isActive && it == activeDevices_.end()) {
286 // Device is active and not in our list? Add it and notify
287 activeDevices_.insert(wsId);
288 deviceEventCallback_(friendlyName, DeviceEventType::BecameActive);
289 } else if (!isActive && it != activeDevices_.end()) {
290 // Device is inactive and in our list? Remove it and notify
291 activeDevices_.erase(it);
292 deviceEventCallback_(friendlyName, DeviceEventType::BecameInactive);
293 }
294 }
295};
296
297// RAII COM wrapper for AudioDeviceNotificationClient
302
303typedef std::unique_ptr<AudioDeviceNotificationClient, AudioDeviceNotificationClient_deleter>
std::string GetFriendlyNameFromIMMDeviceId(CComPtr< IMMDeviceEnumerator > enumerator, LPCWSTR deviceId)
std::unique_ptr< AudioDeviceNotificationClient, AudioDeviceNotificationClient_deleter > AudioDeviceNotificationClientPtr
std::string to_string(DeviceEventType type)
std::function< void(const std::string &deviceName, DeviceEventType)> DeviceEventCallback
HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(LPCWSTR deviceId, const PROPERTYKEY key) override
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppv) override
ULONG STDMETHODCALLTYPE AddRef() override
void setDeviceEventCallback(DeviceEventCallback callback)
ULONG STDMETHODCALLTYPE Release() override
HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(LPCWSTR deviceId, DWORD newState) override
HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR deviceId) override
HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR deviceId) override
HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR deviceId) override
#define JAMI_ERR(...)
Definition logger.h:230
#define JAMI_DBG(...)
Definition logger.h:228
std::string to_string(double value)
void operator()(AudioDeviceNotificationClient *client) const