Adding loopback capture mode for specified PID.
Note that this feature requires Windows 10 build 20348
(Windows 11/Windows Server 2022 or later),
and any process loopback related properties will not be exposed
if OS does not support it.
Example launch lines:
* wasapi2src loopback-mode=include-process-tree loopback-target-pid=<PID>
Captures audio generated by an application (specified by PID)
and its child process
* wasapi2src loopback-mode=exclude-process-tree loopback-target-pid=<PID>
Captures desktop audio excluding PID and its child process
Fixes: https://gitlab.freedesktop.org/gstreamer/gstreamer/-/issues/1278
Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/3195>
"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,
},
"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": {},
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);
}
HRESULT
- ActivateDeviceAsync(const std::wstring &device_id)
+ ActivateDeviceAsync(const std::wstring &device_id,
+ GST_AUDIOCLIENT_ACTIVATION_PARAMS * params)
{
ComPtr<IAsyncAction> async_action;
bool run_async = false;
HRESULT hr;
auto work_item = Callback<Implements<RuntimeClassFlags<ClassicCom>,
- IDispatchedHandler, FtmBase>>([this, device_id]{
+ IDispatchedHandler, FtmBase>>([this, device_id, params]{
ComPtr<IActivateAudioInterfaceAsyncOperation> async_op;
HRESULT async_hr = S_OK;
-
- async_hr = ActivateAudioInterfaceAsync (device_id.c_str (),
- __uuidof(IAudioClient), nullptr, this, &async_op);
+ 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), &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);
PROP_DEVICE_CLASS,
PROP_DISPATCHER,
PROP_CAN_AUTO_ROUTING,
+ PROP_LOOPBACK_TARGET_PID,
};
#define DEFAULT_DEVICE_INDEX -1
gint device_index;
gpointer dispatcher;
gboolean can_auto_routing;
+ guint target_pid;
IAudioClient *audio_client;
GstWasapiDeviceActivator *activator;
{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}
};
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
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;
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;
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",
/* 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;
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);
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* */
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 */
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);
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);
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;
gdouble volume;
gpointer dispatcher;
gboolean can_auto_routing;
+ guint loopback_target_pid;
GstWasapi2Client *client;
GstWasapi2Client *loopback_client;
}
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;
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);
", 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 */
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:
* 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,
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");
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;
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;
- }
-
- GST_INFO_OBJECT (self, "wasapi2 default period: %" G_GINT64_FORMAT
- ", min period: %" G_GINT64_FORMAT, default_period, min_period);
+ 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;
+ }
- 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);
+ 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);
+ }
if (!gst_wasapi2_result (hr)) {
GST_WARNING_OBJECT (self, "Couldn't initialize audioclient");
}
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");
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;
/* 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
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);
}
*/
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)) {
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);
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;
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);
}
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);
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);
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
{
PROP_VOLUME,
PROP_DISPATCHER,
PROP_LOOPBACK,
+ PROP_LOOPBACK_MODE,
+ PROP_LOOPBACK_TARGET_PID,
};
struct _GstWasapi2Src
gdouble volume;
gpointer dispatcher;
gboolean loopback;
+ GstWasapi2SrcLoopbackMode loopback_mode;
+ guint loopback_pid;
gboolean mute_changed;
gboolean volume_changed;
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",
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
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;
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;
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;
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;
+}
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__ */