wasapi2: Implement IMMDeviceEnumerator based enumerator

... and merge wasapi2{capture,render}deviceprovider into single
wasapi2deviceprovider since we can enumerate input/output audio
devices at once using IMMDeviceEnumerator

This is a preparation for complete porting to Win32 API

Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/9307>
This commit is contained in:
Seungha Yang 2025-06-27 21:36:53 +09:00 committed by GStreamer Marge Bot
parent ea08b70946
commit ba41200c96
12 changed files with 1433 additions and 626 deletions

View File

@ -0,0 +1,151 @@
/* 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.
*/
#include "gstwasapi2activator.h"
#include <objidl.h>
GST_DEBUG_CATEGORY_EXTERN (gst_wasapi2_debug);
#define GST_CAT_DEFAULT gst_wasapi2_debug
/* *INDENT-OFF* */
using namespace Microsoft::WRL;
void
Wasapi2ActivationHandler::CreateInstance (Wasapi2ActivationHandler ** handler,
const wchar_t * device_id,
const AUDIOCLIENT_ACTIVATION_PARAMS * params)
{
auto self = new Wasapi2ActivationHandler ();
self->device_id_ = device_id;
if (params) {
self->params_ = *params;
self->prop_.vt = VT_BLOB;
self->prop_.blob.cbSize = sizeof (AUDIOCLIENT_ACTIVATION_PARAMS);
self->prop_.blob.pBlobData = (BYTE *) &self->params_;
self->have_params_ = true;
}
*handler = self;
}
STDMETHODIMP_ (ULONG)
Wasapi2ActivationHandler::AddRef (void)
{
return InterlockedIncrement (&refcount_);
}
STDMETHODIMP_ (ULONG)
Wasapi2ActivationHandler::Release (void)
{
ULONG ref_count;
ref_count = InterlockedDecrement (&refcount_);
if (ref_count == 0)
delete this;
return ref_count;
}
STDMETHODIMP
Wasapi2ActivationHandler::QueryInterface (REFIID riid, void ** object)
{
if (riid == __uuidof(IUnknown) || riid == __uuidof(IAgileObject)) {
*object = static_cast<IUnknown *>(static_cast<Wasapi2ActivationHandler*>(this));
} else if (riid == __uuidof(IActivateAudioInterfaceCompletionHandler)) {
*object = static_cast<IActivateAudioInterfaceCompletionHandler *>(
static_cast<Wasapi2ActivationHandler*>(this));
} else if (riid == IID_Wasapi2ActivationHandler) {
*object = this;
} else {
*object = nullptr;
return E_NOINTERFACE;
}
AddRef ();
return S_OK;
}
STDMETHODIMP
Wasapi2ActivationHandler::ActivateCompleted (IActivateAudioInterfaceAsyncOperation * op)
{
ComPtr<IUnknown> iface;
HRESULT hr = S_OK;
hr = op->GetActivateResult (&hr, &iface);
if (FAILED (hr)) {
GST_ERROR ("Couldn't get activate result, hr: 0x%x", (guint) hr);
activate_hr_ = hr;
SetEvent (event_);
return hr;
}
hr = iface.As (&client_);
activate_hr_ = hr;
GST_LOG ("Activation result 0x%x", (guint) hr);
SetEvent (event_);
return hr;
}
HRESULT
Wasapi2ActivationHandler::ActivateAsync ()
{
ComPtr<IActivateAudioInterfaceAsyncOperation> async_op;
auto hr = ActivateAudioInterfaceAsync (device_id_.c_str (),
__uuidof (IAudioClient), have_params_ ? &prop_ : nullptr,
this, &async_op);
if (!gst_wasapi2_result (hr)) {
activate_hr_ = hr;
SetEvent (event_);
}
return hr;
}
HRESULT
Wasapi2ActivationHandler::GetClient (IAudioClient ** client, DWORD timeout)
{
WaitForSingleObject (event_, timeout);
auto hr = activate_hr_.load ();
if (!gst_wasapi2_result (hr))
return hr;
if (!client)
return S_OK;
*client = client_.Get ();
(*client)->AddRef ();
return S_OK;
}
Wasapi2ActivationHandler::Wasapi2ActivationHandler ()
{
event_ = CreateEvent (nullptr, TRUE, FALSE, nullptr);
}
Wasapi2ActivationHandler::~Wasapi2ActivationHandler ()
{
CloseHandle (event_);
}
/* *INDENT-ON* */

View File

@ -0,0 +1,94 @@
/* 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.
*/
#pragma once
#include <gst/gst.h>
#include "gstwasapi2util.h"
#include <wrl.h>
#include <atomic>
#include <string>
/* Copy of audioclientactivationparams.h since those types are defined only for
* NTDDI_VERSION >= NTDDI_WIN10_FE */
#define VIRTUAL_AUDIO_DEVICE_PROCESS_LOOPBACK L"VAD\\Process_Loopback"
typedef enum
{
PROCESS_LOOPBACK_MODE_INCLUDE_TARGET_PROCESS_TREE = 0,
PROCESS_LOOPBACK_MODE_EXCLUDE_TARGET_PROCESS_TREE = 1
} PROCESS_LOOPBACK_MODE;
typedef struct
{
DWORD TargetProcessId;
PROCESS_LOOPBACK_MODE ProcessLoopbackMode;
} AUDIOCLIENT_PROCESS_LOOPBACK_PARAMS;
typedef enum
{
AUDIOCLIENT_ACTIVATION_TYPE_DEFAULT = 0,
AUDIOCLIENT_ACTIVATION_TYPE_PROCESS_LOOPBACK = 1
} AUDIOCLIENT_ACTIVATION_TYPE;
typedef struct
{
AUDIOCLIENT_ACTIVATION_TYPE ActivationType;
union
{
AUDIOCLIENT_PROCESS_LOOPBACK_PARAMS ProcessLoopbackParams;
} DUMMYUNIONNAME;
} AUDIOCLIENT_ACTIVATION_PARAMS;
/* End of audioclientactivationparams.h */
DEFINE_GUID (IID_Wasapi2ActivationHandler, 0xaa7e8f85, 0x211e,
0x42cc, 0x8c, 0x86, 0x99, 0x83, 0x5b, 0xef, 0x54, 0x86);
class Wasapi2ActivationHandler :
public IActivateAudioInterfaceCompletionHandler
{
public:
static void CreateInstance (Wasapi2ActivationHandler ** handler,
const wchar_t * device_id,
const AUDIOCLIENT_ACTIVATION_PARAMS * params);
STDMETHODIMP_ (ULONG) AddRef (void);
STDMETHODIMP_ (ULONG) Release (void);
STDMETHODIMP QueryInterface (REFIID riid, void ** object);
STDMETHODIMP ActivateCompleted (IActivateAudioInterfaceAsyncOperation * op);
HRESULT ActivateAsync (void);
HRESULT GetClient (IAudioClient ** client, DWORD timeout);
private:
Wasapi2ActivationHandler ();
virtual ~Wasapi2ActivationHandler ();
private:
Microsoft::WRL::ComPtr<IAudioClient> client_;
std::atomic<HRESULT> activate_hr_ = { E_FAIL };
HANDLE event_;
PROPVARIANT prop_ = { };
AUDIOCLIENT_ACTIVATION_PARAMS params_ = { };
bool have_params_ = false;
std::wstring device_id_;
ULONG refcount_ = 1;
};

