wasapi2: Add support for process loopback capture

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>
This commit is contained in:
Seungha Yang 2022-10-17 00:40:46 +09:00 committed by GStreamer Marge Bot
parent 2c3f9d4587
commit cb7958e710
10 changed files with 491 additions and 73 deletions

View file

@ -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": {},

View file

@ -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<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;
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 */

View file

@ -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);

View file

@ -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;

View file

@ -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);
}

View file

@ -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);

View file

@ -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);

View file

@ -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;

View file

@ -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;
}

View file

@ -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__ */