diff --git a/subprojects/gst-plugins-bad/docs/plugins/gst_plugins_cache.json b/subprojects/gst-plugins-bad/docs/plugins/gst_plugins_cache.json index d95644b883..5f7e7d7298 100644 --- a/subprojects/gst-plugins-bad/docs/plugins/gst_plugins_cache.json +++ b/subprojects/gst-plugins-bad/docs/plugins/gst_plugins_cache.json @@ -230073,6 +230073,32 @@ "type": "gboolean", "writable": true }, + "loopback-mode": { + "blurb": "Loopback mode to use", + "conditionally-available": true, + "construct": false, + "construct-only": false, + "controllable": false, + "default": "default (0)", + "mutable": "ready", + "readable": true, + "type": "GstWasapi2SrcLoopbackMode", + "writable": true + }, + "loopback-target-pid": { + "blurb": "Process ID to be recorded or excluded for process loopback mode", + "conditionally-available": true, + "construct": false, + "construct-only": false, + "controllable": false, + "default": "0", + "max": "-1", + "min": "0", + "mutable": "ready", + "readable": true, + "type": "guint", + "writable": true + }, "low-latency": { "blurb": "Optimize all settings for lowest latency. Always safe to enable.", "conditionally-available": false, @@ -230117,7 +230143,28 @@ }, "filename": "gstwasapi2", "license": "LGPL", - "other-types": {}, + "other-types": { + "GstWasapi2SrcLoopbackMode": { + "kind": "enum", + "values": [ + { + "desc": "Default", + "name": "default", + "value": "0" + }, + { + "desc": "Include process and its child processes", + "name": "include-process-tree", + "value": "1" + }, + { + "desc": "Exclude process and its child processes", + "name": "exclude-process-tree", + "value": "2" + } + ] + } + }, "package": "GStreamer Bad Plug-ins", "source": "gst-plugins-bad", "tracers": {}, diff --git a/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2client.cpp b/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2client.cpp index f8a422091a..b2d1ac8472 100644 --- a/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2client.cpp +++ b/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2client.cpp @@ -52,6 +52,37 @@ using namespace ABI::Windows::Devices::Enumeration; using namespace Microsoft::WRL; using namespace Microsoft::WRL::Wrappers; +/* Copy of audioclientactivationparams.h since those types are defined only for + * NTDDI_VERSION >= NTDDI_WIN10_FE */ +#define GST_VIRTUAL_AUDIO_DEVICE_PROCESS_LOOPBACK L"VAD\\Process_Loopback" +typedef enum +{ + GST_PROCESS_LOOPBACK_MODE_INCLUDE_TARGET_PROCESS_TREE = 0, + GST_PROCESS_LOOPBACK_MODE_EXCLUDE_TARGET_PROCESS_TREE = 1 +} GST_PROCESS_LOOPBACK_MODE; + +typedef struct +{ + DWORD TargetProcessId; + GST_PROCESS_LOOPBACK_MODE ProcessLoopbackMode; +} GST_AUDIOCLIENT_PROCESS_LOOPBACK_PARAMS; + +typedef enum +{ + GST_AUDIOCLIENT_ACTIVATION_TYPE_DEFAULT = 0, + GST_AUDIOCLIENT_ACTIVATION_TYPE_PROCESS_LOOPBACK = 1 +} GST_AUDIOCLIENT_ACTIVATION_TYPE; + +typedef struct +{ + GST_AUDIOCLIENT_ACTIVATION_TYPE ActivationType; + union + { + GST_AUDIOCLIENT_PROCESS_LOOPBACK_PARAMS ProcessLoopbackParams; + } DUMMYUNIONNAME; +} GST_AUDIOCLIENT_ACTIVATION_PARAMS; +/* End of audioclientactivationparams.h */ + G_BEGIN_DECLS GST_DEBUG_CATEGORY_EXTERN (gst_wasapi2_client_debug); @@ -152,19 +183,29 @@ public: } HRESULT - ActivateDeviceAsync(const std::wstring &device_id) + ActivateDeviceAsync(const std::wstring &device_id, + GST_AUDIOCLIENT_ACTIVATION_PARAMS * params) { ComPtr async_action; bool run_async = false; HRESULT hr; auto work_item = Callback, - IDispatchedHandler, FtmBase>>([this, device_id]{ + IDispatchedHandler, FtmBase>>([this, device_id, params]{ ComPtr async_op; HRESULT async_hr = S_OK; + PROPVARIANT activate_params = {}; + if (params) { + activate_params.vt = VT_BLOB; + activate_params.blob.cbSize = sizeof(GST_AUDIOCLIENT_ACTIVATION_PARAMS); + activate_params.blob.pBlobData = (BYTE *) params; - async_hr = ActivateAudioInterfaceAsync (device_id.c_str (), - __uuidof(IAudioClient), nullptr, this, &async_op); + async_hr = ActivateAudioInterfaceAsync (device_id.c_str (), + __uuidof(IAudioClient), &activate_params, this, &async_op); + } else { + async_hr = ActivateAudioInterfaceAsync (device_id.c_str (), + __uuidof(IAudioClient), nullptr, this, &async_op); + } /* for debugging */ gst_wasapi2_result (async_hr); @@ -227,6 +268,7 @@ enum PROP_DEVICE_CLASS, PROP_DISPATCHER, PROP_CAN_AUTO_ROUTING, + PROP_LOOPBACK_TARGET_PID, }; #define DEFAULT_DEVICE_INDEX -1 @@ -242,6 +284,7 @@ struct _GstWasapi2Client gint device_index; gpointer dispatcher; gboolean can_auto_routing; + guint target_pid; IAudioClient *audio_client; GstWasapiDeviceActivator *activator; @@ -269,6 +312,12 @@ gst_wasapi2_client_device_class_get_type (void) {GST_WASAPI2_CLIENT_DEVICE_CLASS_RENDER, "Render", "render"}, {GST_WASAPI2_CLIENT_DEVICE_CLASS_LOOPBACK_CAPTURE, "Loopback-Capture", "loopback-capture"}, + {GST_WASAPI2_CLIENT_DEVICE_CLASS_INCLUDE_PROCESS_LOOPBACK_CAPTURE, + "Include-Process-Loopback-Capture", + "include-process-loopback-capture"}, + {GST_WASAPI2_CLIENT_DEVICE_CLASS_EXCLUDE_PROCESS_LOOPBACK_CAPTURE, + "Exclude-Process-Loopback-Capture", + "exclude-process-loopback-capture"}, {0, nullptr, nullptr} }; @@ -328,6 +377,9 @@ gst_wasapi2_client_class_init (GstWasapi2ClientClass * klass) g_param_spec_boolean ("auto-routing", "Auto Routing", "Whether client can support automatic stream routing", FALSE, (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS))); + g_object_class_install_property (gobject_class, PROP_LOOPBACK_TARGET_PID, + g_param_spec_uint ("loopback-target-pid", "Loopback Target PID", + "Target process id to record", 0, G_MAXUINT32, 0, param_flags)); } static void @@ -425,6 +477,9 @@ gst_wasapi2_client_get_property (GObject * object, guint prop_id, case PROP_CAN_AUTO_ROUTING: g_value_set_boolean (value, self->can_auto_routing); break; + case PROP_LOOPBACK_TARGET_PID: + g_value_set_uint (value, self->target_pid); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -456,6 +511,9 @@ gst_wasapi2_client_set_property (GObject * object, guint prop_id, case PROP_DISPATCHER: self->dispatcher = g_value_get_pointer (value); break; + case PROP_LOOPBACK_TARGET_PID: + self->target_pid = g_value_get_uint (value); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -559,6 +617,46 @@ gst_wasapi2_client_activate_async (GstWasapi2Client * self, std::string target_device_id; std::string target_device_name; gboolean use_default_device = FALSE; + GST_AUDIOCLIENT_ACTIVATION_PARAMS activation_params; + gboolean process_loopback = FALSE; + + memset (&activation_params, 0, sizeof (GST_AUDIOCLIENT_ACTIVATION_PARAMS)); + activation_params.ActivationType = GST_AUDIOCLIENT_ACTIVATION_TYPE_DEFAULT; + + if (self->device_class == + GST_WASAPI2_CLIENT_DEVICE_CLASS_INCLUDE_PROCESS_LOOPBACK_CAPTURE || + self->device_class == + GST_WASAPI2_CLIENT_DEVICE_CLASS_EXCLUDE_PROCESS_LOOPBACK_CAPTURE) { + if (self->target_pid == 0) { + GST_ERROR_OBJECT (self, "Process loopback mode without PID"); + goto failed; + } + + if (!gst_wasapi2_can_process_loopback ()) { + GST_ERROR_OBJECT (self, "Process loopback is not supported"); + goto failed; + } + + process_loopback = TRUE; + activation_params.ActivationType = + GST_AUDIOCLIENT_ACTIVATION_TYPE_PROCESS_LOOPBACK; + activation_params.ProcessLoopbackParams.TargetProcessId = + (DWORD) self->target_pid; + target_device_id_wstring = GST_VIRTUAL_AUDIO_DEVICE_PROCESS_LOOPBACK; + target_device_id = convert_wstring_to_string (target_device_id_wstring); + + if (self->device_class == + GST_WASAPI2_CLIENT_DEVICE_CLASS_INCLUDE_PROCESS_LOOPBACK_CAPTURE) { + activation_params.ProcessLoopbackParams.ProcessLoopbackMode = + GST_PROCESS_LOOPBACK_MODE_INCLUDE_TARGET_PROCESS_TREE; + } else { + activation_params.ProcessLoopbackParams.ProcessLoopbackMode = + GST_PROCESS_LOOPBACK_MODE_EXCLUDE_TARGET_PROCESS_TREE; + } + + target_device_name = "Process-loopback"; + goto activate; + } GST_INFO_OBJECT (self, "requested device info, device-class: %s, device: %s, device-index: %d", @@ -773,7 +871,13 @@ activate: /* default device supports automatic stream routing */ self->can_auto_routing = use_default_device; - hr = activator->ActivateDeviceAsync (target_device_id_wstring); + if (process_loopback) { + hr = activator->ActivateDeviceAsync (target_device_id_wstring, + &activation_params); + } else { + hr = activator->ActivateDeviceAsync (target_device_id_wstring, nullptr); + } + if (!gst_wasapi2_result (hr)) { GST_WARNING_OBJECT (self, "Failed to activate device"); goto failed; @@ -886,8 +990,12 @@ gst_wasapi2_client_get_caps (GstWasapi2Client * client) hr = client->audio_client->GetMixFormat (&mix_format); if (!gst_wasapi2_result (hr)) { - GST_WARNING_OBJECT (client, "Failed to get mix format"); - return nullptr; + if (gst_wasapi2_device_class_is_process_loopback (client->device_class)) { + mix_format = gst_wasapi2_get_default_mix_format (); + } else { + GST_WARNING_OBJECT (client, "Failed to get mix format"); + return nullptr; + } } scaps = gst_static_caps_get (&static_caps); @@ -950,7 +1058,8 @@ find_dispatcher (ICoreDispatcher ** dispatcher) GstWasapi2Client * gst_wasapi2_client_new (GstWasapi2ClientDeviceClass device_class, - gint device_index, const gchar * device_id, gpointer dispatcher) + gint device_index, const gchar * device_id, guint32 target_pid, + gpointer dispatcher) { GstWasapi2Client *self; /* *INDENT-OFF* */ @@ -977,7 +1086,8 @@ gst_wasapi2_client_new (GstWasapi2ClientDeviceClass device_class, self = (GstWasapi2Client *) g_object_new (GST_TYPE_WASAPI2_CLIENT, "device-class", device_class, "device-index", device_index, - "device", device_id, "dispatcher", dispatcher, nullptr); + "device", device_id, "loopback-target-pid", target_pid, + "dispatcher", dispatcher, nullptr); /* Reset explicitly to ensure that it happens before * RoInitializeWrapper dtor is called */ diff --git a/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2client.h b/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2client.h index ec1d14df66..4c2ee2134e 100644 --- a/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2client.h +++ b/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2client.h @@ -31,8 +31,37 @@ typedef enum GST_WASAPI2_CLIENT_DEVICE_CLASS_CAPTURE = 0, GST_WASAPI2_CLIENT_DEVICE_CLASS_RENDER, GST_WASAPI2_CLIENT_DEVICE_CLASS_LOOPBACK_CAPTURE, + GST_WASAPI2_CLIENT_DEVICE_CLASS_INCLUDE_PROCESS_LOOPBACK_CAPTURE, + GST_WASAPI2_CLIENT_DEVICE_CLASS_EXCLUDE_PROCESS_LOOPBACK_CAPTURE, } GstWasapi2ClientDeviceClass; +static inline gboolean +gst_wasapi2_device_class_is_loopback (GstWasapi2ClientDeviceClass device_class) +{ + switch (device_class) { + case GST_WASAPI2_CLIENT_DEVICE_CLASS_LOOPBACK_CAPTURE: + return TRUE; + default: + break; + } + + return FALSE; +} + +static inline gboolean +gst_wasapi2_device_class_is_process_loopback (GstWasapi2ClientDeviceClass device_class) +{ + switch (device_class) { + case GST_WASAPI2_CLIENT_DEVICE_CLASS_INCLUDE_PROCESS_LOOPBACK_CAPTURE: + case GST_WASAPI2_CLIENT_DEVICE_CLASS_EXCLUDE_PROCESS_LOOPBACK_CAPTURE: + return TRUE; + default: + break; + } + + return FALSE; +} + #define GST_TYPE_WASAPI2_CLIENT_DEVICE_CLASS (gst_wasapi2_client_device_class_get_type()) GType gst_wasapi2_client_device_class_get_type (void); @@ -43,6 +72,7 @@ G_DECLARE_FINAL_TYPE (GstWasapi2Client, GstWasapi2Client * gst_wasapi2_client_new (GstWasapi2ClientDeviceClass device_class, gint device_index, const gchar * device_id, + guint target_pid, gpointer dispatcher); gboolean gst_wasapi2_client_ensure_activation (GstWasapi2Client * client); diff --git a/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2device.c b/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2device.c index f259c94ad5..0d70801675 100644 --- a/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2device.c +++ b/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2device.c @@ -283,7 +283,7 @@ gst_wasapi2_device_provider_probe_internal (GstWasapi2DeviceProvider * self, gchar *device_id = NULL; gchar *device_name = NULL; - client = gst_wasapi2_client_new (client_class, i, NULL, NULL); + client = gst_wasapi2_client_new (client_class, i, NULL, 0, NULL); if (!client) return; diff --git a/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2ringbuffer.cpp b/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2ringbuffer.cpp index 60231e387b..d1d4f9b559 100644 --- a/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2ringbuffer.cpp +++ b/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2ringbuffer.cpp @@ -145,6 +145,7 @@ struct _GstWasapi2RingBuffer gdouble volume; gpointer dispatcher; gboolean can_auto_routing; + guint loopback_target_pid; GstWasapi2Client *client; GstWasapi2Client *loopback_client; @@ -380,7 +381,7 @@ gst_wasapi2_ring_buffer_open_device (GstAudioRingBuffer * buf) } self->client = gst_wasapi2_client_new (self->device_class, - -1, self->device_id, self->dispatcher); + -1, self->device_id, self->loopback_target_pid, self->dispatcher); if (!self->client) { gst_wasapi2_ring_buffer_post_open_error (self); return FALSE; @@ -389,10 +390,10 @@ gst_wasapi2_ring_buffer_open_device (GstAudioRingBuffer * buf) g_object_get (self->client, "auto-routing", &self->can_auto_routing, nullptr); /* Open another render client to feed silence */ - if (self->device_class == GST_WASAPI2_CLIENT_DEVICE_CLASS_LOOPBACK_CAPTURE) { + if (gst_wasapi2_device_class_is_loopback (self->device_class)) { self->loopback_client = gst_wasapi2_client_new (GST_WASAPI2_CLIENT_DEVICE_CLASS_RENDER, - -1, self->device_id, self->dispatcher); + -1, self->device_id, 0, self->dispatcher); if (!self->loopback_client) { gst_wasapi2_ring_buffer_post_open_error (self); @@ -480,19 +481,25 @@ gst_wasapi2_ring_buffer_read (GstWasapi2RingBuffer * self) ", expected position %" G_GUINT64_FORMAT, to_read, position, self->expected_position); - if (self->is_first) { - self->expected_position = position + to_read; - self->is_first = FALSE; - } else { - if (position > self->expected_position) { - guint gap_frames; + /* XXX: position might not be increased in case of process loopback */ + if (!gst_wasapi2_device_class_is_process_loopback (self->device_class)) { + if (self->is_first) { + self->expected_position = position + to_read; + self->is_first = FALSE; + } else { + if (position > self->expected_position) { + guint gap_frames; - gap_frames = (guint) (position - self->expected_position); - GST_WARNING_OBJECT (self, "Found %u frames gap", gap_frames); - gap_size = gap_frames * GST_AUDIO_INFO_BPF (info); + gap_frames = (guint) (position - self->expected_position); + GST_WARNING_OBJECT (self, "Found %u frames gap", gap_frames); + gap_size = gap_frames * GST_AUDIO_INFO_BPF (info); + } + + self->expected_position = position + to_read; } - - self->expected_position = position + to_read; + } else if (self->mute) { + /* volume clinet might not be available in case of process loopback */ + flags |= AUDCLNT_BUFFERFLAGS_SILENT; } /* Fill gap data if any */ @@ -679,6 +686,8 @@ gst_wasapi2_ring_buffer_io_callback (GstWasapi2RingBuffer * self) switch (self->device_class) { case GST_WASAPI2_CLIENT_DEVICE_CLASS_CAPTURE: case GST_WASAPI2_CLIENT_DEVICE_CLASS_LOOPBACK_CAPTURE: + case GST_WASAPI2_CLIENT_DEVICE_CLASS_INCLUDE_PROCESS_LOOPBACK_CAPTURE: + case GST_WASAPI2_CLIENT_DEVICE_CLASS_EXCLUDE_PROCESS_LOOPBACK_CAPTURE: hr = gst_wasapi2_ring_buffer_read (self); break; case GST_WASAPI2_CLIENT_DEVICE_CLASS_RENDER: @@ -694,7 +703,8 @@ gst_wasapi2_ring_buffer_io_callback (GstWasapi2RingBuffer * self) * loopback capture client doesn't seem to be able to recover status from this * situation */ if (self->can_auto_routing && - self->device_class != GST_WASAPI2_CLIENT_DEVICE_CLASS_LOOPBACK_CAPTURE && + !gst_wasapi2_device_class_is_loopback (self->device_class) && + !gst_wasapi2_device_class_is_process_loopback (self->device_class) && (hr == AUDCLNT_E_ENDPOINT_CREATE_FAILED || hr == AUDCLNT_E_DEVICE_INVALIDATED)) { GST_WARNING_OBJECT (self, @@ -777,8 +787,8 @@ gst_wasapi2_ring_buffer_loopback_callback (GstWasapi2RingBuffer * self) HRESULT hr = E_FAIL; g_return_val_if_fail (GST_IS_WASAPI2_RING_BUFFER (self), E_FAIL); - g_return_val_if_fail (self->device_class == - GST_WASAPI2_CLIENT_DEVICE_CLASS_LOOPBACK_CAPTURE, E_FAIL); + g_return_val_if_fail (gst_wasapi2_device_class_is_loopback + (self->device_class), E_FAIL); if (!self->running) { GST_INFO_OBJECT (self, "We are not running now"); @@ -852,7 +862,7 @@ gst_wasapi2_ring_buffer_initialize_audio_client3 (GstWasapi2RingBuffer * self, static HRESULT gst_wasapi2_ring_buffer_initialize_audio_client (GstWasapi2RingBuffer * self, IAudioClient * client_handle, WAVEFORMATEX * mix_format, guint * period, - DWORD extra_flags) + DWORD extra_flags, GstWasapi2ClientDeviceClass device_class) { GstAudioRingBuffer *ringbuffer = GST_AUDIO_RING_BUFFER_CAST (self); REFERENCE_TIME default_period, min_period; @@ -862,24 +872,36 @@ gst_wasapi2_ring_buffer_initialize_audio_client (GstWasapi2RingBuffer * self, stream_flags |= extra_flags; - hr = client_handle->GetDevicePeriod (&default_period, &min_period); - if (!gst_wasapi2_result (hr)) { - GST_WARNING_OBJECT (self, "Couldn't get device period info"); - return hr; + if (!gst_wasapi2_device_class_is_process_loopback (device_class)) { + hr = client_handle->GetDevicePeriod (&default_period, &min_period); + if (!gst_wasapi2_result (hr)) { + GST_WARNING_OBJECT (self, "Couldn't get device period info"); + return hr; + } + + GST_INFO_OBJECT (self, "wasapi2 default period: %" G_GINT64_FORMAT + ", min period: %" G_GINT64_FORMAT, default_period, min_period); + + hr = client_handle->Initialize (AUDCLNT_SHAREMODE_SHARED, stream_flags, + /* hnsBufferDuration should be same as hnsPeriodicity + * when AUDCLNT_STREAMFLAGS_EVENTCALLBACK is used. + * And in case of shared mode, hnsPeriodicity should be zero, so + * this value should be zero as well */ + 0, + /* This must always be 0 in shared mode */ + 0, mix_format, nullptr); + } else { + /* XXX: virtual device will not report device period. + * Use hardcoded period 20ms, same as Microsoft sample code + * https://github.com/microsoft/windows-classic-samples/tree/main/Samples/ApplicationLoopback + */ + default_period = (20 * GST_MSECOND) / 100; + hr = client_handle->Initialize (AUDCLNT_SHAREMODE_SHARED, + AUDCLNT_STREAMFLAGS_LOOPBACK | AUDCLNT_STREAMFLAGS_EVENTCALLBACK, + default_period, + AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM, mix_format, nullptr); } - GST_INFO_OBJECT (self, "wasapi2 default period: %" G_GINT64_FORMAT - ", min period: %" G_GINT64_FORMAT, default_period, min_period); - - hr = client_handle->Initialize (AUDCLNT_SHAREMODE_SHARED, stream_flags, - /* hnsBufferDuration should be same as hnsPeriodicity - * when AUDCLNT_STREAMFLAGS_EVENTCALLBACK is used. - * And in case of shared mode, hnsPeriodicity should be zero, so - * this value should be zero as well */ - 0, - /* This must always be 0 in shared mode */ - 0, mix_format, nullptr); - if (!gst_wasapi2_result (hr)) { GST_WARNING_OBJECT (self, "Couldn't initialize audioclient"); return hr; @@ -923,7 +945,7 @@ gst_wasapi2_ring_buffer_prepare_loopback_client (GstWasapi2RingBuffer * self) } hr = gst_wasapi2_ring_buffer_initialize_audio_client (self, client_handle, - mix_format, &period, 0); + mix_format, &period, 0, GST_WASAPI2_CLIENT_DEVICE_CLASS_RENDER); if (!gst_wasapi2_result (hr)) { GST_ERROR_OBJECT (self, "Failed to initialize audio client"); @@ -970,7 +992,7 @@ gst_wasapi2_ring_buffer_acquire (GstAudioRingBuffer * buf, if (!self->client && !gst_wasapi2_ring_buffer_open_device (buf)) return FALSE; - if (self->device_class == GST_WASAPI2_CLIENT_DEVICE_CLASS_LOOPBACK_CAPTURE) { + if (gst_wasapi2_device_class_is_loopback (self->device_class)) { if (!gst_wasapi2_ring_buffer_prepare_loopback_client (self)) { GST_ERROR_OBJECT (self, "Failed to prepare loopback client"); goto error; @@ -991,8 +1013,12 @@ gst_wasapi2_ring_buffer_acquire (GstAudioRingBuffer * buf, /* TODO: convert given caps to mix format */ hr = client_handle->GetMixFormat (&mix_format); if (!gst_wasapi2_result (hr)) { - GST_ERROR_OBJECT (self, "Failed to get mix format"); - goto error; + if (gst_wasapi2_device_class_is_process_loopback (self->device_class)) { + mix_format = gst_wasapi2_get_default_mix_format (); + } else { + GST_ERROR_OBJECT (self, "Failed to get mix format"); + goto error; + } } /* Only use audioclient3 when low-latency is requested because otherwise @@ -1002,7 +1028,8 @@ gst_wasapi2_ring_buffer_acquire (GstAudioRingBuffer * buf, if (self->low_latency && /* AUDCLNT_STREAMFLAGS_LOOPBACK is not allowed for * InitializeSharedAudioStream */ - self->device_class != GST_WASAPI2_CLIENT_DEVICE_CLASS_LOOPBACK_CAPTURE) { + !gst_wasapi2_device_class_is_loopback (self->device_class) && + !gst_wasapi2_device_class_is_process_loopback (self->device_class)) { hr = gst_wasapi2_ring_buffer_initialize_audio_client3 (self, client_handle, mix_format, &period); } @@ -1015,11 +1042,11 @@ gst_wasapi2_ring_buffer_acquire (GstAudioRingBuffer * buf, */ if (FAILED (hr)) { DWORD extra_flags = 0; - if (self->device_class == GST_WASAPI2_CLIENT_DEVICE_CLASS_LOOPBACK_CAPTURE) + if (gst_wasapi2_device_class_is_loopback (self->device_class)) extra_flags = AUDCLNT_STREAMFLAGS_LOOPBACK; hr = gst_wasapi2_ring_buffer_initialize_audio_client (self, client_handle, - mix_format, &period, extra_flags); + mix_format, &period, extra_flags, self->device_class); } if (!gst_wasapi2_result (hr)) { @@ -1090,25 +1117,24 @@ gst_wasapi2_ring_buffer_acquire (GstAudioRingBuffer * buf, hr = client_handle->GetService (IID_PPV_ARGS (&audio_volume)); if (!gst_wasapi2_result (hr)) { - GST_ERROR_OBJECT (self, "ISimpleAudioVolume is unavailable"); - goto error; - } - - g_mutex_lock (&self->volume_lock); - self->volume_object = audio_volume.Detach (); - - if (self->mute_changed) { - self->volume_object->SetMute (self->mute, nullptr); - self->mute_changed = FALSE; + GST_WARNING_OBJECT (self, "ISimpleAudioVolume is unavailable"); } else { - self->volume_object->SetMute (FALSE, nullptr); - } + g_mutex_lock (&self->volume_lock); + self->volume_object = audio_volume.Detach (); - if (self->volume_changed) { - self->volume_object->SetMasterVolume (self->volume, nullptr); - self->volume_changed = FALSE; + if (self->mute_changed) { + self->volume_object->SetMute (self->mute, nullptr); + self->mute_changed = FALSE; + } else { + self->volume_object->SetMute (FALSE, nullptr); + } + + if (self->volume_changed) { + self->volume_object->SetMasterVolume (self->volume, nullptr); + self->volume_changed = FALSE; + } + g_mutex_unlock (&self->volume_lock); } - g_mutex_unlock (&self->volume_lock); buf->size = spec->segtotal * spec->segsize; buf->memory = (guint8 *) g_malloc (buf->size); @@ -1327,7 +1353,7 @@ gst_wasapi2_ring_buffer_delay (GstAudioRingBuffer * buf) GstAudioRingBuffer * gst_wasapi2_ring_buffer_new (GstWasapi2ClientDeviceClass device_class, gboolean low_latency, const gchar * device_id, gpointer dispatcher, - const gchar * name) + const gchar * name, guint loopback_target_pid) { GstWasapi2RingBuffer *self; @@ -1343,6 +1369,7 @@ gst_wasapi2_ring_buffer_new (GstWasapi2ClientDeviceClass device_class, self->low_latency = low_latency; self->device_id = g_strdup (device_id); self->dispatcher = dispatcher; + self->loopback_target_pid = loopback_target_pid; return GST_AUDIO_RING_BUFFER_CAST (self); } diff --git a/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2ringbuffer.h b/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2ringbuffer.h index 5bbb6e976e..f8d91d8fe2 100644 --- a/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2ringbuffer.h +++ b/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2ringbuffer.h @@ -34,7 +34,8 @@ GstAudioRingBuffer * gst_wasapi2_ring_buffer_new (GstWasapi2ClientDeviceClass gboolean low_latency, const gchar *device_id, gpointer dispatcher, - const gchar * name); + const gchar * name, + guint loopback_target_pid); GstCaps * gst_wasapi2_ring_buffer_get_caps (GstWasapi2RingBuffer * buf); diff --git a/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2sink.c b/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2sink.c index 67608dcfd8..d7869cbb8f 100644 --- a/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2sink.c +++ b/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2sink.c @@ -333,7 +333,7 @@ gst_wasapi2_sink_create_ringbuffer (GstAudioBaseSink * sink) ringbuffer = gst_wasapi2_ring_buffer_new (GST_WASAPI2_CLIENT_DEVICE_CLASS_RENDER, - self->low_latency, self->device_id, self->dispatcher, name); + self->low_latency, self->device_id, self->dispatcher, name, 0); g_free (name); diff --git a/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2src.c b/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2src.c index 60123eca6a..0b612eb5ef 100644 --- a/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2src.c +++ b/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2src.c @@ -53,10 +53,72 @@ static GstStaticPadTemplate src_template = GST_STATIC_PAD_TEMPLATE ("src", GST_PAD_ALWAYS, GST_STATIC_CAPS (GST_WASAPI2_STATIC_CAPS)); +/** + * GstWasapi2SrcLoopbackMode: + * + * Loopback capture mode + * + * Since: 1.22 + */ +typedef enum +{ + /** + * GstWasapi2SrcLoopbackMode::default: + * + * Default loopback mode + * + * Since: 1.22 + */ + GST_WASAPI2_SRC_LOOPBACK_DEFAULT, + + /** + * GstWasapi2SrcLoopbackMode::include-process-tree: + * + * Captures only specified process and its child process + * + * Since: 1.22 + */ + GST_WASAPI2_SRC_LOOPBACK_INCLUDE_PROCESS_TREE, + + /** + * GstWasapi2SrcLoopbackMode::exclude-process-tree: + * + * Excludes specified process and its child process + * + * Since: 1.22 + */ + GST_WASAPI2_SRC_LOOPBACK_EXCLUDE_PROCESS_TREE, +} GstWasapi2SrcLoopbackMode; + +#define GST_TYPE_WASAPI2_SRC_LOOPBACK_MODE (gst_wasapi2_src_loopback_mode_get_type ()) +static GType +gst_wasapi2_src_loopback_mode_get_type (void) +{ + static GType loopback_type = 0; + static const GEnumValue types[] = { + {GST_WASAPI2_SRC_LOOPBACK_DEFAULT, "Default", "default"}, + {GST_WASAPI2_SRC_LOOPBACK_INCLUDE_PROCESS_TREE, + "Include process and its child processes", + "include-process-tree"}, + {GST_WASAPI2_SRC_LOOPBACK_EXCLUDE_PROCESS_TREE, + "Exclude process and its child processes", + "exclude-process-tree"}, + {0, NULL, NULL} + }; + + if (g_once_init_enter (&loopback_type)) { + GType gtype = g_enum_register_static ("GstWasapi2SrcLoopbackMode", types); + g_once_init_leave (&loopback_type, gtype); + } + + return loopback_type; +} + #define DEFAULT_LOW_LATENCY FALSE #define DEFAULT_MUTE FALSE #define DEFAULT_VOLUME 1.0 #define DEFAULT_LOOPBACK FALSE +#define DEFAULT_LOOPBACK_MODE GST_WASAPI2_SRC_LOOPBACK_DEFAULT enum { @@ -67,6 +129,8 @@ enum PROP_VOLUME, PROP_DISPATCHER, PROP_LOOPBACK, + PROP_LOOPBACK_MODE, + PROP_LOOPBACK_TARGET_PID, }; struct _GstWasapi2Src @@ -80,6 +144,8 @@ struct _GstWasapi2Src gdouble volume; gpointer dispatcher; gboolean loopback; + GstWasapi2SrcLoopbackMode loopback_mode; + guint loopback_pid; gboolean mute_changed; gboolean volume_changed; @@ -173,6 +239,41 @@ gst_wasapi2_src_class_init (GstWasapi2SrcClass * klass) GST_PARAM_MUTABLE_READY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + if (gst_wasapi2_can_process_loopback ()) { + /** + * GstWasapi2Src:loopback-mode: + * + * Loopback mode. "target-process-id" must be specified in case of + * process loopback modes. + * + * This feature requires "Windows 10 build 20348" + * + * Since: 1.22 + */ + g_object_class_install_property (gobject_class, PROP_LOOPBACK_MODE, + g_param_spec_enum ("loopback-mode", "Loopback Mode", + "Loopback mode to use", GST_TYPE_WASAPI2_SRC_LOOPBACK_MODE, + DEFAULT_LOOPBACK_MODE, + GST_PARAM_CONDITIONALLY_AVAILABLE | GST_PARAM_MUTABLE_READY | + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + /** + * GstWasapi2Src:loopback-target-pid: + * + * Target process id to be recorded or excluded depending on loopback mode + * + * This feature requires "Windows 10 build 20348" + * + * Since: 1.22 + */ + g_object_class_install_property (gobject_class, PROP_LOOPBACK_TARGET_PID, + g_param_spec_uint ("loopback-target-pid", "Loopback Target PID", + "Process ID to be recorded or excluded for process loopback mode", + 0, G_MAXUINT32, 0, + GST_PARAM_CONDITIONALLY_AVAILABLE | GST_PARAM_MUTABLE_READY | + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + } + gst_element_class_add_static_pad_template (element_class, &src_template); gst_element_class_set_static_metadata (element_class, "Wasapi2Src", "Source/Audio/Hardware", @@ -191,6 +292,9 @@ gst_wasapi2_src_class_init (GstWasapi2SrcClass * klass) GST_DEBUG_CATEGORY_INIT (gst_wasapi2_src_debug, "wasapi2src", 0, "Windows audio session API source"); + + if (gst_wasapi2_can_process_loopback ()) + gst_type_mark_as_plugin_api (GST_TYPE_WASAPI2_SRC_LOOPBACK_MODE, 0); } static void @@ -238,6 +342,12 @@ gst_wasapi2_src_set_property (GObject * object, guint prop_id, case PROP_LOOPBACK: self->loopback = g_value_get_boolean (value); break; + case PROP_LOOPBACK_MODE: + self->loopback_mode = g_value_get_enum (value); + break; + case PROP_LOOPBACK_TARGET_PID: + self->loopback_pid = g_value_get_uint (value); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -266,6 +376,12 @@ gst_wasapi2_src_get_property (GObject * object, guint prop_id, case PROP_LOOPBACK: g_value_set_boolean (value, self->loopback); break; + case PROP_LOOPBACK_MODE: + g_value_set_enum (value, self->loopback_mode); + break; + case PROP_LOOPBACK_TARGET_PID: + g_value_set_uint (value, self->loopback_pid); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -350,14 +466,27 @@ gst_wasapi2_src_create_ringbuffer (GstAudioBaseSrc * src) GstWasapi2ClientDeviceClass device_class = GST_WASAPI2_CLIENT_DEVICE_CLASS_CAPTURE; - if (self->loopback) + if (self->loopback_pid) { + if (self->loopback_mode == GST_WASAPI2_SRC_LOOPBACK_INCLUDE_PROCESS_TREE) { + device_class = + GST_WASAPI2_CLIENT_DEVICE_CLASS_INCLUDE_PROCESS_LOOPBACK_CAPTURE; + } else if (self->loopback_mode == + GST_WASAPI2_SRC_LOOPBACK_EXCLUDE_PROCESS_TREE) { + device_class = + GST_WASAPI2_CLIENT_DEVICE_CLASS_EXCLUDE_PROCESS_LOOPBACK_CAPTURE; + } + } else if (self->loopback) { device_class = GST_WASAPI2_CLIENT_DEVICE_CLASS_LOOPBACK_CAPTURE; + } + + GST_DEBUG_OBJECT (self, "Device class %d", device_class); name = g_strdup_printf ("%s-ringbuffer", GST_OBJECT_NAME (src)); ringbuffer = gst_wasapi2_ring_buffer_new (device_class, - self->low_latency, self->device_id, self->dispatcher, name); + self->low_latency, self->device_id, self->dispatcher, name, + self->loopback_pid); g_free (name); return ringbuffer; diff --git a/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2util.c b/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2util.c index fe666f4289..d1e2e8bed1 100644 --- a/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2util.c +++ b/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2util.c @@ -490,3 +490,73 @@ gst_wasapi2_can_automatic_stream_routing (void) return ret; #endif } + +gboolean +gst_wasapi2_can_process_loopback (void) +{ +#ifdef GST_WASAPI2_WINAPI_ONLY_APP + /* FIXME: Needs WinRT (Windows.System.Profile) API call + * for OS version check */ + return FALSE; +#else + static gboolean ret = FALSE; + static gsize version_once = 0; + + if (g_once_init_enter (&version_once)) { + OSVERSIONINFOEXW osverinfo; + typedef NTSTATUS (WINAPI fRtlGetVersion) (PRTL_OSVERSIONINFOEXW); + fRtlGetVersion *RtlGetVersion = NULL; + HMODULE hmodule = NULL; + + memset (&osverinfo, 0, sizeof (OSVERSIONINFOEXW)); + osverinfo.dwOSVersionInfoSize = sizeof (OSVERSIONINFOEXW); + + hmodule = LoadLibraryW (L"ntdll.dll"); + if (hmodule) + RtlGetVersion = + (fRtlGetVersion *) GetProcAddress (hmodule, "RtlGetVersion"); + + if (RtlGetVersion) { + RtlGetVersion (&osverinfo); + + /* Process loopback requires Windows 10 build 20348 + * https://learn.microsoft.com/en-us/windows/win32/api/audioclientactivationparams/ns-audioclientactivationparams-audioclient_process_loopback_params + * + * Note: "Windows 10 build 20348" would mean "Windows server 2022" or + * "Windows 11", since build number of "Windows 10 version 21H2" is + * still 19044.XXX + */ + if (osverinfo.dwMajorVersion > 10 || + (osverinfo.dwMajorVersion == 10 && osverinfo.dwBuildNumber >= 20348)) + ret = TRUE; + } + + if (hmodule) + FreeLibrary (hmodule); + + g_once_init_leave (&version_once, 1); + } + + GST_INFO ("Process loopback support: %d", ret); + + return ret; +#endif +} + +WAVEFORMATEX * +gst_wasapi2_get_default_mix_format (void) +{ + WAVEFORMATEX *format; + + /* virtual loopback device might not provide mix format. Create our default + * mix format */ + format = CoTaskMemAlloc (sizeof (WAVEFORMATEX)); + format->wFormatTag = WAVE_FORMAT_PCM; + format->nChannels = 2; + format->nSamplesPerSec = 44100; + format->wBitsPerSample = 16; + format->nBlockAlign = format->nChannels * format->wBitsPerSample / 8; + format->nAvgBytesPerSec = format->nSamplesPerSec * format->nBlockAlign; + + return format; +} diff --git a/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2util.h b/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2util.h index f824f2b0ab..91c8fee5a4 100644 --- a/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2util.h +++ b/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2util.h @@ -65,6 +65,10 @@ gchar * gst_wasapi2_util_get_error_message (HRESULT hr); gboolean gst_wasapi2_can_automatic_stream_routing (void); +gboolean gst_wasapi2_can_process_loopback (void); + +WAVEFORMATEX * gst_wasapi2_get_default_mix_format (void); + G_END_DECLS #endif /* __GST_WASAPI_UTIL_H__ */