diff options
-rw-r--r-- | sys/wasapi/gstmmdeviceenumerator.cpp | 472 | ||||
-rw-r--r-- | sys/wasapi/gstmmdeviceenumerator.h | 69 | ||||
-rw-r--r-- | sys/wasapi/gstwasapidevice.c | 205 | ||||
-rw-r--r-- | sys/wasapi/gstwasapidevice.h | 2 | ||||
-rw-r--r-- | sys/wasapi/gstwasapisink.c | 12 | ||||
-rw-r--r-- | sys/wasapi/gstwasapisink.h | 2 | ||||
-rw-r--r-- | sys/wasapi/gstwasapisrc.c | 14 | ||||
-rw-r--r-- | sys/wasapi/gstwasapisrc.h | 2 | ||||
-rw-r--r-- | sys/wasapi/gstwasapiutil.c | 49 | ||||
-rw-r--r-- | sys/wasapi/gstwasapiutil.h | 8 | ||||
-rw-r--r-- | sys/wasapi/meson.build | 2 | ||||
-rw-r--r-- | tests/check/elements/wasapi.c | 205 |
12 files changed, 967 insertions, 75 deletions
diff --git a/sys/wasapi/gstmmdeviceenumerator.cpp b/sys/wasapi/gstmmdeviceenumerator.cpp new file mode 100644 index 000000000..1826b7fe1 --- /dev/null +++ b/sys/wasapi/gstmmdeviceenumerator.cpp @@ -0,0 +1,472 @@ +/* GStreamer + * Copyright (C) 2021 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 "gstmmdeviceenumerator.h" + +#ifndef INITGUID +#include <initguid.h> +#endif + +/* *INDENT-OFF* */ +G_BEGIN_DECLS + +GST_DEBUG_CATEGORY_EXTERN (gst_wasapi_debug); +#define GST_CAT_DEFAULT gst_wasapi_debug + +G_END_DECLS + +/* IMMNotificationClient implementation */ +class GstIMMNotificationClient : public IMMNotificationClient +{ +public: + static HRESULT + CreateInstance (GstMMDeviceEnumerator * enumerator, + const GstMMNotificationClientCallbacks * callbacks, + gpointer user_data, + IMMNotificationClient ** client) + { + GstIMMNotificationClient *self; + + self = new GstIMMNotificationClient (); + + self->callbacks_ = *callbacks; + self->user_data_ = user_data; + g_weak_ref_set (&self->enumerator_, enumerator); + + *client = (IMMNotificationClient *) self; + + return S_OK; + } + + /* 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) + { + GstMMDeviceEnumerator *listener; + HRESULT hr; + + if (!callbacks_.device_state_changed) + return S_OK; + + listener = (GstMMDeviceEnumerator *) g_weak_ref_get (&enumerator_); + if (!listener) + return S_OK; + + hr = callbacks_.device_state_changed (listener, device_id, new_state, + user_data_); + gst_object_unref (listener); + + return hr; + } + + STDMETHODIMP + OnDeviceAdded (LPCWSTR device_id) + { + GstMMDeviceEnumerator *listener; + HRESULT hr; + + if (!callbacks_.device_added) + return S_OK; + + listener = (GstMMDeviceEnumerator *) g_weak_ref_get (&enumerator_); + if (!listener) + return S_OK; + + hr = callbacks_.device_added (listener, device_id, user_data_); + gst_object_unref (listener); + + return hr; + } + + STDMETHODIMP + OnDeviceRemoved (LPCWSTR device_id) + { + GstMMDeviceEnumerator *listener; + HRESULT hr; + + if (!callbacks_.device_removed) + return S_OK; + + listener = (GstMMDeviceEnumerator *) g_weak_ref_get (&enumerator_); + if (!listener) + return S_OK; + + hr = callbacks_.device_removed (listener, device_id, user_data_); + gst_object_unref (listener); + + return hr; + } + + STDMETHODIMP + OnDefaultDeviceChanged (EDataFlow flow, ERole role, LPCWSTR default_device_id) + { + GstMMDeviceEnumerator *listener; + HRESULT hr; + + if (!callbacks_.default_device_changed) + return S_OK; + + listener = (GstMMDeviceEnumerator *) g_weak_ref_get (&enumerator_); + if (!listener) + return S_OK; + + hr = callbacks_.default_device_changed (listener, + flow, role, default_device_id, user_data_); + gst_object_unref (listener); + + return hr; + } + + STDMETHODIMP + OnPropertyValueChanged (LPCWSTR device_id, const PROPERTYKEY key) + { + GstMMDeviceEnumerator *listener; + HRESULT hr; + + if (!callbacks_.property_value_changed) + return S_OK; + + listener = (GstMMDeviceEnumerator *) g_weak_ref_get (&enumerator_); + if (!device_id) + return S_OK; + + hr = callbacks_.property_value_changed (listener, + device_id, key, user_data_); + gst_object_unref (listener); + + return hr; + } + +private: + GstIMMNotificationClient () + : ref_count_ (1) + { + g_weak_ref_init (&enumerator_, nullptr); + } + + virtual ~GstIMMNotificationClient () + { + g_weak_ref_clear (&enumerator_); + } + +private: + ULONG ref_count_; + GstMMNotificationClientCallbacks callbacks_; + gpointer user_data_; + GWeakRef enumerator_; +}; +/* *INDENT-ON* */ + +struct _GstMMDeviceEnumerator +{ + GstObject parent; + + IMMDeviceEnumerator *handle; + IMMNotificationClient *client; + + GMutex lock; + GCond cond; + + GThread *thread; + GMainContext *context; + GMainLoop *loop; + + gboolean running; +}; + +static void gst_mm_device_enumerator_constructed (GObject * object); +static void gst_mm_device_enumerator_finalize (GObject * object); + +static gpointer +gst_mm_device_enumerator_thread_func (GstMMDeviceEnumerator * self); + +#define gst_mm_device_enumerator_parent_class parent_class +G_DEFINE_TYPE (GstMMDeviceEnumerator, + gst_mm_device_enumerator, GST_TYPE_OBJECT); + +static void +gst_mm_device_enumerator_class_init (GstMMDeviceEnumeratorClass * klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->constructed = gst_mm_device_enumerator_constructed; + gobject_class->finalize = gst_mm_device_enumerator_finalize; +} + +static void +gst_mm_device_enumerator_init (GstMMDeviceEnumerator * self) +{ + g_mutex_init (&self->lock); + g_cond_init (&self->cond); + self->context = g_main_context_new (); + self->loop = g_main_loop_new (self->context, FALSE); +} + +static void +gst_mm_device_enumerator_constructed (GObject * object) +{ + GstMMDeviceEnumerator *self = GST_MM_DEVICE_ENUMERATOR (object); + + g_mutex_lock (&self->lock); + self->thread = g_thread_new ("GstMMDeviceEnumerator", + (GThreadFunc) gst_mm_device_enumerator_thread_func, self); + while (!g_main_loop_is_running (self->loop)) + g_cond_wait (&self->cond, &self->lock); + g_mutex_unlock (&self->lock); +} + +static void +gst_mm_device_enumerator_finalize (GObject * object) +{ + GstMMDeviceEnumerator *self = GST_MM_DEVICE_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); + + g_mutex_clear (&self->lock); + g_cond_clear (&self->cond); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static gboolean +loop_running_cb (GstMMDeviceEnumerator * self) +{ + g_mutex_lock (&self->lock); + g_cond_signal (&self->cond); + g_mutex_unlock (&self->lock); + + return G_SOURCE_REMOVE; +} + +static gpointer +gst_mm_device_enumerator_thread_func (GstMMDeviceEnumerator * self) +{ + GSource *idle_source; + IMMDeviceEnumerator *enumerator = nullptr; + HRESULT hr; + + CoInitializeEx (NULL, COINIT_MULTITHREADED); + g_main_context_push_thread_default (self->context); + + idle_source = g_idle_source_new (); + g_source_set_callback (idle_source, + (GSourceFunc) loop_running_cb, self, nullptr); + g_source_attach (idle_source, self->context); + g_source_unref (idle_source); + + hr = CoCreateInstance (__uuidof (MMDeviceEnumerator), + nullptr, CLSCTX_ALL, IID_PPV_ARGS (&enumerator)); + if (FAILED (hr)) { + GST_ERROR_OBJECT (self, "Failed to create IMMDeviceEnumerator instance"); + goto run_loop; + } + + self->handle = enumerator; + +run_loop: + GST_INFO_OBJECT (self, "Starting loop"); + g_main_loop_run (self->loop); + GST_INFO_OBJECT (self, "Stopped loop"); + + if (self->client && self->handle) { + self->handle->UnregisterEndpointNotificationCallback (self->client); + + self->client->Release (); + } + + if (self->handle) + self->handle->Release (); + + g_main_context_pop_thread_default (self->context); + CoUninitialize (); + + return nullptr; +} + +GstMMDeviceEnumerator * +gst_mm_device_enumerator_new (void) +{ + GstMMDeviceEnumerator *self; + + self = (GstMMDeviceEnumerator *) g_object_new (GST_TYPE_MM_DEVICE_ENUMERATOR, + nullptr); + + if (!self->handle) { + gst_object_unref (self); + return nullptr; + } + + gst_object_ref_sink (self); + + return self; +} + +IMMDeviceEnumerator * +gst_mm_device_enumerator_get_handle (GstMMDeviceEnumerator * enumerator) +{ + g_return_val_if_fail (GST_IS_MM_DEVICE_ENUMERATOR (enumerator), nullptr); + + return enumerator->handle; +} + +typedef struct +{ + GstMMDeviceEnumerator *self; + GstMMNotificationClientCallbacks *callbacks; + gpointer user_data; + + gboolean handled; + GMutex lock; + GCond cond; + + gboolean ret; +} SetNotificationCallbackData; + +static gboolean +set_notification_callback (SetNotificationCallbackData * data) +{ + GstMMDeviceEnumerator *self = data->self; + HRESULT hr; + + g_mutex_lock (&data->lock); + g_mutex_lock (&self->lock); + + data->ret = TRUE; + + if (self->client) { + self->handle->UnregisterEndpointNotificationCallback (self->client); + self->client->Release (); + self->client = nullptr; + } + + if (data->callbacks) { + IMMNotificationClient *client; + + hr = GstIMMNotificationClient::CreateInstance (self, data->callbacks, + data->user_data, &client); + if (FAILED (hr)) { + GST_ERROR_OBJECT (self, + "Failed to create IMMNotificationClient instance"); + data->ret = FALSE; + goto out; + } + + hr = self->handle->RegisterEndpointNotificationCallback (client); + if (FAILED (hr)) { + GST_ERROR_OBJECT (self, "Failed to register callback"); + client->Release (); + data->ret = FALSE; + goto out; + } + + self->client = client; + } + +out: + data->handled = TRUE; + g_cond_signal (&data->cond); + g_mutex_unlock (&self->lock); + g_mutex_unlock (&data->lock); + + return G_SOURCE_REMOVE; +} + +gboolean +gst_mm_device_enumerator_set_notification_callback (GstMMDeviceEnumerator * + enumerator, GstMMNotificationClientCallbacks * callbacks, + gpointer user_data) +{ + SetNotificationCallbackData data; + gboolean ret; + + g_return_val_if_fail (GST_IS_MM_DEVICE_ENUMERATOR (enumerator), FALSE); + + data.self = enumerator; + data.callbacks = callbacks; + data.user_data = user_data; + data.handled = FALSE; + + g_mutex_init (&data.lock); + g_cond_init (&data.cond); + + g_main_context_invoke (enumerator->context, + (GSourceFunc) set_notification_callback, &data); + g_mutex_lock (&data.lock); + while (!data.handled) + g_cond_wait (&data.cond, &data.lock); + g_mutex_unlock (&data.lock); + + ret = data.ret; + + g_mutex_clear (&data.lock); + g_cond_clear (&data.cond); + + return ret; +} diff --git a/sys/wasapi/gstmmdeviceenumerator.h b/sys/wasapi/gstmmdeviceenumerator.h new file mode 100644 index 000000000..d3f7b07a9 --- /dev/null +++ b/sys/wasapi/gstmmdeviceenumerator.h @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2021 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. + */ + +#ifndef __GST_MM_DEVICE_ENUMERATOR_H__ +#define __GST_MM_DEVICE_ENUMERATOR_H__ + +#include <gst/gst.h> +#include <mmdeviceapi.h> + +G_BEGIN_DECLS + +#define GST_TYPE_MM_DEVICE_ENUMERATOR (gst_mm_device_enumerator_get_type ()) +G_DECLARE_FINAL_TYPE (GstMMDeviceEnumerator, gst_mm_device_enumerator, + GST, MM_DEVICE_ENUMERATOR, GstObject); + +typedef struct +{ + HRESULT (*device_state_changed) (GstMMDeviceEnumerator * enumerator, + LPCWSTR device_id, + DWORD new_state, + gpointer user_data); + + HRESULT (*device_added) (GstMMDeviceEnumerator * enumerator, + LPCWSTR device_id, + gpointer user_data); + + HRESULT (*device_removed) (GstMMDeviceEnumerator * provider, + LPCWSTR device_id, + gpointer user_data); + + HRESULT (*default_device_changed) (GstMMDeviceEnumerator * provider, + EDataFlow flow, + ERole role, + LPCWSTR default_device_id, + gpointer user_data); + + HRESULT (*property_value_changed) (GstMMDeviceEnumerator * provider, + LPCWSTR device_id, + const PROPERTYKEY key, + gpointer user_data); +} GstMMNotificationClientCallbacks; + +GstMMDeviceEnumerator * gst_mm_device_enumerator_new (void); + +IMMDeviceEnumerator * gst_mm_device_enumerator_get_handle (GstMMDeviceEnumerator * enumerator); + +gboolean gst_mm_device_enumerator_set_notification_callback (GstMMDeviceEnumerator * enumerator, + GstMMNotificationClientCallbacks * callbacks, + gpointer user_data); + +G_END_DECLS + +#endif /* __GST_MM_DEVICE_ENUMERATOR_H__ */ diff --git a/sys/wasapi/gstwasapidevice.c b/sys/wasapi/gstwasapidevice.c index 4e6379c3e..c13fc6a8a 100644 --- a/sys/wasapi/gstwasapidevice.c +++ b/sys/wasapi/gstwasapidevice.c @@ -1,5 +1,6 @@ /* GStreamer * Copyright (C) 2018 Nirbheek Chauhan <nirbheek@centricular.com> + * Copyright (C) 2021 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 @@ -23,11 +24,27 @@ #include "gstwasapidevice.h" +GST_DEBUG_CATEGORY_EXTERN (gst_wasapi_debug); +#define GST_CAT_DEFAULT gst_wasapi_debug + G_DEFINE_TYPE (GstWasapiDeviceProvider, gst_wasapi_device_provider, GST_TYPE_DEVICE_PROVIDER); static void gst_wasapi_device_provider_finalize (GObject * object); static GList *gst_wasapi_device_provider_probe (GstDeviceProvider * provider); +static gboolean gst_wasapi_device_provider_start (GstDeviceProvider * provider); +static void gst_wasapi_device_provider_stop (GstDeviceProvider * provider); + +static HRESULT +gst_wasapi_device_provider_device_added (GstMMDeviceEnumerator * enumerator, + LPCWSTR device_id, gpointer user_data); +static HRESULT +gst_wasapi_device_provider_device_removed (GstMMDeviceEnumerator * enumerator, + LPCWSTR device_id, gpointer user_data); +static HRESULT +gst_wasapi_device_provider_default_device_changed (GstMMDeviceEnumerator * + enumerator, EDataFlow flow, ERole role, LPCWSTR device_id, + gpointer user_data); static void gst_wasapi_device_provider_class_init (GstWasapiDeviceProviderClass * klass) @@ -38,6 +55,8 @@ gst_wasapi_device_provider_class_init (GstWasapiDeviceProviderClass * klass) gobject_class->finalize = gst_wasapi_device_provider_finalize; dm_class->probe = gst_wasapi_device_provider_probe; + dm_class->start = gst_wasapi_device_provider_start; + dm_class->stop = gst_wasapi_device_provider_stop; gst_device_provider_class_set_static_metadata (dm_class, "WASAPI (Windows Audio Session API) Device Provider", @@ -46,15 +65,68 @@ gst_wasapi_device_provider_class_init (GstWasapiDeviceProviderClass * klass) } static void -gst_wasapi_device_provider_init (GstWasapiDeviceProvider * provider) +gst_wasapi_device_provider_init (GstWasapiDeviceProvider * self) { - CoInitializeEx (NULL, COINIT_MULTITHREADED); + self->enumerator = gst_mm_device_enumerator_new (); +} + +static gboolean +gst_wasapi_device_provider_start (GstDeviceProvider * provider) +{ + GstWasapiDeviceProvider *self = GST_WASAPI_DEVICE_PROVIDER (provider); + GstMMNotificationClientCallbacks callbacks = { NULL, }; + GList *devices = NULL; + GList *iter; + + if (!self->enumerator) { + GST_WARNING_OBJECT (self, "Enumerator wasn't configured"); + return FALSE; + } + + callbacks.device_added = gst_wasapi_device_provider_device_added; + callbacks.device_removed = gst_wasapi_device_provider_device_removed; + callbacks.default_device_changed = + gst_wasapi_device_provider_default_device_changed; + + if (!gst_mm_device_enumerator_set_notification_callback (self->enumerator, + &callbacks, self)) { + GST_WARNING_OBJECT (self, "Failed to set callbacks"); + return FALSE; + } + + /* baseclass will not call probe() once it's started, but we can get + * notification only add/remove or change case. To this manually */ + devices = gst_wasapi_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); + } + + return TRUE; +} + +static void +gst_wasapi_device_provider_stop (GstDeviceProvider * provider) +{ + GstWasapiDeviceProvider *self = GST_WASAPI_DEVICE_PROVIDER (provider); + + if (self->enumerator) { + gst_mm_device_enumerator_set_notification_callback (self->enumerator, + NULL, NULL); + } } static void gst_wasapi_device_provider_finalize (GObject * object) { - CoUninitialize (); + GstWasapiDeviceProvider *self = GST_WASAPI_DEVICE_PROVIDER (object); + + gst_clear_object (&self->enumerator); + + G_OBJECT_CLASS (gst_wasapi_device_provider_parent_class)->finalize (object); } static GList * @@ -63,12 +135,137 @@ gst_wasapi_device_provider_probe (GstDeviceProvider * provider) GstWasapiDeviceProvider *self = GST_WASAPI_DEVICE_PROVIDER (provider); GList *devices = NULL; - if (!gst_wasapi_util_get_devices (GST_OBJECT (self), TRUE, &devices)) + if (!gst_wasapi_util_get_devices (self->enumerator, TRUE, &devices)) GST_ERROR_OBJECT (self, "Failed to enumerate devices"); return devices; } +static gboolean +gst_wasapi_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.strid"); + 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.strid"); + 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_wasapi_device_provider_update_devices (GstWasapiDeviceProvider * 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_wasapi_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_wasapi_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_wasapi_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 HRESULT +gst_wasapi_device_provider_device_added (GstMMDeviceEnumerator * enumerator, + LPCWSTR device_id, gpointer user_data) +{ + GstWasapiDeviceProvider *self = GST_WASAPI_DEVICE_PROVIDER (user_data); + + gst_wasapi_device_provider_update_devices (self); + + return S_OK; +} + +static HRESULT +gst_wasapi_device_provider_device_removed (GstMMDeviceEnumerator * enumerator, + LPCWSTR device_id, gpointer user_data) +{ + GstWasapiDeviceProvider *self = GST_WASAPI_DEVICE_PROVIDER (user_data); + + gst_wasapi_device_provider_update_devices (self); + + return S_OK; +} + +static HRESULT +gst_wasapi_device_provider_default_device_changed (GstMMDeviceEnumerator * + enumerator, EDataFlow flow, ERole role, LPCWSTR device_id, + gpointer user_data) +{ + GstWasapiDeviceProvider *self = GST_WASAPI_DEVICE_PROVIDER (user_data); + + gst_wasapi_device_provider_update_devices (self); + + return S_OK; +} + /* GstWasapiDevice begins */ enum diff --git a/sys/wasapi/gstwasapidevice.h b/sys/wasapi/gstwasapidevice.h index 60d454b81..55eddd06b 100644 --- a/sys/wasapi/gstwasapidevice.h +++ b/sys/wasapi/gstwasapidevice.h @@ -38,6 +38,8 @@ typedef struct _GstWasapiDeviceProviderClass GstWasapiDeviceProviderClass; struct _GstWasapiDeviceProvider { GstDeviceProvider parent; + + GstMMDeviceEnumerator *enumerator; }; struct _GstWasapiDeviceProviderClass diff --git a/sys/wasapi/gstwasapisink.c b/sys/wasapi/gstwasapisink.c index 711f6f68a..b10d39110 100644 --- a/sys/wasapi/gstwasapisink.c +++ b/sys/wasapi/gstwasapisink.c @@ -180,7 +180,7 @@ gst_wasapi_sink_init (GstWasapiSink * self) self->cancellable = CreateEvent (NULL, TRUE, FALSE, NULL); self->client_needs_restart = FALSE; - CoInitializeEx (NULL, COINIT_MULTITHREADED); + self->enumerator = gst_mm_device_enumerator_new (); } static void @@ -208,6 +208,8 @@ gst_wasapi_sink_dispose (GObject * object) self->render_client = NULL; } + gst_clear_object (&self->enumerator); + G_OBJECT_CLASS (gst_wasapi_sink_parent_class)->dispose (object); } @@ -219,8 +221,6 @@ gst_wasapi_sink_finalize (GObject * object) CoTaskMemFree (self->mix_format); self->mix_format = NULL; - CoUninitialize (); - if (self->cached_caps != NULL) { gst_caps_unref (self->cached_caps); self->cached_caps = NULL; @@ -412,7 +412,7 @@ gst_wasapi_sink_open (GstAudioSink * asink) * even if the old device was unplugged. We need to handle this somehow. * For example, perhaps we should automatically switch to the new device if * the default device is changed and a device isn't explicitly selected. */ - if (!gst_wasapi_util_get_device (GST_ELEMENT (self), eRender, + if (!gst_wasapi_util_get_device (self->enumerator, eRender, self->role, self->device_strid, &device) || !gst_wasapi_util_get_audio_client (GST_ELEMENT (self), device, &client)) { @@ -485,8 +485,6 @@ gst_wasapi_sink_prepare (GstAudioSink * asink, GstAudioRingBufferSpec * spec) guint bpf, rate, devicep_frames; HRESULT hr; - CoInitializeEx (NULL, COINIT_MULTITHREADED); - if (!self->client) { GST_DEBUG_OBJECT (self, "no IAudioClient, creating a new one"); if (!gst_wasapi_util_get_audio_client (GST_ELEMENT (self), @@ -608,8 +606,6 @@ gst_wasapi_sink_unprepare (GstAudioSink * asink) self->render_client = NULL; } - CoUninitialize (); - return TRUE; } diff --git a/sys/wasapi/gstwasapisink.h b/sys/wasapi/gstwasapisink.h index 76ec38fb1..4aac69efc 100644 --- a/sys/wasapi/gstwasapisink.h +++ b/sys/wasapi/gstwasapisink.h @@ -40,6 +40,8 @@ struct _GstWasapiSink { GstAudioSink parent; + GstMMDeviceEnumerator *enumerator; + IMMDevice *device; IAudioClient *client; IAudioRenderClient *render_client; diff --git a/sys/wasapi/gstwasapisrc.c b/sys/wasapi/gstwasapisrc.c index 016f862a5..8bdff3be0 100644 --- a/sys/wasapi/gstwasapisrc.c +++ b/sys/wasapi/gstwasapisrc.c @@ -199,7 +199,7 @@ gst_wasapi_src_init (GstWasapiSrc * self) self->loopback_event_handle = CreateEvent (NULL, FALSE, FALSE, NULL); self->loopback_cancellable = CreateEvent (NULL, TRUE, FALSE, NULL); - CoInitializeEx (NULL, COINIT_MULTITHREADED); + self->enumerator = gst_mm_device_enumerator_new (); } static void @@ -247,6 +247,8 @@ gst_wasapi_src_dispose (GObject * object) self->loopback_cancellable = NULL; } + gst_clear_object (&self->enumerator); + G_OBJECT_CLASS (parent_class)->dispose (object); } @@ -258,8 +260,6 @@ gst_wasapi_src_finalize (GObject * object) CoTaskMemFree (self->mix_format); self->mix_format = NULL; - CoUninitialize (); - g_clear_pointer (&self->cached_caps, gst_caps_unref); g_clear_pointer (&self->positions, g_free); g_clear_pointer (&self->device_strid, g_free); @@ -426,7 +426,7 @@ gst_wasapi_src_open (GstAudioSrc * asrc) * even if the old device was unplugged. We need to handle this somehow. * For example, perhaps we should automatically switch to the new device if * the default device is changed and a device isn't explicitly selected. */ - if (!gst_wasapi_util_get_device (GST_ELEMENT (self), + if (!gst_wasapi_util_get_device (self->enumerator, self->loopback ? eRender : eCapture, self->role, self->device_strid, &device) || !gst_wasapi_util_get_audio_client (GST_ELEMENT (self), @@ -447,7 +447,7 @@ gst_wasapi_src_open (GstAudioSrc * asrc) * we will keep pusing silence data to into wasapi client so that make audio * client report audio data in any case */ - if (!gst_wasapi_util_get_device (GST_ELEMENT (self), + if (!gst_wasapi_util_get_device (self->enumerator, eRender, self->role, self->device_strid, &loopback_device) || !gst_wasapi_util_get_audio_client (GST_ELEMENT (self), loopback_device, &self->loopback_client)) { @@ -604,8 +604,6 @@ gst_wasapi_src_prepare (GstAudioSrc * asrc, GstAudioRingBufferSpec * spec) guint bpf, rate, devicep_frames, buffer_frames; HRESULT hr; - CoInitializeEx (NULL, COINIT_MULTITHREADED); - if (gst_wasapi_src_can_audioclient3 (self)) { if (!gst_wasapi_util_initialize_audioclient3 (GST_ELEMENT (self), spec, (IAudioClient3 *) self->client, self->mix_format, self->low_latency, @@ -744,8 +742,6 @@ gst_wasapi_src_unprepare (GstAudioSrc * asrc) self->client_clock_freq = 0; - CoUninitialize (); - return TRUE; } diff --git a/sys/wasapi/gstwasapisrc.h b/sys/wasapi/gstwasapisrc.h index 53dacd91f..8b78b7f17 100644 --- a/sys/wasapi/gstwasapisrc.h +++ b/sys/wasapi/gstwasapisrc.h @@ -40,6 +40,8 @@ struct _GstWasapiSrc { GstAudioSrc parent; + GstMMDeviceEnumerator *enumerator; + IMMDevice *device; IAudioClient *client; IAudioClock *client_clock; diff --git a/sys/wasapi/gstwasapiutil.c b/sys/wasapi/gstwasapiutil.c index 73442f7f8..b59f5f193 100644 --- a/sys/wasapi/gstwasapiutil.c +++ b/sys/wasapi/gstwasapiutil.c @@ -305,39 +305,29 @@ gst_wasapi_util_hresult_to_string (HRESULT hr) return error_text; } -static IMMDeviceEnumerator * -gst_wasapi_util_get_device_enumerator (GstObject * self) -{ - HRESULT hr; - IMMDeviceEnumerator *enumerator = NULL; - - hr = CoCreateInstance (&CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, - &IID_IMMDeviceEnumerator, (void **) &enumerator); - HR_FAILED_RET (hr, CoCreateInstance (MMDeviceEnumerator), NULL); - - return enumerator; -} - gboolean -gst_wasapi_util_get_devices (GstObject * self, gboolean active, - GList ** devices) +gst_wasapi_util_get_devices (GstMMDeviceEnumerator * self, + gboolean active, GList ** devices) { gboolean res = FALSE; static GstStaticCaps scaps = GST_STATIC_CAPS (GST_WASAPI_STATIC_CAPS); DWORD dwStateMask = active ? DEVICE_STATE_ACTIVE : DEVICE_STATEMASK_ALL; IMMDeviceCollection *device_collection = NULL; - IMMDeviceEnumerator *enumerator = NULL; + IMMDeviceEnumerator *enum_handle = NULL; const gchar *device_class, *element_name; guint ii, count; HRESULT hr; *devices = NULL; - enumerator = gst_wasapi_util_get_device_enumerator (self); - if (!enumerator) + if (!self) return FALSE; - hr = IMMDeviceEnumerator_EnumAudioEndpoints (enumerator, eAll, dwStateMask, + enum_handle = gst_mm_device_enumerator_get_handle (self); + if (!enum_handle) + return FALSE; + + hr = IMMDeviceEnumerator_EnumAudioEndpoints (enum_handle, eAll, dwStateMask, &device_collection); HR_FAILED_GOTO (hr, IMMDeviceEnumerator::EnumAudioEndpoints, err); @@ -459,8 +449,6 @@ gst_wasapi_util_get_devices (GstObject * self, gboolean active, res = TRUE; err: - if (enumerator) - IUnknown_Release (enumerator); if (device_collection) IUnknown_Release (device_collection); return res; @@ -533,24 +521,28 @@ out: } gboolean -gst_wasapi_util_get_device (GstElement * self, +gst_wasapi_util_get_device (GstMMDeviceEnumerator * self, gint data_flow, gint role, const wchar_t * device_strid, IMMDevice ** ret_device) { gboolean res = FALSE; HRESULT hr; - IMMDeviceEnumerator *enumerator = NULL; + IMMDeviceEnumerator *enum_handle = NULL; IMMDevice *device = NULL; - if (!(enumerator = gst_wasapi_util_get_device_enumerator (GST_OBJECT (self)))) - goto beach; + if (!self) + return FALSE; + + enum_handle = gst_mm_device_enumerator_get_handle (self); + if (!enum_handle) + return FALSE; if (!device_strid) { - hr = IMMDeviceEnumerator_GetDefaultAudioEndpoint (enumerator, data_flow, + hr = IMMDeviceEnumerator_GetDefaultAudioEndpoint (enum_handle, data_flow, role, &device); HR_FAILED_GOTO (hr, IMMDeviceEnumerator::GetDefaultAudioEndpoint, beach); } else { - hr = IMMDeviceEnumerator_GetDevice (enumerator, device_strid, &device); + hr = IMMDeviceEnumerator_GetDevice (enum_handle, device_strid, &device); if (hr != S_OK) { gchar *msg = gst_wasapi_util_hresult_to_string (hr); GST_ERROR_OBJECT (self, "IMMDeviceEnumerator::GetDevice (%S) failed" @@ -569,9 +561,6 @@ beach: if (device != NULL) IUnknown_Release (device); - if (enumerator != NULL) - IUnknown_Release (enumerator); - return res; } diff --git a/sys/wasapi/gstwasapiutil.h b/sys/wasapi/gstwasapiutil.h index 8cd5d3240..70f241980 100644 --- a/sys/wasapi/gstwasapiutil.h +++ b/sys/wasapi/gstwasapiutil.h @@ -29,6 +29,7 @@ #include <audioclient.h> #include "gstaudioclient3.h" +#include "gstmmdeviceenumerator.h" /* Static Caps shared between source, sink, and device provider */ #define GST_WASAPI_STATIC_CAPS "audio/x-raw, " \ @@ -92,10 +93,11 @@ gint gst_wasapi_erole_to_device_role (gint erole); gchar *gst_wasapi_util_hresult_to_string (HRESULT hr); -gboolean gst_wasapi_util_get_devices (GstObject * element, gboolean active, - GList ** devices); +gboolean gst_wasapi_util_get_devices (GstMMDeviceEnumerator * enumerator, + gboolean active, + GList ** devices); -gboolean gst_wasapi_util_get_device (GstElement * self, +gboolean gst_wasapi_util_get_device (GstMMDeviceEnumerator * enumerator, gint data_flow, gint role, const wchar_t * device_strid, IMMDevice ** ret_device); diff --git a/sys/wasapi/meson.build b/sys/wasapi/meson.build index 9adc3d690..3fdd73a2f 100644 --- a/sys/wasapi/meson.build +++ b/sys/wasapi/meson.build @@ -1,4 +1,5 @@ wasapi_sources = [ + 'gstmmdeviceenumerator.cpp', 'gstwasapi.c', 'gstwasapisrc.c', 'gstwasapisink.c', @@ -30,6 +31,7 @@ if ole32_dep.found() and ksuser_dep.found() and have_audioclient_h gstwasapi = library('gstwasapi', wasapi_sources, c_args : gst_plugins_bad_args + wasapi_args, + cpp_args: gst_plugins_bad_args, include_directories : [configinc], dependencies : [gstaudio_dep, ole32_dep, ksuser_dep], install : true, diff --git a/tests/check/elements/wasapi.c b/tests/check/elements/wasapi.c index 9132b19ad..881bb682c 100644 --- a/tests/check/elements/wasapi.c +++ b/tests/check/elements/wasapi.c @@ -28,14 +28,143 @@ typedef struct { GMainLoop *loop; + GstElement *pipeline; + guint n_buffers; + guint restart_count; + GstState reuse_state; +} SrcReuseTestData; + +static gboolean +src_reuse_bus_handler (GstBus * bus, GstMessage * message, + SrcReuseTestData * data) +{ + if (GST_MESSAGE_TYPE (message) == GST_MESSAGE_ERROR) { + GST_ERROR ("Got error message from pipeline"); + g_main_loop_quit (data->loop); + } + + return TRUE; +} + +static void +start_pipeline (SrcReuseTestData * data) +{ + GstStateChangeReturn ret; + + GST_INFO ("Start pipeline"); + ret = gst_element_set_state (data->pipeline, GST_STATE_PLAYING); + fail_unless (ret != GST_STATE_CHANGE_FAILURE); +} + +static gboolean +restart_pipeline (SrcReuseTestData * data) +{ + data->restart_count++; + start_pipeline (data); + + return G_SOURCE_REMOVE; +} + +static gboolean +handle_handoff (SrcReuseTestData * data) +{ + data->n_buffers++; + + /* Restart every 10 packets */ + if (data->n_buffers > 10) { + GstStateChangeReturn ret; + data->n_buffers = 0; + + ret = gst_element_set_state (data->pipeline, data->reuse_state); + fail_unless (ret != GST_STATE_CHANGE_FAILURE); + + if (data->restart_count < 2) { + GST_INFO ("Restart pipeline, current restart count %d", + data->restart_count); + g_timeout_add_seconds (1, (GSourceFunc) restart_pipeline, data); + } else { + GST_INFO ("Finish test"); + g_main_loop_quit (data->loop); + } + } + + return G_SOURCE_REMOVE; +} + +static void +on_sink_handoff (GstElement * element, GstBuffer * buffer, GstPad * pad, + SrcReuseTestData * data) +{ + g_idle_add ((GSourceFunc) handle_handoff, data); +} + +static void +wasapisrc_reuse (GstState reuse_state) +{ + GstBus *bus; + GstElement *sink; + SrcReuseTestData data; + + memset (&data, 0, sizeof (SrcReuseTestData)); + + data.loop = g_main_loop_new (NULL, FALSE); + + data.pipeline = gst_parse_launch ("wasapisrc provide-clock=false ! queue ! " + "fakesink name=sink async=false", NULL); + fail_unless (data.pipeline != NULL); + data.reuse_state = reuse_state; + + sink = gst_bin_get_by_name (GST_BIN (data.pipeline), "sink"); + fail_unless (sink); + + g_object_set (G_OBJECT (sink), "signal-handoffs", TRUE, NULL); + g_signal_connect (sink, "handoff", G_CALLBACK (on_sink_handoff), &data); + + bus = gst_element_get_bus (GST_ELEMENT (data.pipeline)); + fail_unless (bus != NULL); + + gst_bus_add_watch (bus, (GstBusFunc) src_reuse_bus_handler, &data); + start_pipeline (&data); + g_main_loop_run (data.loop); + + fail_unless (data.restart_count == 2); + + gst_element_set_start_time (data.pipeline, GST_STATE_NULL); + gst_bus_remove_watch (bus); + gst_object_unref (bus); + + gst_object_unref (data.pipeline); + g_main_loop_unref (data.loop); +} + +/* https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad/-/issues/1110 */ +GST_START_TEST (test_wasapisrc_reuse_null) +{ + wasapisrc_reuse (GST_STATE_NULL); +} + +GST_END_TEST; + +GST_START_TEST (test_wasapisrc_reuse_ready) +{ + wasapisrc_reuse (GST_STATE_READY); +} + +GST_END_TEST; + +typedef struct +{ + GMainLoop *loop; GstElement *pipe; guint rem_st_changes; + GstState reuse_state; } SinkPlayReadyTData; static gboolean -bus_watch_cb (GstBus * bus, GstMessage * message, gpointer user_data) +sink_reuse_bus_watch_cb (GstBus * bus, GstMessage * message, gpointer user_data) { fail_unless (message->type != GST_MESSAGE_ERROR); + return G_SOURCE_CONTINUE; } @@ -44,7 +173,7 @@ state_timer_cb (gpointer user_data) { SinkPlayReadyTData *tdata = user_data; GstState nxt_st = tdata->rem_st_changes % 2 == 1 ? - GST_STATE_READY : GST_STATE_PLAYING; + tdata->reuse_state : GST_STATE_PLAYING; ASSERT_SET_STATE (tdata->pipe, nxt_st, GST_STATE_CHANGE_SUCCESS); tdata->rem_st_changes--; @@ -58,7 +187,8 @@ state_timer_cb (gpointer user_data) /* Test that the wasapisink can survive the state change * from PLAYING to READY and then back to PLAYING */ -GST_START_TEST (test_sink_play_ready) +static void +wasapisink_reuse (GstState reuse_state) { SinkPlayReadyTData tdata; GstBus *bus; @@ -67,7 +197,9 @@ GST_START_TEST (test_sink_play_ready) fail_unless (tdata.pipe != NULL); bus = gst_element_get_bus (tdata.pipe); fail_unless (bus != NULL); - gst_bus_add_watch (bus, bus_watch_cb, NULL); + gst_bus_add_watch (bus, sink_reuse_bus_watch_cb, NULL); + + tdata.reuse_state = reuse_state; ASSERT_SET_STATE (tdata.pipe, GST_STATE_PLAYING, GST_STATE_CHANGE_SUCCESS); tdata.rem_st_changes = 3; /* -> READY -> PLAYING -> QUIT */ @@ -82,47 +214,78 @@ GST_START_TEST (test_sink_play_ready) gst_object_unref (tdata.pipe); } +GST_START_TEST (test_wasapisink_reuse_null) +{ + wasapisink_reuse (GST_STATE_NULL); +} + +GST_END_TEST; + +GST_START_TEST (test_wasapisink_reuse_ready) +{ + wasapisink_reuse (GST_STATE_READY); +} + GST_END_TEST; static gboolean -device_is_available (const gchar * factory_name) +check_wasapi_element (gboolean is_src) { + gboolean ret = TRUE; GstElement *elem; - gboolean avail; + const gchar *elem_name; + + if (is_src) + elem_name = "wasapisrc"; + else + elem_name = "wasapisink"; - elem = gst_element_factory_make (factory_name, NULL); - if (elem == NULL) { - GST_INFO ("%s: not available", factory_name); + elem = gst_element_factory_make (elem_name, NULL); + if (!elem) { + GST_INFO ("%s is not available", elem_name); return FALSE; } - avail = gst_element_set_state (elem, GST_STATE_READY) - == GST_STATE_CHANGE_SUCCESS; - if (!avail) { - GST_INFO ("%s: cannot change state to ready", factory_name); + /* GST_STATE_READY is meaning that camera is available */ + if (gst_element_set_state (elem, GST_STATE_READY) != GST_STATE_CHANGE_SUCCESS) { + GST_INFO ("cannot open device"); + ret = FALSE; } gst_element_set_state (elem, GST_STATE_NULL); gst_object_unref (elem); - return avail; + return ret; } static Suite * wasapi_suite (void) { - Suite *suite = suite_create ("wasapi"); - TCase *tc_sink = tcase_create ("sink"); + Suite *s = suite_create ("wasapi"); + TCase *tc_basic = tcase_create ("general"); + gboolean have_src = FALSE; + gboolean have_sink = FALSE; - suite_add_tcase (suite, tc_sink); + suite_add_tcase (s, tc_basic); - if (device_is_available ("wasapisink")) { - tcase_add_test (tc_sink, test_sink_play_ready); + have_src = check_wasapi_element (TRUE); + have_sink = check_wasapi_element (FALSE); + + if (!have_src && !have_sink) { + GST_INFO ("Skipping tests, wasapisrc/wasapisink are unavailable"); } else { - GST_INFO ("Sink not available, skipping sink tests"); + if (have_src) { + tcase_add_test (tc_basic, test_wasapisrc_reuse_null); + tcase_add_test (tc_basic, test_wasapisrc_reuse_ready); + } + + if (have_sink) { + tcase_add_test (tc_basic, test_wasapisink_reuse_null); + tcase_add_test (tc_basic, test_wasapisink_reuse_ready); + } } - return suite; + return s; } GST_CHECK_MAIN (wasapi) |