2025-08-05 13:40:28 +00:00

835 lines
21 KiB
C++

/* GStreamer
* Copyright (C) 2025 Seungha Yang <seungha@centricular.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "gstwasapi2enumerator.h"
#include "gstwasapi2activator.h"
#include <mutex>
#include <condition_variable>
#include <wrl.h>
#include <functiondiscoverykeys_devpkey.h>
#include <string>
/* *INDENT-OFF* */
using namespace Microsoft::WRL;
#ifndef GST_DISABLE_GST_DEBUG
#define GST_CAT_DEFAULT ensure_debug_category()
static GstDebugCategory *
ensure_debug_category (void)
{
static GstDebugCategory *cat = nullptr;
GST_WASAPI2_CALL_ONCE_BEGIN {
cat = _gst_debug_category_new ("wasapi2enumerator", 0, "wasapi2enumerator");
} GST_WASAPI2_CALL_ONCE_END;
return cat;
}
#endif
static GstStaticCaps template_caps = GST_STATIC_CAPS (GST_WASAPI2_STATIC_CAPS);
static void gst_wasapi2_on_device_updated (GstWasapi2Enumerator * object);
static std::string
device_state_to_string (DWORD state)
{
std::string ret;
bool is_first = true;
if ((state & DEVICE_STATE_ACTIVE) == DEVICE_STATE_ACTIVE) {
if (!is_first)
ret += "|";
ret += "ACTIVE";
is_first = false;
}
if ((state & DEVICE_STATE_DISABLED) == DEVICE_STATE_DISABLED) {
if (!is_first)
ret += "|";
ret += "DISABLED";
is_first = false;
}
if ((state & DEVICE_STATE_NOTPRESENT) == DEVICE_STATE_NOTPRESENT) {
if (!is_first)
ret += "|";
ret += "NOTPRESENT";
is_first = false;
}
if ((state & DEVICE_STATE_UNPLUGGED) == DEVICE_STATE_UNPLUGGED) {
if (!is_first)
ret += "|";
ret += "UNPLUGGED";
is_first = false;
}
return ret;
}
/* IMMNotificationClient implementation */
class IWasapi2NotificationClient : public IMMNotificationClient
{
public:
static void
CreateInstance (GstWasapi2Enumerator * object, IMMNotificationClient ** client)
{
auto self = new IWasapi2NotificationClient ();
g_weak_ref_set (&self->obj_, object);
*client = (IMMNotificationClient *) self;
}
/* IUnknown */
STDMETHODIMP
QueryInterface (REFIID riid, void ** object)
{
if (!object)
return E_POINTER;
if (riid == IID_IUnknown) {
*object = static_cast<IUnknown *> (this);
} else if (riid == __uuidof(IMMNotificationClient)) {
*object = static_cast<IMMNotificationClient *> (this);
} else {
*object = nullptr;
return E_NOINTERFACE;
}
AddRef ();
return S_OK;
}
STDMETHODIMP_ (ULONG)
AddRef (void)
{
return InterlockedIncrement (&ref_count_);
}
STDMETHODIMP_ (ULONG)
Release (void)
{
ULONG ref_count;
GST_TRACE ("%p, %d", this, (guint) ref_count_);
ref_count = InterlockedDecrement (&ref_count_);
if (ref_count == 0) {
GST_TRACE ("Delete instance %p", this);
delete this;
}
return ref_count;
}
/* IMMNotificationClient */
STDMETHODIMP
OnDeviceStateChanged (LPCWSTR device_id, DWORD new_state)
{
auto object = (GstWasapi2Enumerator *) g_weak_ref_get (&obj_);
if (!object)
return S_OK;
auto id = g_utf16_to_utf8 ((gunichar2 *) device_id,
-1, nullptr, nullptr, nullptr);
auto state = device_state_to_string (new_state);
GST_LOG ("%s, %s (0x%x)", id, state.c_str (), (guint) new_state);
g_free (id);
gst_wasapi2_on_device_updated (object);
gst_object_unref (object);
return S_OK;
}
STDMETHODIMP
OnDeviceAdded (LPCWSTR device_id)
{
auto object = (GstWasapi2Enumerator *) g_weak_ref_get (&obj_);
if (!object)
return S_OK;
auto id = g_utf16_to_utf8 ((gunichar2 *) device_id,
-1, nullptr, nullptr, nullptr);
GST_LOG ("%s", id);
g_free (id);
gst_wasapi2_on_device_updated (object);
gst_object_unref (object);
return S_OK;
}
STDMETHODIMP
OnDeviceRemoved (LPCWSTR device_id)
{
auto object = (GstWasapi2Enumerator *) g_weak_ref_get (&obj_);
if (!object)
return S_OK;
auto id = g_utf16_to_utf8 ((gunichar2 *) device_id,
-1, nullptr, nullptr, nullptr);
GST_LOG ("%s", id);
g_free (id);
gst_wasapi2_on_device_updated (object);
gst_object_unref (object);
return S_OK;
}
STDMETHODIMP
OnDefaultDeviceChanged (EDataFlow flow, ERole role, LPCWSTR device_id)
{
auto object = (GstWasapi2Enumerator *) g_weak_ref_get (&obj_);
if (!object)
return S_OK;
auto id = g_utf16_to_utf8 ((gunichar2 *) device_id,
-1, nullptr, nullptr, nullptr);
GST_LOG ("%s, flow: %s, role: %s", id,
gst_wasapi2_data_flow_to_string (flow),
gst_wasapi2_role_to_string (role));
g_free (id);
gst_wasapi2_on_device_updated (object);
gst_object_unref (object);
return S_OK;
}
STDMETHODIMP
OnPropertyValueChanged (LPCWSTR device_id, const PROPERTYKEY key)
{
return S_OK;
}
private:
IWasapi2NotificationClient ()
{
g_weak_ref_init (&obj_, nullptr);
}
virtual ~IWasapi2NotificationClient ()
{
g_weak_ref_clear (&obj_);
}
private:
ULONG ref_count_ = 1;
GWeakRef obj_;
};
enum
{
PROP_0,
PROP_ENUMERATOR,
};
enum
{
SIGNAL_UPDATED,
SIGNAL_LAST,
};
static guint wasapi2_device_signals[SIGNAL_LAST] = { };
struct GstWasapi2EnumeratorPrivate
{
ComPtr<IMMDeviceEnumerator> handle;
std::mutex lock;
std::condition_variable cond;
ComPtr<IMMNotificationClient> client;
Wasapi2ActivationHandler *capture_activator = nullptr;
Wasapi2ActivationHandler *render_activator = nullptr;
void ClearCOM ()
{
if (capture_activator) {
capture_activator->GetClient (nullptr, INFINITE);
capture_activator->Release ();
}
if (render_activator) {
render_activator->GetClient (nullptr, INFINITE);
render_activator->Release ();
}
if (client && handle)
handle->UnregisterEndpointNotificationCallback (client.Get ());
client = nullptr;
handle = nullptr;
}
};
/* *INDENT-ON* */
struct _GstWasapi2Enumerator
{
GstObject parent;
GstWasapi2EnumeratorPrivate *priv;
GThread *thread;
GMainContext *context;
GMainLoop *loop;
};
static void gst_wasapi2_enumerator_finalize (GObject * object);
#define gst_wasapi2_enumerator_parent_class parent_class
G_DEFINE_TYPE (GstWasapi2Enumerator, gst_wasapi2_enumerator, GST_TYPE_OBJECT);
static void
gst_wasapi2_enumerator_class_init (GstWasapi2EnumeratorClass * klass)
{
auto object_class = G_OBJECT_CLASS (klass);
object_class->finalize = gst_wasapi2_enumerator_finalize;
wasapi2_device_signals[SIGNAL_UPDATED] =
g_signal_new_class_handler ("updated", G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST, nullptr, nullptr, nullptr, nullptr, G_TYPE_NONE, 0);
}
static void
gst_wasapi2_enumerator_init (GstWasapi2Enumerator * self)
{
self->priv = new GstWasapi2EnumeratorPrivate ();
self->context = g_main_context_new ();
self->loop = g_main_loop_new (self->context, FALSE);
}
static void
gst_wasapi2_enumerator_finalize (GObject * object)
{
auto self = GST_WASAPI2_ENUMERATOR (object);
g_main_loop_quit (self->loop);
g_thread_join (self->thread);
g_main_loop_unref (self->loop);
g_main_context_unref (self->context);
delete self->priv;
G_OBJECT_CLASS (parent_class)->finalize (object);
}
static void
gst_wasapi2_on_device_updated (GstWasapi2Enumerator * object)
{
/* *INDENT-OFF* */
g_main_context_invoke_full (object->context, G_PRIORITY_DEFAULT,
[] (gpointer obj) -> gboolean {
g_signal_emit (obj, wasapi2_device_signals[SIGNAL_UPDATED], 0);
return G_SOURCE_REMOVE;
},
gst_object_ref (object), (GDestroyNotify) gst_object_unref);
/* *INDENT-ON* */
}
static gpointer
gst_wasapi2_enumerator_thread_func (GstWasapi2Enumerator * self)
{
auto priv = self->priv;
CoInitializeEx (nullptr, COINIT_MULTITHREADED);
g_main_context_push_thread_default (self->context);
auto idle_source = g_idle_source_new ();
/* *INDENT-OFF* */
g_source_set_callback (idle_source,
[] (gpointer user_data) -> gboolean {
auto self = (GstWasapi2Enumerator *) user_data;
auto priv = self->priv;
std::lock_guard < std::mutex > lk (priv->lock);
priv->cond.notify_all ();
return G_SOURCE_REMOVE;
},
self, nullptr);
/* *INDENT-ON* */
g_source_attach (idle_source, self->context);
g_source_unref (idle_source);
auto hr = CoCreateInstance (__uuidof (MMDeviceEnumerator),
nullptr, CLSCTX_ALL, IID_PPV_ARGS (&priv->handle));
if (FAILED (hr)) {
GST_ERROR_OBJECT (self, "Failed to create IMMDeviceEnumerator instance");
goto run_loop;
}
if (gst_wasapi2_can_automatic_stream_routing ()) {
Wasapi2ActivationHandler::CreateInstance (&priv->capture_activator,
gst_wasapi2_get_default_device_id_wide (eCapture), nullptr);
priv->capture_activator->ActivateAsync ();
Wasapi2ActivationHandler::CreateInstance (&priv->render_activator,
gst_wasapi2_get_default_device_id_wide (eRender), nullptr);
priv->render_activator->ActivateAsync ();
}
run_loop:
GST_INFO_OBJECT (self, "Starting loop");
g_main_loop_run (self->loop);
GST_INFO_OBJECT (self, "Stopped loop");
priv->ClearCOM ();
g_main_context_pop_thread_default (self->context);
CoUninitialize ();
return nullptr;
}
GstWasapi2Enumerator *
gst_wasapi2_enumerator_new (void)
{
auto self = (GstWasapi2Enumerator *)
g_object_new (GST_TYPE_WASAPI2_ENUMERATOR, nullptr);
gst_object_ref_sink (self);
auto priv = self->priv;
{
std::unique_lock < std::mutex > lk (priv->lock);
self->thread = g_thread_new ("GstWasapi2Enumerator",
(GThreadFunc) gst_wasapi2_enumerator_thread_func, self);
while (!g_main_loop_is_running (self->loop))
priv->cond.wait (lk);
}
if (!priv->handle) {
gst_object_unref (self);
return nullptr;
}
return self;
}
/* *INDENT-OFF* */
struct ActivateNotificationData
{
ActivateNotificationData ()
{
event = CreateEvent (nullptr, FALSE, FALSE, nullptr);
}
~ActivateNotificationData ()
{
CloseHandle (event);
}
GstWasapi2Enumerator *self;
gboolean active;
HANDLE event;
};
/* *INDENT-ON* */
static gboolean
set_notification_callback (ActivateNotificationData * data)
{
auto self = data->self;
auto priv = self->priv;
if (data->active) {
if (!priv->client) {
ComPtr < IMMNotificationClient > client;
IWasapi2NotificationClient::CreateInstance (self, &client);
auto hr =
priv->handle->RegisterEndpointNotificationCallback (client.Get ());
if (FAILED (hr)) {
GST_ERROR_OBJECT (self, "Couldn't register callback");
} else {
GST_LOG_OBJECT (self, "Registered notification");
priv->client = client;
}
}
} else if (priv->client) {
priv->handle->UnregisterEndpointNotificationCallback (priv->client.Get ());
priv->client = nullptr;
GST_LOG_OBJECT (self, "Unregistered notification");
}
SetEvent (data->event);
return G_SOURCE_REMOVE;
}
void
gst_wasapi2_enumerator_activate_notification (GstWasapi2Enumerator * object,
gboolean active)
{
auto priv = object->priv;
if (!priv->handle)
return;
ActivateNotificationData data;
data.self = object;
data.active = active;
g_main_context_invoke (object->context,
(GSourceFunc) set_notification_callback, &data);
WaitForSingleObject (data.event, INFINITE);
}
void
gst_wasapi2_enumerator_entry_free (GstWasapi2EnumeratorEntry * entry)
{
delete entry;
}
/* *INDENT-OFF* */
struct EnumerateData
{
EnumerateData ()
{
event = CreateEvent (nullptr, FALSE, FALSE, nullptr);
}
~EnumerateData ()
{
CloseHandle (event);
}
GstWasapi2Enumerator *self;
GPtrArray *device_list;
HANDLE event;
};
/* *INDENT-ON* */
static void
gst_wasapi2_enumerator_add_entry (GstWasapi2Enumerator * self,
IAudioClient * client,
GstCaps * static_caps, EDataFlow flow, gboolean is_default,
gchar * device_id, gchar * device_name,
gchar * actual_device_id, gchar * actual_device_name,
GstWasapi2DeviceProps * device_props, GPtrArray * device_list)
{
WAVEFORMATEX *mix_format = nullptr;
GstCaps *supported_caps = nullptr;
client->GetMixFormat (&mix_format);
if (!mix_format) {
g_free (device_id);
g_free (device_name);
g_free (actual_device_id);
g_free (actual_device_name);
return;
}
gst_wasapi2_util_parse_waveformatex (mix_format,
static_caps, &supported_caps, nullptr);
CoTaskMemFree (mix_format);
if (!supported_caps) {
g_free (device_id);
g_free (device_name);
g_free (actual_device_id);
g_free (actual_device_name);
return;
}
auto entry = new GstWasapi2EnumeratorEntry ();
entry->device_id = device_id;
entry->device_name = device_name;
entry->caps = supported_caps;
entry->flow = flow;
entry->is_default = is_default;
if (actual_device_id)
entry->actual_device_id = actual_device_id;
if (actual_device_name)
entry->actual_device_name = actual_device_name;
if (device_props) {
entry->device_props.form_factor = device_props->form_factor;
entry->device_props.enumerator_name = device_props->enumerator_name;
}
GST_LOG_OBJECT (self, "Adding entry %s (%s), flow %d, caps %" GST_PTR_FORMAT,
device_id, device_name, flow, supported_caps);
g_free (device_id);
g_free (device_name);
g_free (actual_device_id);
g_free (actual_device_name);
g_ptr_array_add (device_list, entry);
}
static void
gst_wasapi2_enumerator_probe_props (IPropertyStore * store,
GstWasapi2DeviceProps * props)
{
PROPVARIANT var;
PropVariantInit (&var);
auto hr = store->GetValue (PKEY_AudioEndpoint_FormFactor, &var);
if (SUCCEEDED (hr) && var.vt == VT_UI4)
props->form_factor = (EndpointFormFactor) var.ulVal;
PropVariantClear (&var);
hr = store->GetValue (PKEY_Device_EnumeratorName, &var);
if (SUCCEEDED (hr) && var.vt == VT_LPWSTR) {
auto name = g_utf16_to_utf8 ((gunichar2 *) var.pwszVal,
-1, nullptr, nullptr, nullptr);
props->enumerator_name = name;
g_free (name);
}
PropVariantClear (&var);
}
static void
probe_default_device_props (GstWasapi2Enumerator * self, EDataFlow flow,
GstWasapi2DeviceProps * props, gchar ** actual_device_id,
gchar ** actual_device_name)
{
auto priv = self->priv;
ComPtr < IMMDevice > device;
ComPtr < IPropertyStore > prop;
*actual_device_id = nullptr;
*actual_device_name = nullptr;
auto hr = priv->handle->GetDefaultAudioEndpoint (flow,
eConsole, &device);
if (!gst_wasapi2_result (hr)) {
GST_WARNING_OBJECT (self, "Couldn't get default endpoint for %s",
gst_wasapi2_data_flow_to_string (flow));
return;
}
LPWSTR wid = nullptr;
hr = device->GetId (&wid);
if (gst_wasapi2_result (hr)) {
*actual_device_id = g_utf16_to_utf8 ((gunichar2 *) wid,
-1, nullptr, nullptr, nullptr);
CoTaskMemFree (wid);
}
hr = device->OpenPropertyStore (STGM_READ, &prop);
if (!gst_wasapi2_result (hr))
return;
PROPVARIANT var;
PropVariantInit (&var);
hr = prop->GetValue (PKEY_Device_FriendlyName, &var);
if (gst_wasapi2_result (hr)) {
*actual_device_name = g_utf16_to_utf8 ((gunichar2 *) var.pwszVal,
-1, nullptr, nullptr, nullptr);
PropVariantClear (&var);
}
gst_wasapi2_enumerator_probe_props (prop.Get (), props);
}
static gboolean
gst_wasapi2_enumerator_enumerate_internal (EnumerateData * data)
{
auto self = data->self;
auto priv = self->priv;
ComPtr < IMMDeviceCollection > collection;
auto hr = priv->handle->EnumAudioEndpoints (eAll, DEVICE_STATE_ACTIVE,
&collection);
if (!gst_wasapi2_result (hr)) {
SetEvent (data->event);
return G_SOURCE_REMOVE;
}
UINT count = 0;
hr = collection->GetCount (&count);
if (!gst_wasapi2_result (hr) || count == 0) {
SetEvent (data->event);
return G_SOURCE_REMOVE;
}
auto scaps = gst_static_caps_get (&template_caps);
GST_DEBUG_OBJECT (self, "Start enumerate");
ComPtr < IAudioClient > default_capture_client;
ComPtr < IAudioClient > default_render_client;
if (priv->capture_activator)
priv->capture_activator->GetClient (&default_capture_client, 10000);
if (priv->render_activator)
priv->render_activator->GetClient (&default_render_client, 10000);
if (default_capture_client) {
GstWasapi2DeviceProps props;
props.form_factor = UnknownFormFactor;
props.enumerator_name = "UNKNOWN";
gchar *actual_device_id = nullptr;
gchar *actual_device_name = nullptr;
probe_default_device_props (self, eCapture, &props, &actual_device_id,
&actual_device_name);
gst_wasapi2_enumerator_add_entry (self, default_capture_client.Get (),
scaps, eCapture, TRUE,
g_strdup (gst_wasapi2_get_default_device_id (eCapture)),
g_strdup ("Default Audio Capture Device"), actual_device_id,
actual_device_name, &props, data->device_list);
}
if (default_render_client) {
GstWasapi2DeviceProps props;
props.form_factor = UnknownFormFactor;
props.enumerator_name = "UNKNOWN";
gchar *actual_device_id = nullptr;
gchar *actual_device_name = nullptr;
probe_default_device_props (self, eRender, &props, &actual_device_id,
&actual_device_name);
gst_wasapi2_enumerator_add_entry (self, default_render_client.Get (),
scaps, eRender, TRUE,
g_strdup (gst_wasapi2_get_default_device_id (eRender)),
g_strdup ("Default Audio Render Device"), actual_device_id,
actual_device_name, &props, data->device_list);
}
for (UINT i = 0; i < count; i++) {
ComPtr < IMMDevice > device;
ComPtr < IMMEndpoint > endpoint;
EDataFlow flow;
GstWasapi2DeviceProps props;
props.form_factor = UnknownFormFactor;
props.enumerator_name = "UNKNOWN";
hr = collection->Item (i, &device);
if (!gst_wasapi2_result (hr))
continue;
hr = device.As (&endpoint);
if (!gst_wasapi2_result (hr))
continue;
hr = endpoint->GetDataFlow (&flow);
if (!gst_wasapi2_result (hr))
continue;
ComPtr < IPropertyStore > prop;
hr = device->OpenPropertyStore (STGM_READ, &prop);
if (!gst_wasapi2_result (hr))
continue;
PROPVARIANT var;
PropVariantInit (&var);
hr = prop->GetValue (PKEY_Device_FriendlyName, &var);
if (!gst_wasapi2_result (hr))
continue;
auto desc = g_utf16_to_utf8 ((gunichar2 *) var.pwszVal,
-1, nullptr, nullptr, nullptr);
PropVariantClear (&var);
LPWSTR wid = nullptr;
hr = device->GetId (&wid);
if (!gst_wasapi2_result (hr)) {
g_free (desc);
continue;
}
auto device_id = g_utf16_to_utf8 ((gunichar2 *) wid,
-1, nullptr, nullptr, nullptr);
CoTaskMemFree (wid);
ComPtr < IAudioClient > client;
hr = device->Activate (__uuidof (IAudioClient), CLSCTX_ALL, nullptr,
&client);
if (!gst_wasapi2_result (hr)) {
GST_DEBUG_OBJECT (self, "Couldn't activate device %s (%s)",
device_id, desc);
g_free (device_id);
g_free (desc);
continue;
}
gst_wasapi2_enumerator_probe_props (prop.Get (), &props);
gst_wasapi2_enumerator_add_entry (self, client.Get (), scaps, flow, FALSE,
device_id, desc, nullptr, nullptr, &props, data->device_list);
}
gst_caps_unref (scaps);
GST_DEBUG_OBJECT (self, "End enumerate");
SetEvent (data->event);
return G_SOURCE_REMOVE;
}
void
gst_wasapi2_enumerator_enumerate_devices (GstWasapi2Enumerator * object,
GPtrArray * device_list)
{
EnumerateData data;
data.self = object;
data.device_list = device_list;
g_main_context_invoke (object->context,
(GSourceFunc) gst_wasapi2_enumerator_enumerate_internal, &data);
WaitForSingleObject (data.event, INFINITE);
}
const gchar *
gst_wasapi2_form_factor_to_string (EndpointFormFactor form_factor)
{
switch (form_factor) {
case RemoteNetworkDevice:
return "RemoteNetworkDevice";
case Speakers:
return "Speakers";
case LineLevel:
return "LineLevel";
case Microphone:
return "Microphone";
case Headset:
return "Headset";
case Handset:
return "Handset";
case UnknownDigitalPassthrough:
return "UnknownDigitalPassthrough";
case SPDIF:
return "SPDIF";
case DigitalAudioDisplayDevice:
return "DigitalAudioDisplayDevice";
case UnknownFormFactor:
default:
return "UnknownFormFactor";
}
}