View File

@ -84,12 +84,9 @@ typedef struct
} GST_AUDIOCLIENT_ACTIVATION_PARAMS;
/* End of audioclientactivationparams.h */
G_BEGIN_DECLS
GST_DEBUG_CATEGORY_EXTERN (gst_wasapi2_client_debug);
#define GST_CAT_DEFAULT gst_wasapi2_client_debug
G_END_DECLS
/* *INDENT-ON* */
static void

View File

@ -1,601 +0,0 @@
/* GStreamer
* Copyright (C) 2018 Nirbheek Chauhan <nirbheek@centricular.com>
* Copyright (C) 2020 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 "gstwasapi2device.h"
#include "gstwasapi2client.h"
#include "gstwasapi2util.h"
#include <gst/winrt/gstwinrt.h>
GST_DEBUG_CATEGORY_EXTERN (gst_wasapi2_debug);
#define GST_CAT_DEFAULT gst_wasapi2_debug
enum
{
PROP_0,
PROP_DEVICE,
};
struct _GstWasapi2Device
{
GstDevice parent;
gchar *device_id;
const gchar *factory_name;
GstWasapi2ClientDeviceClass device_class;
};
G_DEFINE_TYPE (GstWasapi2Device, gst_wasapi2_device, GST_TYPE_DEVICE);
static void gst_wasapi2_device_get_property (GObject * object,
guint prop_id, GValue * value, GParamSpec * pspec);
static void gst_wasapi2_device_set_property (GObject * object,
guint prop_id, const GValue * value, GParamSpec * pspec);
static void gst_wasapi2_device_finalize (GObject * object);
static GstElement *gst_wasapi2_device_create_element (GstDevice * device,
const gchar * name);
static void
gst_wasapi2_device_class_init (GstWasapi2DeviceClass * klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
GstDeviceClass *dev_class = GST_DEVICE_CLASS (klass);
dev_class->create_element = gst_wasapi2_device_create_element;
gobject_class->get_property = gst_wasapi2_device_get_property;
gobject_class->set_property = gst_wasapi2_device_set_property;
gobject_class->finalize = gst_wasapi2_device_finalize;
g_object_class_install_property (gobject_class, PROP_DEVICE,
g_param_spec_string ("device", "Device",
"Audio device ID as provided by "
"Windows.Devices.Enumeration.DeviceInformation.Id", NULL,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
}
static void
gst_wasapi2_device_init (GstWasapi2Device * self)
{
}
static void
gst_wasapi2_device_finalize (GObject * object)
{
GstWasapi2Device *self = GST_WASAPI2_DEVICE (object);
g_free (self->device_id);
G_OBJECT_CLASS (gst_wasapi2_device_parent_class)->finalize (object);
}
static GstElement *
gst_wasapi2_device_create_element (GstDevice * device, const gchar * name)
{
GstWasapi2Device *self = GST_WASAPI2_DEVICE (device);
GstElement *elem;
elem = gst_element_factory_make (self->factory_name, name);
g_object_set (elem, "device", self->device_id, NULL);
if (self->device_class == GST_WASAPI2_CLIENT_DEVICE_CLASS_LOOPBACK_CAPTURE)
g_object_set (elem, "loopback", TRUE, NULL);
return elem;
}
static void
gst_wasapi2_device_get_property (GObject * object, guint prop_id,
GValue * value, GParamSpec * pspec)
{
GstWasapi2Device *self = GST_WASAPI2_DEVICE (object);
switch (prop_id) {
case PROP_DEVICE:
g_value_set_string (value, self->device_id);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gst_wasapi2_device_set_property (GObject * object, guint prop_id,
const GValue * value, GParamSpec * pspec)
{
GstWasapi2Device *self = GST_WASAPI2_DEVICE (object);
switch (prop_id) {
case PROP_DEVICE:
/* G_PARAM_CONSTRUCT_ONLY */
self->device_id = g_value_dup_string (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
typedef struct _GstWasapi2DeviceProvider
{
GstDeviceProvider parent;
GstWinRTDeviceWatcher *watcher;
GMutex lock;
GCond cond;
gboolean enum_completed;
} GstWasapi2DeviceProvider;
typedef struct _GstWasapi2DeviceProviderClass
{
GstDeviceProviderClass parent_class;
GstWinRTDeviceClass winrt_device_class;
} GstWasapi2DeviceProviderClass;
static GstDeviceProviderClass *parent_class = NULL;
#define GST_WASAPI2_DEVICE_PROVIDER(object) \
((GstWasapi2DeviceProvider *) (object))
#define GST_WASAPI2_DEVICE_PROVIDER_GET_CLASS(object) \
(G_TYPE_INSTANCE_GET_CLASS ((object),G_TYPE_FROM_INSTANCE (object),GstWasapi2DeviceProviderClass))
static void gst_wasapi2_device_provider_dispose (GObject * object);
static void gst_wasapi2_device_provider_finalize (GObject * object);
static GList *gst_wasapi2_device_provider_probe (GstDeviceProvider * provider);
static gboolean
gst_wasapi2_device_provider_start (GstDeviceProvider * provider);
static void gst_wasapi2_device_provider_stop (GstDeviceProvider * provider);
static void
gst_wasapi2_device_provider_device_added (GstWinRTDeviceWatcher * watcher,
__x_ABI_CWindows_CDevices_CEnumeration_CIDeviceInformation * info,
gpointer user_data);
static void
gst_wasapi2_device_provider_device_updated (GstWinRTDeviceWatcher * watcher,
__x_ABI_CWindows_CDevices_CEnumeration_CIDeviceInformationUpdate *
info_update, gpointer user_data);
static void gst_wasapi2_device_provider_device_removed (GstWinRTDeviceWatcher *
watcher,
__x_ABI_CWindows_CDevices_CEnumeration_CIDeviceInformationUpdate *
info_update, gpointer user_data);
static void
gst_wasapi2_device_provider_device_enum_completed (GstWinRTDeviceWatcher *
watcher, gpointer user_data);
static void
gst_wasapi2_device_provider_class_init (GstWasapi2DeviceProviderClass * klass,
gpointer data)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
GstDeviceProviderClass *provider_class = GST_DEVICE_PROVIDER_CLASS (klass);
gobject_class->dispose = gst_wasapi2_device_provider_dispose;
gobject_class->finalize = gst_wasapi2_device_provider_finalize;
provider_class->probe = GST_DEBUG_FUNCPTR (gst_wasapi2_device_provider_probe);
provider_class->start = GST_DEBUG_FUNCPTR (gst_wasapi2_device_provider_start);
provider_class->stop = GST_DEBUG_FUNCPTR (gst_wasapi2_device_provider_stop);
parent_class = (GstDeviceProviderClass *) g_type_class_peek_parent (klass);
klass->winrt_device_class = (GstWinRTDeviceClass) GPOINTER_TO_INT (data);
if (klass->winrt_device_class == GST_WINRT_DEVICE_CLASS_AUDIO_CAPTURE) {
gst_device_provider_class_set_static_metadata (provider_class,
"WASAPI (Windows Audio Session API) Capture Device Provider",
"Source/Audio", "List WASAPI source devices",
"Nirbheek Chauhan <nirbheek@centricular.com>, "
"Seungha Yang <seungha@centricular.com>");
} else {
gst_device_provider_class_set_static_metadata (provider_class,
"WASAPI (Windows Audio Session API) Render and Loopback Capture Device Provider",
"Source/Sink/Audio", "List WASAPI loop back source and sink devices",
"Nirbheek Chauhan <nirbheek@centricular.com>, "
"Seungha Yang <seungha@centricular.com>");
}
}
static void
gst_wasapi2_device_provider_init (GstWasapi2DeviceProvider * self)
{
GstWasapi2DeviceProviderClass *klass =
GST_WASAPI2_DEVICE_PROVIDER_GET_CLASS (self);
GstWinRTDeviceWatcherCallbacks callbacks;
g_mutex_init (&self->lock);
g_cond_init (&self->cond);
callbacks.added = gst_wasapi2_device_provider_device_added;
callbacks.updated = gst_wasapi2_device_provider_device_updated;
callbacks.removed = gst_wasapi2_device_provider_device_removed;
callbacks.enumeration_completed =
gst_wasapi2_device_provider_device_enum_completed;
self->watcher =
gst_winrt_device_watcher_new (klass->winrt_device_class,
&callbacks, self);
}
static void
gst_wasapi2_device_provider_dispose (GObject * object)
{
GstWasapi2DeviceProvider *self = GST_WASAPI2_DEVICE_PROVIDER (object);
gst_clear_object (&self->watcher);
G_OBJECT_CLASS (parent_class)->dispose (object);
}
static void
gst_wasapi2_device_provider_finalize (GObject * object)
{
GstWasapi2DeviceProvider *self = GST_WASAPI2_DEVICE_PROVIDER (object);
g_mutex_clear (&self->lock);
g_cond_clear (&self->cond);
G_OBJECT_CLASS (parent_class)->finalize (object);
}
static void
gst_wasapi2_device_provider_probe_internal (GstWasapi2DeviceProvider * self,
GstWasapi2ClientDeviceClass client_class, GList ** devices)
{
gint i;
const gchar *device_class, *factory_name;
if (client_class == GST_WASAPI2_CLIENT_DEVICE_CLASS_RENDER) {
device_class = "Audio/Sink";
factory_name = "wasapi2sink";
} else {
device_class = "Audio/Source";
factory_name = "wasapi2src";
}
for (i = 0;; i++) {
GstWasapi2Client *client = NULL;
GstDevice *device;
GstStructure *props = NULL;
GstCaps *caps = NULL;
gchar *device_id = NULL;
gchar *device_name = NULL;
GstWasapi2Result result;
result = gst_wasapi2_client_enumerate (client_class, i, &client);
if (result == GST_WASAPI2_DEVICE_NOT_FOUND)
return;
else if (result == GST_WASAPI2_ACTIVATION_FAILED)
continue;
g_assert (client);
caps = gst_wasapi2_client_get_caps (client);
if (!caps) {
GST_WARNING_OBJECT (self, "Couldn't get caps from client %d", i);
/* this might be a case where device activation is not finished yet */
caps = gst_caps_from_string (GST_WASAPI2_STATIC_CAPS);
}
g_object_get (client,
"device", &device_id, "device-name", &device_name, NULL);
if (!device_id) {
GST_WARNING_OBJECT (self, "Couldn't get device name from client %d", i);
goto next;
}
if (!device_name) {
GST_WARNING_OBJECT (self, "Couldn't get device name from client %d", i);
goto next;
}
props = gst_structure_new ("wasapi2-proplist",
"device.api", G_TYPE_STRING, "wasapi2",
"device.id", G_TYPE_STRING, device_id,
"device.default", G_TYPE_BOOLEAN, i == 0,
"wasapi2.device.description", G_TYPE_STRING, device_name, NULL);
switch (client_class) {
case GST_WASAPI2_CLIENT_DEVICE_CLASS_CAPTURE:
gst_structure_set (props,
"wasapi2.device.loopback", G_TYPE_BOOLEAN, FALSE, NULL);
break;
case GST_WASAPI2_CLIENT_DEVICE_CLASS_LOOPBACK_CAPTURE:
gst_structure_set (props,
"wasapi2.device.loopback", G_TYPE_BOOLEAN, TRUE, NULL);
break;
default:
break;
}
device = g_object_new (GST_TYPE_WASAPI2_DEVICE, "device", device_id,
"display-name", device_name, "caps", caps,
"device-class", device_class, "properties", props, NULL);
GST_WASAPI2_DEVICE (device)->factory_name = factory_name;
GST_WASAPI2_DEVICE (device)->device_class = client_class;
*devices = g_list_append (*devices, device);
next:
gst_clear_object (&client);
gst_clear_caps (&caps);
g_free (device_id);
g_free (device_name);
if (props)
gst_structure_free (props);
}
return;
}
static GList *
gst_wasapi2_device_provider_probe (GstDeviceProvider * provider)
{
GstWasapi2DeviceProvider *self = GST_WASAPI2_DEVICE_PROVIDER (provider);
GstWasapi2DeviceProviderClass *klass =
GST_WASAPI2_DEVICE_PROVIDER_GET_CLASS (self);
GList *devices = NULL;
if (klass->winrt_device_class == GST_WINRT_DEVICE_CLASS_AUDIO_CAPTURE) {
gst_wasapi2_device_provider_probe_internal (self,
GST_WASAPI2_CLIENT_DEVICE_CLASS_CAPTURE, &devices);
} else {
gst_wasapi2_device_provider_probe_internal (self,
GST_WASAPI2_CLIENT_DEVICE_CLASS_LOOPBACK_CAPTURE, &devices);
gst_wasapi2_device_provider_probe_internal (self,
GST_WASAPI2_CLIENT_DEVICE_CLASS_RENDER, &devices);
}
return devices;
}
static gboolean
gst_wasapi2_device_provider_start (GstDeviceProvider * provider)
{
GstWasapi2DeviceProvider *self = GST_WASAPI2_DEVICE_PROVIDER (provider);
GList *devices = NULL;
GList *iter;
if (!self->watcher) {
GST_ERROR_OBJECT (self, "DeviceWatcher object wasn't configured");
return FALSE;
}
self->enum_completed = FALSE;
if (!gst_winrt_device_watcher_start (self->watcher))
return FALSE;
/* Wait for initial enumeration to be completed */
g_mutex_lock (&self->lock);
while (!self->enum_completed)
g_cond_wait (&self->cond, &self->lock);
devices = gst_wasapi2_device_provider_probe (provider);
if (devices) {
for (iter = devices; iter; iter = g_list_next (iter)) {
gst_device_provider_device_add (provider, GST_DEVICE (iter->data));
}
g_list_free (devices);
}
g_mutex_unlock (&self->lock);
return TRUE;
}
static void
gst_wasapi2_device_provider_stop (GstDeviceProvider * provider)
{
GstWasapi2DeviceProvider *self = GST_WASAPI2_DEVICE_PROVIDER (provider);
if (self->watcher)
gst_winrt_device_watcher_stop (self->watcher);
}
static gboolean
gst_wasapi2_device_is_in_list (GList * list, GstDevice * device)
{
GList *iter;
GstStructure *s;
const gchar *device_id;
gboolean found = FALSE;
s = gst_device_get_properties (device);
g_assert (s);
device_id = gst_structure_get_string (s, "device.id");
g_assert (device_id);
for (iter = list; iter; iter = g_list_next (iter)) {
GstStructure *other_s;
const gchar *other_id;
other_s = gst_device_get_properties (GST_DEVICE (iter->data));
g_assert (other_s);
other_id = gst_structure_get_string (other_s, "device.id");
g_assert (other_id);
if (g_ascii_strcasecmp (device_id, other_id) == 0) {
found = TRUE;
}
gst_structure_free (other_s);
if (found)
break;
}
gst_structure_free (s);
return found;
}
static void
gst_wasapi2_device_provider_update_devices (GstWasapi2DeviceProvider * self)
{
GstDeviceProvider *provider = GST_DEVICE_PROVIDER_CAST (self);
GList *prev_devices = NULL;
GList *new_devices = NULL;
GList *to_add = NULL;
GList *to_remove = NULL;
GList *iter;
GST_OBJECT_LOCK (self);
prev_devices = g_list_copy_deep (provider->devices,
(GCopyFunc) gst_object_ref, NULL);
GST_OBJECT_UNLOCK (self);
new_devices = gst_wasapi2_device_provider_probe (provider);
/* Ownership of GstDevice for gst_device_provider_device_add()
* and gst_device_provider_device_remove() is a bit complicated.
* Remove floating reference here for things to be clear */
for (iter = new_devices; iter; iter = g_list_next (iter))
gst_object_ref_sink (iter->data);
/* Check newly added devices */
for (iter = new_devices; iter; iter = g_list_next (iter)) {
if (!gst_wasapi2_device_is_in_list (prev_devices, GST_DEVICE (iter->data))) {
to_add = g_list_prepend (to_add, gst_object_ref (iter->data));
}
}
/* Check removed device */
for (iter = prev_devices; iter; iter = g_list_next (iter)) {
if (!gst_wasapi2_device_is_in_list (new_devices, GST_DEVICE (iter->data))) {
to_remove = g_list_prepend (to_remove, gst_object_ref (iter->data));
}
}
for (iter = to_remove; iter; iter = g_list_next (iter))
gst_device_provider_device_remove (provider, GST_DEVICE (iter->data));
for (iter = to_add; iter; iter = g_list_next (iter))
gst_device_provider_device_add (provider, GST_DEVICE (iter->data));
if (prev_devices)
g_list_free_full (prev_devices, (GDestroyNotify) gst_object_unref);
if (to_add)
g_list_free_full (to_add, (GDestroyNotify) gst_object_unref);
if (to_remove)
g_list_free_full (to_remove, (GDestroyNotify) gst_object_unref);
}
static void
gst_wasapi2_device_provider_device_added (GstWinRTDeviceWatcher * watcher,
__x_ABI_CWindows_CDevices_CEnumeration_CIDeviceInformation * info,
gpointer user_data)
{
GstWasapi2DeviceProvider *self = GST_WASAPI2_DEVICE_PROVIDER (user_data);
if (self->enum_completed)
gst_wasapi2_device_provider_update_devices (self);
}
static void
gst_wasapi2_device_provider_device_removed (GstWinRTDeviceWatcher * watcher,
__x_ABI_CWindows_CDevices_CEnumeration_CIDeviceInformationUpdate *
info_update, gpointer user_data)
{
GstWasapi2DeviceProvider *self = GST_WASAPI2_DEVICE_PROVIDER (user_data);
if (self->enum_completed)
gst_wasapi2_device_provider_update_devices (self);
}
static void
gst_wasapi2_device_provider_device_updated (GstWinRTDeviceWatcher * watcher,
__x_ABI_CWindows_CDevices_CEnumeration_CIDeviceInformationUpdate * info,
gpointer user_data)
{
GstWasapi2DeviceProvider *self = GST_WASAPI2_DEVICE_PROVIDER (user_data);
gst_wasapi2_device_provider_update_devices (self);
}
static void
gst_wasapi2_device_provider_device_enum_completed (GstWinRTDeviceWatcher *
watcher, gpointer user_data)
{
GstWasapi2DeviceProvider *self = GST_WASAPI2_DEVICE_PROVIDER (user_data);
g_mutex_lock (&self->lock);
GST_DEBUG_OBJECT (self, "Enumeration completed");
self->enum_completed = TRUE;
g_cond_signal (&self->cond);
g_mutex_unlock (&self->lock);
}
static void
gst_wasapi2_device_provider_register_internal (GstPlugin * plugin,
guint rank, GstWinRTDeviceClass winrt_device_class)
{
GType type;
const gchar *type_name = NULL;
const gchar *feature_name = NULL;
GTypeInfo type_info = {
sizeof (GstWasapi2DeviceProviderClass),
NULL,
NULL,
(GClassInitFunc) gst_wasapi2_device_provider_class_init,
NULL,
NULL,
sizeof (GstWasapi2DeviceProvider),
0,
(GInstanceInitFunc) gst_wasapi2_device_provider_init,
};
type_info.class_data = GINT_TO_POINTER (winrt_device_class);
if (winrt_device_class == GST_WINRT_DEVICE_CLASS_AUDIO_CAPTURE) {
type_name = "GstWasapi2CaputreDeviceProvider";
feature_name = "wasapi2capturedeviceprovider";
} else if (winrt_device_class == GST_WINRT_DEVICE_CLASS_AUDIO_RENDER) {
type_name = "GstWasapi2RenderDeviceProvider";
feature_name = "wasapi2renderdeviceprovider";
} else {
g_assert_not_reached ();
return;
}
type = g_type_register_static (GST_TYPE_DEVICE_PROVIDER,
type_name, &type_info, (GTypeFlags) 0);
if (!gst_device_provider_register (plugin, feature_name, rank, type))
GST_WARNING ("Failed to register provider '%s'", type_name);
}
void
gst_wasapi2_device_provider_register (GstPlugin * plugin, guint rank)
{
gst_wasapi2_device_provider_register_internal (plugin, rank,
GST_WINRT_DEVICE_CLASS_AUDIO_CAPTURE);
gst_wasapi2_device_provider_register_internal (plugin, rank,
GST_WINRT_DEVICE_CLASS_AUDIO_RENDER);
}

