... and report it via device provider property Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/9586>
943 lines
24 KiB
C++
943 lines
24 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>
|
|
#include <atomic>
|
|
|
|
/* *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 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
|
|
{
|
|
GstWasapi2EnumeratorPrivate ()
|
|
{
|
|
device_list = g_ptr_array_new_with_free_func ((GDestroyNotify)
|
|
gst_wasapi2_enumerator_entry_free);
|
|
endpoint_formats = g_ptr_array_new_with_free_func ((GDestroyNotify)
|
|
gst_wasapi2_free_wfx);
|
|
}
|
|
|
|
~GstWasapi2EnumeratorPrivate ()
|
|
{
|
|
g_ptr_array_unref (device_list);
|
|
g_ptr_array_unref (endpoint_formats);
|
|
}
|
|
|
|
ComPtr<IMMDeviceEnumerator> handle;
|
|
std::mutex lock;
|
|
std::condition_variable cond;
|
|
|
|
ComPtr<IMMNotificationClient> client;
|
|
Wasapi2ActivationHandler *capture_activator = nullptr;
|
|
Wasapi2ActivationHandler *render_activator = nullptr;
|
|
std::atomic<int> notify_count = { 0 };
|
|
GPtrArray *device_list;
|
|
GPtrArray *endpoint_formats;
|
|
|
|
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* */
|
|
auto priv = object->priv;
|
|
|
|
auto count = priv->notify_count.fetch_add (1);
|
|
GST_LOG ("notify count before scheduling %d", count);
|
|
|
|
auto source = g_timeout_source_new (100);
|
|
g_source_set_callback (source,
|
|
[] (gpointer obj) -> gboolean {
|
|
auto self = GST_WASAPI2_ENUMERATOR (obj);
|
|
auto priv = self->priv;
|
|
auto count = priv->notify_count.fetch_sub (1);
|
|
GST_LOG ("scheduled notify count %d", count);
|
|
if (count == 1)
|
|
g_signal_emit (obj, wasapi2_device_signals[SIGNAL_UPDATED], 0);
|
|
return G_SOURCE_REMOVE;
|
|
},
|
|
gst_object_ref (object), (GDestroyNotify) gst_object_unref);
|
|
|
|
g_source_attach (source, object->context);
|
|
g_source_unref (source);
|
|
/* *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 GstWasapi2EnumeratorEntry *
|
|
gst_wasapi2_enumerator_build_entry (GstWasapi2Enumerator * self,
|
|
GstCaps * caps, EDataFlow flow, gboolean is_default,
|
|
gchar * device_id, gchar * device_name,
|
|
gchar * actual_device_id, gchar * actual_device_name,
|
|
GstWasapi2DeviceProps * device_props)
|
|
{
|
|
auto entry = new GstWasapi2EnumeratorEntry ();
|
|
|
|
entry->device_id = device_id;
|
|
entry->device_name = device_name;
|
|
entry->caps = 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, caps);
|
|
g_free (device_id);
|
|
g_free (device_name);
|
|
g_free (actual_device_id);
|
|
g_free (actual_device_name);
|
|
|
|
return 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
|
|
get_default_device (GstWasapi2Enumerator * self, EDataFlow flow,
|
|
IMMDevice ** device, IPropertyStore ** prop, gchar ** actual_device_id,
|
|
gchar ** actual_device_name)
|
|
{
|
|
auto priv = self->priv;
|
|
ComPtr < IMMDevice > rst_device;
|
|
ComPtr < IPropertyStore > rst_prop;
|
|
|
|
*actual_device_id = nullptr;
|
|
*actual_device_name = nullptr;
|
|
|
|
auto hr = priv->handle->GetDefaultAudioEndpoint (flow,
|
|
eConsole, &rst_device);
|
|
if (FAILED (hr))
|
|
return;
|
|
|
|
hr = rst_device->OpenPropertyStore (STGM_READ, &rst_prop);
|
|
if (FAILED (hr))
|
|
return;
|
|
|
|
LPWSTR wid = nullptr;
|
|
hr = rst_device->GetId (&wid);
|
|
if (!gst_wasapi2_result (hr))
|
|
return;
|
|
|
|
*actual_device_id = g_utf16_to_utf8 ((gunichar2 *) wid,
|
|
-1, nullptr, nullptr, nullptr);
|
|
CoTaskMemFree (wid);
|
|
|
|
PROPVARIANT var;
|
|
PropVariantInit (&var);
|
|
hr = rst_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);
|
|
}
|
|
|
|
*device = rst_device.Detach ();
|
|
*prop = rst_prop.Detach ();
|
|
return;
|
|
}
|
|
|
|
static gboolean
|
|
gst_wasapi2_enumerator_execute (GstWasapi2Enumerator * self,
|
|
IMMDeviceCollection * collection, gboolean ignore_error)
|
|
{
|
|
auto priv = self->priv;
|
|
|
|
GST_DEBUG_OBJECT (self, "Start enumerate");
|
|
|
|
UINT count = 0;
|
|
auto hr = collection->GetCount (&count);
|
|
if (!gst_wasapi2_result (hr) || count == 0)
|
|
return TRUE;
|
|
|
|
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);
|
|
|
|
ComPtr < IMMDevice > default_capture_device;
|
|
ComPtr < IPropertyStore > default_capture_prop;
|
|
gchar *default_capture_device_id = nullptr;
|
|
gchar *default_capture_device_name = nullptr;
|
|
|
|
ComPtr < IMMDevice > default_render_device;
|
|
ComPtr < IPropertyStore > default_render_prop;
|
|
gchar *default_render_device_id = nullptr;
|
|
gchar *default_render_device_name = nullptr;
|
|
|
|
get_default_device (self, eCapture, &default_capture_device,
|
|
&default_capture_prop,
|
|
&default_capture_device_id, &default_capture_device_name);
|
|
get_default_device (self, eRender, &default_render_device,
|
|
&default_render_prop,
|
|
&default_render_device_id, &default_render_device_name);
|
|
|
|
if (priv->capture_activator && !default_capture_client &&
|
|
default_capture_device) {
|
|
default_capture_device->Activate (__uuidof (IAudioClient), CLSCTX_ALL,
|
|
nullptr, &default_capture_client);
|
|
}
|
|
|
|
if (priv->render_activator && !default_render_client && default_render_device) {
|
|
default_render_device->Activate (__uuidof (IAudioClient), CLSCTX_ALL,
|
|
nullptr, &default_render_client);
|
|
}
|
|
|
|
if (default_capture_client) {
|
|
GstWasapi2DeviceProps props;
|
|
props.form_factor = UnknownFormFactor;
|
|
props.enumerator_name = "UNKNOWN";
|
|
|
|
if (default_capture_prop)
|
|
gst_wasapi2_enumerator_probe_props (default_capture_prop.Get (), &props);
|
|
|
|
g_ptr_array_set_size (priv->endpoint_formats, 0);
|
|
gst_wasapi2_get_shared_mode_formats (default_capture_client.Get (),
|
|
priv->endpoint_formats);
|
|
auto caps = gst_wasapi2_wfx_list_to_caps (priv->endpoint_formats);
|
|
g_ptr_array_set_size (priv->endpoint_formats, 0);
|
|
|
|
if (caps) {
|
|
auto entry = gst_wasapi2_enumerator_build_entry (self,
|
|
caps, eCapture, TRUE,
|
|
g_strdup (gst_wasapi2_get_default_device_id (eCapture)),
|
|
g_strdup ("Default Audio Capture Device"),
|
|
g_strdup (default_capture_device_id),
|
|
g_strdup (default_capture_device_name), &props);
|
|
|
|
if (entry)
|
|
g_ptr_array_add (priv->device_list, entry);
|
|
}
|
|
}
|
|
|
|
if (default_render_client) {
|
|
GstWasapi2DeviceProps props;
|
|
props.form_factor = UnknownFormFactor;
|
|
props.enumerator_name = "UNKNOWN";
|
|
|
|
if (default_render_prop)
|
|
gst_wasapi2_enumerator_probe_props (default_render_prop.Get (), &props);
|
|
|
|
g_ptr_array_set_size (priv->endpoint_formats, 0);
|
|
gst_wasapi2_get_shared_mode_formats (default_render_client.Get (),
|
|
priv->endpoint_formats);
|
|
auto caps = gst_wasapi2_wfx_list_to_caps (priv->endpoint_formats);
|
|
g_ptr_array_set_size (priv->endpoint_formats, 0);
|
|
|
|
if (caps) {
|
|
auto entry = gst_wasapi2_enumerator_build_entry (self,
|
|
caps, eRender, TRUE,
|
|
g_strdup (gst_wasapi2_get_default_device_id (eRender)),
|
|
g_strdup ("Default Audio Render Device"),
|
|
g_strdup (default_render_device_id),
|
|
g_strdup (default_render_device_name), &props);
|
|
|
|
if (entry)
|
|
g_ptr_array_add (priv->device_list, entry);
|
|
}
|
|
}
|
|
|
|
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)) {
|
|
/* Requested active devices via DEVICE_STATE_ACTIVE but activate fail here.
|
|
* That means devices were changed while we were enumerating.
|
|
* Need retry here */
|
|
GST_DEBUG_OBJECT (self, "Couldn't activate device %s (%s)",
|
|
device_id, desc);
|
|
g_free (device_id);
|
|
g_free (desc);
|
|
|
|
if (!ignore_error && hr == AUDCLNT_E_DEVICE_INVALIDATED)
|
|
return FALSE;
|
|
}
|
|
|
|
gst_wasapi2_enumerator_probe_props (prop.Get (), &props);
|
|
|
|
g_ptr_array_set_size (priv->endpoint_formats, 0);
|
|
gst_wasapi2_get_shared_mode_formats (default_render_client.Get (),
|
|
priv->endpoint_formats);
|
|
auto caps = gst_wasapi2_wfx_list_to_caps (priv->endpoint_formats);
|
|
g_ptr_array_set_size (priv->endpoint_formats, 0);
|
|
|
|
if (caps) {
|
|
auto entry = gst_wasapi2_enumerator_build_entry (self, caps, flow,
|
|
FALSE, device_id, desc, nullptr, nullptr, &props);
|
|
if (entry) {
|
|
g_ptr_array_set_size (priv->endpoint_formats, 0);
|
|
gst_wasapi2_get_exclusive_mode_formats (client.Get (),
|
|
prop.Get (), priv->endpoint_formats);
|
|
auto exclusive_caps =
|
|
gst_wasapi2_wfx_list_to_caps (priv->endpoint_formats);
|
|
g_ptr_array_set_size (priv->endpoint_formats, 0);
|
|
|
|
entry->exclusive_caps = exclusive_caps;
|
|
g_ptr_array_add (priv->device_list, entry);
|
|
}
|
|
}
|
|
}
|
|
|
|
g_free (default_capture_device_id);
|
|
g_free (default_capture_device_name);
|
|
g_free (default_render_device_id);
|
|
g_free (default_render_device_name);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
gst_wasapi2_enumerator_enumerate_internal (EnumerateData * data)
|
|
{
|
|
auto self = data->self;
|
|
auto priv = self->priv;
|
|
/* Upto 3 times retry */
|
|
const guint num_retry = 5;
|
|
|
|
for (guint i = 0; i < num_retry; i++) {
|
|
ComPtr < IMMDeviceCollection > collection;
|
|
gboolean is_last = FALSE;
|
|
|
|
if (i + 1 == num_retry)
|
|
is_last = TRUE;
|
|
|
|
g_ptr_array_set_size (priv->device_list, 0);
|
|
|
|
auto hr = priv->handle->EnumAudioEndpoints (eAll, DEVICE_STATE_ACTIVE,
|
|
&collection);
|
|
if (!gst_wasapi2_result (hr)) {
|
|
SetEvent (data->event);
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
|
|
if (gst_wasapi2_enumerator_execute (self, collection.Get (), is_last))
|
|
break;
|
|
|
|
if (!is_last) {
|
|
GST_DEBUG_OBJECT (self, "Sleep for retrying");
|
|
Sleep (50);
|
|
}
|
|
}
|
|
|
|
while (priv->device_list->len > 0) {
|
|
g_ptr_array_add (data->device_list,
|
|
g_ptr_array_steal_index (priv->device_list, 0));
|
|
}
|
|
|
|
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";
|
|
}
|
|
}
|