wasapi2: Add support for process loopback capture
authorSeungha Yang <seungha@centricular.com>
Sun, 16 Oct 2022 15:40:46 +0000 (00:40 +0900)
committerGStreamer Marge Bot <gitlab-merge-bot@gstreamer-foundation.org>
Mon, 17 Oct 2022 23:28:48 +0000 (23:28 +0000)
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>

subprojects/gst-plugins-bad/docs/plugins/gst_plugins_cache.json
subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2client.cpp
subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2client.h
subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2device.c
subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2ringbuffer.cpp
subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2ringbuffer.h
subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2sink.c
subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2src.c
subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2util.c
subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2util.h

index d95644b..5f7e7d7 100644 (file)
                         "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": {},
index f8a4220..b2d1ac8 100644 (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;
-
-      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);
@@ -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 */
index ec1d14d..4c2ee21 100644 (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);
index f259c94..0d70801 100644 (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;
index 60231e3..d1d4f9b 100644 (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,23 +872,35 @@ 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;
-  }
-
-  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");
@@ -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);
 }
index 5bbb6e9..f8d91d8 100644 (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);
 
index 67608dc..d7869cb 100644 (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);
 
index 60123ec..0b612eb 100644 (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;
index fe666f4..d1e2e8b 100644 (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;
+}
index f824f2b..91c8fee 100644 (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__ */