View File

@ -0,0 +1,395 @@
/* GStreamer
* Copyright (C) 2018 Nirbheek Chauhan <nirbheek@centricular.com>
* Copyright (C) 2020 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 "gstwasapi2device.h"
#include "gstwasapi2util.h"
#include "gstwasapi2enumerator.h"
GST_DEBUG_CATEGORY_EXTERN (gst_wasapi2_debug);
#define GST_CAT_DEFAULT gst_wasapi2_debug
enum
{
PROP_0,
PROP_DEVICE,
};
struct _GstWasapi2Device
{
GstDevice parent;
gchar *device_id;
const gchar *factory_name;
GstWasapi2EndpointClass device_class;
};
G_DEFINE_TYPE (GstWasapi2Device, gst_wasapi2_device, GST_TYPE_DEVICE);
static void gst_wasapi2_device_get_property (GObject * object,
guint prop_id, GValue * value, GParamSpec * pspec);
static void gst_wasapi2_device_set_property (GObject * object,
guint prop_id, const GValue * value, GParamSpec * pspec);
static void gst_wasapi2_device_finalize (GObject * object);
static GstElement *gst_wasapi2_device_create_element (GstDevice * device,
const gchar * name);
static void
gst_wasapi2_device_class_init (GstWasapi2DeviceClass * klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
GstDeviceClass *dev_class = GST_DEVICE_CLASS (klass);
dev_class->create_element = gst_wasapi2_device_create_element;
gobject_class->get_property = gst_wasapi2_device_get_property;
gobject_class->set_property = gst_wasapi2_device_set_property;
gobject_class->finalize = gst_wasapi2_device_finalize;
g_object_class_install_property (gobject_class, PROP_DEVICE,
g_param_spec_string ("device", "Device",
"Audio device ID as provided by "
"Windows.Devices.Enumeration.DeviceInformation.Id", nullptr,
(GParamFlags) (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_STRINGS)));
}
static void
gst_wasapi2_device_init (GstWasapi2Device * self)
{
}
static void
gst_wasapi2_device_finalize (GObject * object)
{
GstWasapi2Device *self = GST_WASAPI2_DEVICE (object);
g_free (self->device_id);
G_OBJECT_CLASS (gst_wasapi2_device_parent_class)->finalize (object);
}
static GstElement *
gst_wasapi2_device_create_element (GstDevice * device, const gchar * name)
{
GstWasapi2Device *self = GST_WASAPI2_DEVICE (device);
GstElement *elem;
elem = gst_element_factory_make (self->factory_name, name);
g_object_set (elem, "device", self->device_id, nullptr);
if (self->device_class == GST_WASAPI2_ENDPOINT_CLASS_LOOPBACK_CAPTURE)
g_object_set (elem, "loopback", TRUE, nullptr);
return elem;
}
static void
gst_wasapi2_device_get_property (GObject * object, guint prop_id,
GValue * value, GParamSpec * pspec)
{
GstWasapi2Device *self = GST_WASAPI2_DEVICE (object);
switch (prop_id) {
case PROP_DEVICE:
g_value_set_string (value, self->device_id);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gst_wasapi2_device_set_property (GObject * object, guint prop_id,
const GValue * value, GParamSpec * pspec)
{
GstWasapi2Device *self = GST_WASAPI2_DEVICE (object);
switch (prop_id) {
case PROP_DEVICE:
/* G_PARAM_CONSTRUCT_ONLY */
self->device_id = g_value_dup_string (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
struct _GstWasapi2DeviceProvider
{
GstDeviceProvider parent;
GstWasapi2Enumerator *enumerator;
GPtrArray *device_list;
};
G_DEFINE_TYPE (GstWasapi2DeviceProvider, gst_wasapi2_device_provider,
GST_TYPE_DEVICE_PROVIDER);
static void gst_wasapi2_device_provider_dispose (GObject * object);
static GList *gst_wasapi2_device_provider_probe (GstDeviceProvider * provider);
static gboolean
gst_wasapi2_device_provider_start (GstDeviceProvider * provider);
static void gst_wasapi2_device_provider_stop (GstDeviceProvider * provider);
static void gst_wasapi2_device_provider_on_updated (GstWasapi2Enumerator *
object, GstWasapi2DeviceProvider * self);
static void
gst_wasapi2_device_provider_class_init (GstWasapi2DeviceProviderClass * klass)
{
auto gobject_class = G_OBJECT_CLASS (klass);
auto provider_class = GST_DEVICE_PROVIDER_CLASS (klass);
gobject_class->dispose = gst_wasapi2_device_provider_dispose;
provider_class->probe = GST_DEBUG_FUNCPTR (gst_wasapi2_device_provider_probe);
provider_class->start = GST_DEBUG_FUNCPTR (gst_wasapi2_device_provider_start);
provider_class->stop = GST_DEBUG_FUNCPTR (gst_wasapi2_device_provider_stop);
gst_device_provider_class_set_static_metadata (provider_class,
"WASAPI (Windows Audio Session API) Device Provider",
"Source/Sink/Audio", "List WASAPI source devices",
"Nirbheek Chauhan <nirbheek@centricular.com>, "
"Seungha Yang <seungha@centricular.com>");
}
static void
gst_wasapi2_device_provider_init (GstWasapi2DeviceProvider * self)
{
self->enumerator = gst_wasapi2_enumerator_new ();
g_signal_connect (self->enumerator, "updated",
G_CALLBACK (gst_wasapi2_device_provider_on_updated), self);
self->device_list = g_ptr_array_new_with_free_func ((GDestroyNotify)
gst_wasapi2_enumerator_entry_free);
}
static void
gst_wasapi2_device_provider_dispose (GObject * object)
{
auto self = GST_WASAPI2_DEVICE_PROVIDER (object);
gst_clear_object (&self->enumerator);
g_clear_pointer (&self->device_list, g_ptr_array_unref);
G_OBJECT_CLASS (gst_wasapi2_device_provider_parent_class)->dispose (object);
}
static GList *
gst_wasapi2_device_provider_probe (GstDeviceProvider * provider)
{
auto self = GST_WASAPI2_DEVICE_PROVIDER (provider);
GList *devices = nullptr;
g_ptr_array_set_size (self->device_list, 0);
gst_wasapi2_enumerator_enumerate_devices (self->enumerator,
self->device_list);
for (guint i = 0; i < self->device_list->len; i++) {
auto entry = (GstWasapi2EnumeratorEntry *)
g_ptr_array_index (self->device_list, i);
auto props = gst_structure_new ("wasapi2-proplist",
"device.api", G_TYPE_STRING, "wasapi2",
"device.id", G_TYPE_STRING, entry->device_id,
"device.default", G_TYPE_BOOLEAN, entry->is_default,
"wasapi2.device.description", G_TYPE_STRING, entry->device_name,
nullptr);
if (entry->flow == eCapture) {
gst_structure_set (props,
"wasapi2.device.loopback", G_TYPE_BOOLEAN, FALSE, nullptr);
auto device = (GstDevice *) g_object_new (GST_TYPE_WASAPI2_DEVICE,
"device", entry->device_id,
"display-name", entry->device_name, "caps", entry->caps,
"device-class", "Audio/Source", "properties", props, nullptr);
gst_structure_free (props);
GST_WASAPI2_DEVICE (device)->factory_name = "wasapi2src";
GST_WASAPI2_DEVICE (device)->device_class =
GST_WASAPI2_ENDPOINT_CLASS_CAPTURE;
devices = g_list_append (devices, device);
} else {
auto prop_copy = gst_structure_copy (props);
gst_structure_set (prop_copy,
"wasapi2.device.loopback", G_TYPE_BOOLEAN, TRUE, nullptr);
auto device = (GstDevice *) g_object_new (GST_TYPE_WASAPI2_DEVICE,
"device", entry->device_id,
"display-name", entry->device_name, "caps", entry->caps,
"device-class", "Audio/Sink", "properties", props, nullptr);
gst_structure_free (props);
GST_WASAPI2_DEVICE (device)->factory_name = "wasapi2sink";
GST_WASAPI2_DEVICE (device)->device_class =
GST_WASAPI2_ENDPOINT_CLASS_RENDER;
devices = g_list_append (devices, device);
device = (GstDevice *) g_object_new (GST_TYPE_WASAPI2_DEVICE,
"device", entry->device_id,
"display-name", entry->device_name, "caps", entry->caps,
"device-class", "Audio/Source", "properties", prop_copy, nullptr);
gst_structure_free (prop_copy);
GST_WASAPI2_DEVICE (device)->factory_name = "wasapi2src";
GST_WASAPI2_DEVICE (device)->device_class =
GST_WASAPI2_ENDPOINT_CLASS_LOOPBACK_CAPTURE;
devices = g_list_append (devices, device);
}
}
g_ptr_array_set_size (self->device_list, 0);
return devices;
}
static gboolean
gst_wasapi2_device_provider_start (GstDeviceProvider * provider)
{
auto self = GST_WASAPI2_DEVICE_PROVIDER (provider);
GList *devices = nullptr;
GList *iter;
if (!self->enumerator) {
GST_ERROR_OBJECT (self, "Enumerator object wasn't configured");
return FALSE;
}
devices = gst_wasapi2_device_provider_probe (provider);
if (devices) {
for (iter = devices; iter; iter = g_list_next (iter))
gst_device_provider_device_add (provider, GST_DEVICE (iter->data));
g_list_free (devices);
}
gst_wasapi2_enumerator_activate_notification (self->enumerator, TRUE);
return TRUE;
}
static void
gst_wasapi2_device_provider_stop (GstDeviceProvider * provider)
{
auto self = GST_WASAPI2_DEVICE_PROVIDER (provider);
if (self->enumerator)
gst_wasapi2_enumerator_activate_notification (self->enumerator, FALSE);
}
static gboolean
gst_wasapi2_device_is_in_list (GList * list, GstDevice * device)
{
GList *iter;
GstStructure *s;
gboolean found = FALSE;
s = gst_device_get_properties (device);
g_assert (s);
for (iter = list; iter; iter = g_list_next (iter)) {
GstStructure *other_s;
other_s = gst_device_get_properties (GST_DEVICE (iter->data));
g_assert (other_s);
found = gst_structure_is_equal (s, other_s);
gst_structure_free (other_s);
if (found)
break;
}
gst_structure_free (s);
return found;
}
static void
gst_wasapi2_device_provider_update_devices (GstWasapi2DeviceProvider * self)
{
GstDeviceProvider *provider = GST_DEVICE_PROVIDER_CAST (self);
GList *prev_devices = nullptr;
GList *new_devices = nullptr;
GList *to_add = nullptr;
GList *to_remove = nullptr;
GList *iter;
GST_OBJECT_LOCK (self);
prev_devices = g_list_copy_deep (provider->devices,
(GCopyFunc) gst_object_ref, nullptr);
GST_OBJECT_UNLOCK (self);
new_devices = gst_wasapi2_device_provider_probe (provider);
/* Ownership of GstDevice for gst_device_provider_device_add()
* and gst_device_provider_device_remove() is a bit complicated.
* Remove floating reference here for things to be clear */
for (iter = new_devices; iter; iter = g_list_next (iter))
gst_object_ref_sink (iter->data);
/* Check newly added devices */
for (iter = new_devices; iter; iter = g_list_next (iter)) {
if (!gst_wasapi2_device_is_in_list (prev_devices, GST_DEVICE (iter->data))) {
to_add = g_list_prepend (to_add, gst_object_ref (iter->data));
}
}
/* Check removed device */
for (iter = prev_devices; iter; iter = g_list_next (iter)) {
if (!gst_wasapi2_device_is_in_list (new_devices, GST_DEVICE (iter->data))) {
to_remove = g_list_prepend (to_remove, gst_object_ref (iter->data));
}
}
for (iter = to_remove; iter; iter = g_list_next (iter))
gst_device_provider_device_remove (provider, GST_DEVICE (iter->data));
for (iter = to_add; iter; iter = g_list_next (iter))
gst_device_provider_device_add (provider, GST_DEVICE (iter->data));
if (prev_devices)
g_list_free_full (prev_devices, (GDestroyNotify) gst_object_unref);
if (to_add)
g_list_free_full (to_add, (GDestroyNotify) gst_object_unref);
if (to_remove)
g_list_free_full (to_remove, (GDestroyNotify) gst_object_unref);
}
static void
gst_wasapi2_device_provider_on_updated (GstWasapi2Enumerator * object,
GstWasapi2DeviceProvider * self)
{
gst_wasapi2_device_provider_update_devices (self);
}

View File

@ -28,7 +28,9 @@ G_BEGIN_DECLS
G_DECLARE_FINAL_TYPE (GstWasapi2Device, gst_wasapi2_device,
GST, WASAPI2_DEVICE, GstDevice);
void gst_wasapi2_device_provider_register (GstPlugin * plugin, guint rank);
#define GST_TYPE_WASAPI2_DEVICE_PROVIDER (gst_wasapi2_device_provider_get_type())
G_DECLARE_FINAL_TYPE (GstWasapi2DeviceProvider, gst_wasapi2_device_provider,
GST, WASAPI2_DEVICE_PROVIDER, GstDeviceProvider);
G_END_DECLS

View File

@ -0,0 +1,620 @@
/* 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;
GST_DEBUG_CATEGORY_EXTERN (gst_wasapi2_debug);
#define GST_CAT_DEFAULT gst_wasapi2_debug
static GstStaticCaps template_caps = GST_STATIC_CAPS (GST_WASAPI2_STATIC_CAPS);
static void gst_wasapi2_on_device_updated (GstWasapi2Enumerator * object);
/* 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)
{
GST_TRACE ("%p, %d", this, (guint) ref_count_);
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;
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;
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;
gst_wasapi2_on_device_updated (object);
gst_object_unref (object);
return S_OK;
}
STDMETHODIMP
OnDefaultDeviceChanged (EDataFlow flow, ERole role, LPCWSTR default_device_id)
{
auto object = (GstWasapi2Enumerator *) g_weak_ref_get (&obj_);
if (!object)
return S_OK;
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)
{
g_free (entry->device_id);
g_free (entry->device_name);
gst_clear_caps (&entry->caps);
g_free (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, 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);
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);
return;
}
auto entry = g_new0 (GstWasapi2EnumeratorEntry, 1);
entry->device_id = device_id;
entry->device_name = device_name;
entry->caps = supported_caps;
entry->flow = flow;
entry->is_default = is_default;
GST_LOG_OBJECT (self, "Adding entry %s (%s), flow %d, caps %" GST_PTR_FORMAT,
device_id, device_name, flow, supported_caps);
g_ptr_array_add (device_list, entry);
}
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);
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) {
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"), data->device_list);
}
if (default_render_client) {
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"), data->device_list);
}
for (UINT i = 0; i < count; i++) {
ComPtr < IMMDevice > device;
ComPtr < IMMEndpoint > endpoint;
EDataFlow flow;
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)) {
g_free (device_id);
g_free (desc);
continue;
}
gst_wasapi2_enumerator_add_entry (self, client.Get (), scaps, flow, FALSE,
device_id, desc, data->device_list);
}
gst_caps_unref (scaps);
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);
}

View File

@ -0,0 +1,51 @@
/* 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.
*/
#pragma once
#include <gst/gst.h>
#include "gstwasapi2util.h"
G_BEGIN_DECLS
#define GST_TYPE_WASAPI2_ENUMERATOR (gst_wasapi2_enumerator_get_type ())
G_DECLARE_FINAL_TYPE (GstWasapi2Enumerator, gst_wasapi2_enumerator,
GST, WASAPI2_ENUMERATOR, GstObject);
typedef struct _GstWasapi2EnumeratorEntry
{
gchar *device_id;
gchar *device_name;
gboolean is_default;
GstCaps *caps;
EDataFlow flow;
} GstWasapi2EnumeratorEntry;
GstWasapi2Enumerator * gst_wasapi2_enumerator_new (void);
void gst_wasapi2_enumerator_activate_notification (GstWasapi2Enumerator * object,
gboolean active);
void gst_wasapi2_enumerator_entry_free (GstWasapi2EnumeratorEntry * entry);
void gst_wasapi2_enumerator_enumerate_devices (GstWasapi2Enumerator * object,
GPtrArray * entry);
G_END_DECLS

View File

@ -27,6 +27,8 @@
#include <audioclient.h>
#include <mmdeviceapi.h>
#include <winternl.h>
#include <mutex>
#include <string.h>
GST_DEBUG_CATEGORY_EXTERN (gst_wasapi2_debug);
#define GST_CAT_DEFAULT gst_wasapi2_debug
@ -320,7 +322,7 @@ gst_wasapi2_util_waveformatex_to_channel_mask (WAVEFORMATEX * format,
/* Too many channels, have to assume that they are all non-positional */
if (nChannels > G_N_ELEMENTS (wasapi_to_gst_pos)) {
GST_INFO ("Got too many (%i) channels, assuming non-positional", nChannels);
GST_LOG ("Got too many (%i) channels, assuming non-positional", nChannels);
goto out;
}
@ -344,7 +346,7 @@ gst_wasapi2_util_waveformatex_to_channel_mask (WAVEFORMATEX * format,
/* XXX: Warn if some channel masks couldn't be mapped? */
GST_DEBUG ("Converted WASAPI mask 0x%" G_GINT64_MODIFIER "x -> 0x%"
GST_TRACE ("Converted WASAPI mask 0x%" G_GINT64_MODIFIER "x -> 0x%"
G_GINT64_MODIFIER "x", (guint64) dwChannelMask, (guint64) mask);
out:
@ -377,10 +379,10 @@ gst_wasapi2_util_waveformatex_to_audio_format (WAVEFORMATEX * format)
case WAVE_FORMAT_EXTENSIBLE:
{
WAVEFORMATEXTENSIBLE *ex = (WAVEFORMATEXTENSIBLE *) format;
if (IsEqualGUID (&ex->SubFormat, &KSDATAFORMAT_SUBTYPE_PCM)) {
if (IsEqualGUID (ex->SubFormat, KSDATAFORMAT_SUBTYPE_PCM)) {
fmt = gst_audio_format_build_integer (TRUE, G_LITTLE_ENDIAN,
format->wBitsPerSample, ex->Samples.wValidBitsPerSample);
} else if (IsEqualGUID (&ex->SubFormat, &KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)) {
} else if (IsEqualGUID (ex->SubFormat, KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)) {
if (format->wBitsPerSample == 32
&& ex->Samples.wValidBitsPerSample == 32)
fmt = GST_AUDIO_FORMAT_F32LE;
@ -453,9 +455,8 @@ gst_wasapi2_can_automatic_stream_routing (void)
return TRUE;
#else
static gboolean ret = FALSE;
static gsize version_once = 0;
if (g_once_init_enter (&version_once)) {
GST_WASAPI2_CALL_ONCE_BEGIN {
OSVERSIONINFOEXW osverinfo;
typedef NTSTATUS (WINAPI fRtlGetVersion) (PRTL_OSVERSIONINFOEXW);
fRtlGetVersion *RtlGetVersion = NULL;
@ -481,11 +482,10 @@ gst_wasapi2_can_automatic_stream_routing (void)
if (hmodule)
FreeLibrary (hmodule);
g_once_init_leave (&version_once, 1);
}
GST_WASAPI2_CALL_ONCE_END;
GST_INFO ("Automatic stream routing support: %d", ret);
GST_TRACE ("Automatic stream routing support: %d", ret);
return ret;
#endif
@ -500,9 +500,8 @@ gst_wasapi2_can_process_loopback (void)
return FALSE;
#else
static gboolean ret = FALSE;
static gsize version_once = 0;
if (g_once_init_enter (&version_once)) {
GST_WASAPI2_CALL_ONCE_BEGIN {
OSVERSIONINFOEXW osverinfo;
typedef NTSTATUS (WINAPI fRtlGetVersion) (PRTL_OSVERSIONINFOEXW);
fRtlGetVersion *RtlGetVersion = NULL;
@ -535,9 +534,8 @@ gst_wasapi2_can_process_loopback (void)
if (hmodule)
FreeLibrary (hmodule);
g_once_init_leave (&version_once, 1);
}
GST_WASAPI2_CALL_ONCE_END;
GST_INFO ("Process loopback support: %d", ret);
@ -552,7 +550,7 @@ gst_wasapi2_get_default_mix_format (void)
/* virtual loopback device might not provide mix format. Create our default
* mix format */
format = CoTaskMemAlloc (sizeof (WAVEFORMATEX));
format = (WAVEFORMATEX *) CoTaskMemAlloc (sizeof (WAVEFORMATEX));
format->wFormatTag = WAVE_FORMAT_PCM;
format->nChannels = 2;
format->nSamplesPerSec = 44100;
@ -562,3 +560,46 @@ gst_wasapi2_get_default_mix_format (void)
return format;
}
const wchar_t *
gst_wasapi2_get_default_device_id_wide (EDataFlow flow)
{
static wchar_t *capture = nullptr;
static wchar_t *render = nullptr;
GST_WASAPI2_CALL_ONCE_BEGIN {
StringFromIID (DEVINTERFACE_AUDIO_CAPTURE, &capture);
StringFromIID (DEVINTERFACE_AUDIO_RENDER, &render);
} GST_WASAPI2_CALL_ONCE_END;
if (flow == eCapture)
return (const wchar_t *) capture;
return (const wchar_t *) render;
}
const char *
gst_wasapi2_get_default_device_id (EDataFlow flow)
{
static char *capture = nullptr;
static char *render = nullptr;
GST_WASAPI2_CALL_ONCE_BEGIN {
auto wstr = gst_wasapi2_get_default_device_id_wide (eCapture);
if (wstr) {
capture = g_utf16_to_utf8 ((const gunichar2 *) wstr,
-1, nullptr, nullptr, nullptr);
}
wstr = gst_wasapi2_get_default_device_id_wide (eRender);
if (wstr) {
render = g_utf16_to_utf8 ((const gunichar2 *) wstr,
-1, nullptr, nullptr, nullptr);
}
} GST_WASAPI2_CALL_ONCE_END;
if (flow == eCapture)
return (const char *) capture;
return (const char *) render;
}

View File

@ -26,6 +26,8 @@
#include <initguid.h>
#include <audioclient.h>
#include <endpointvolume.h>
#include <string.h>
#include <mmdeviceapi.h>
G_BEGIN_DECLS
@ -57,6 +59,42 @@ gboolean _gst_wasapi2_result (HRESULT hr,
_gst_wasapi2_result (result, NULL, __FILE__, GST_FUNCTION, __LINE__)
#endif
typedef enum
{
GST_WASAPI2_ENDPOINT_CLASS_CAPTURE = 0,
GST_WASAPI2_ENDPOINT_CLASS_RENDER,
GST_WASAPI2_ENDPOINT_CLASS_LOOPBACK_CAPTURE,
GST_WASAPI2_ENDPOINT_CLASS_INCLUDE_PROCESS_LOOPBACK_CAPTURE,
GST_WASAPI2_ENDPOINT_CLASS_EXCLUDE_PROCESS_LOOPBACK_CAPTURE,
} GstWasapi2EndpointClass;
static inline gboolean
gst_wasapi2_is_loopback_class (GstWasapi2EndpointClass device_class)
{
switch (device_class) {
case GST_WASAPI2_ENDPOINT_CLASS_LOOPBACK_CAPTURE:
return TRUE;
default:
break;
}
return FALSE;
}
static inline gboolean
gst_wasapi2_is_process_loopback_class (GstWasapi2EndpointClass device_class)
{
switch (device_class) {
case GST_WASAPI2_ENDPOINT_CLASS_INCLUDE_PROCESS_LOOPBACK_CAPTURE:
case GST_WASAPI2_ENDPOINT_CLASS_EXCLUDE_PROCESS_LOOPBACK_CAPTURE:
return TRUE;
default:
break;
}
return FALSE;
}
guint64 gst_wasapi2_util_waveformatex_to_channel_mask (WAVEFORMATEX * format,
GstAudioChannelPosition ** out_position);
@ -75,6 +113,20 @@ gboolean gst_wasapi2_can_process_loopback (void);
WAVEFORMATEX * gst_wasapi2_get_default_mix_format (void);
const wchar_t * gst_wasapi2_get_default_device_id_wide (EDataFlow flow);
const char * gst_wasapi2_get_default_device_id (EDataFlow flow);
G_END_DECLS
#ifdef __cplusplus
#include <mutex>
#define GST_WASAPI2_CALL_ONCE_BEGIN \
static std::once_flag __once_flag; \
std::call_once (__once_flag, [&]()
#define GST_WASAPI2_CALL_ONCE_END )
#endif
#endif /* __GST_WASAPI_UTIL_H__ */

View File

@ -1,11 +1,13 @@
wasapi2_sources = [
'gstwasapi2src.c',
'gstwasapi2sink.c',
'gstwasapi2util.c',
'gstwasapi2util.cpp',
'gstwasapi2client.cpp',
'gstwasapi2device.c',
'gstwasapi2device.cpp',
'gstwasapi2ringbuffer.cpp',
'plugin.c',
'gstwasapi2activator.cpp',
'gstwasapi2enumerator.cpp',
'plugin.cpp',
]
wasapi2_headers = [
@ -15,6 +17,7 @@ wasapi2_headers = [
'gstwasapi2src.h',
'gstwasapi2sink.h',
'gstwasapi2client.h',
'gstwasapi2object.h',
'AsyncOperations.h',
]
@ -163,6 +166,7 @@ gstwasapi2 = library('gstwasapi2',
cpp_args : gst_plugins_bad_args + extra_args + extra_cpp_args,
include_directories : [configinc],
dependencies : [gstaudio_dep, gstwinrt_dep] + wasapi2_dep,
override_options : ['cpp_std=c++14'],
install : true,
install_dir : plugins_install_dir)
plugins += [gstwasapi2]

View File

@ -41,7 +41,7 @@ plugin_deinit (gpointer data)
static gboolean
plugin_init (GstPlugin * plugin)
{
GstRank rank = GST_RANK_PRIMARY + 1;
guint rank = GST_RANK_PRIMARY + 1;
HRESULT hr;
/**
@ -52,7 +52,7 @@ plugin_init (GstPlugin * plugin)
hr = MFStartup (MF_VERSION, MFSTARTUP_NOSOCKET);
if (!gst_wasapi2_result (hr)) {
GST_WARNING ("MFStartup failure, hr: 0x%x", hr);
GST_WARNING ("MFStartup failure, hr: 0x%x", (guint) hr);
return TRUE;
}
@ -63,10 +63,11 @@ plugin_init (GstPlugin * plugin)
gst_element_register (plugin, "wasapi2sink", rank, GST_TYPE_WASAPI2_SINK);
gst_element_register (plugin, "wasapi2src", rank, GST_TYPE_WASAPI2_SRC);
gst_wasapi2_device_provider_register (plugin, rank);
gst_device_provider_register (plugin, "wasapi2deviceprovider", rank,
GST_TYPE_WASAPI2_DEVICE_PROVIDER);
g_object_set_data_full (G_OBJECT (plugin),
"plugin-wasapi2-shutdown", "shutdown-data",
"plugin-wasapi2-shutdown", (gpointer) "shutdown-data",
(GDestroyNotify) plugin_deinit);
return TRUE;