jack: Add port-names property to select ports explicitly
authorSeungha Yang <seungha@centricular.com>
Tue, 27 Jul 2021 09:33:18 +0000 (18:33 +0900)
committerSeungha Yang <seungha@centricular.com>
Fri, 30 Jul 2021 06:58:20 +0000 (15:58 +0900)
By this new property, user can select physical port to connect,
and element will pick requested port instead of random ones.
User should provide full port name including "client_name:" prefix.
An example is
jackaudiosrc port-names="system:capture_1,system:capture_3" ! ...
   jackaudiosink port-names="system:playback_2"

In addition to "port-names" property, a new connect type "explicit"
is added so that element can post error message if requested
"port-names" contains invalid port(s).

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-good/-/merge_requests/1037>

docs/gst_plugins_cache.json
ext/jack/gstjack.c
ext/jack/gstjack.h
ext/jack/gstjackaudioclient.c
ext/jack/gstjackaudioclient.h
ext/jack/gstjackaudiosink.c
ext/jack/gstjackaudiosink.h
ext/jack/gstjackaudiosrc.c
ext/jack/gstjackaudiosrc.h

index 44f006d..c23a7b7 100644 (file)
                         "type": "gboolean",
                         "writable": true
                     },
+                    "port-names": {
+                        "blurb": "Comma-separated list of port name including \"client_name:\" prefix",
+                        "conditionally-available": false,
+                        "construct": false,
+                        "construct-only": false,
+                        "controllable": false,
+                        "default": "NULL",
+                        "mutable": "ready",
+                        "readable": true,
+                        "type": "gchararray",
+                        "writable": true
+                    },
                     "port-pattern": {
                         "blurb": "A pattern to select which ports to connect to (NULL = first physical ports)",
                         "conditionally-available": false,
                         "type": "gboolean",
                         "writable": true
                     },
+                    "port-names": {
+                        "blurb": "Comma-separated list of port name including \"client_name:\" prefix",
+                        "conditionally-available": false,
+                        "construct": false,
+                        "construct-only": false,
+                        "controllable": false,
+                        "default": "NULL",
+                        "mutable": "ready",
+                        "readable": true,
+                        "type": "gchararray",
+                        "writable": true
+                    },
                     "port-pattern": {
                         "blurb": "A pattern to select which ports to connect to (NULL = first physical ports)",
                         "conditionally-available": false,
                         "desc": "Automatically connect ports to as many physical ports as possible",
                         "name": "auto-forced",
                         "value": "2"
+                    },
+                    {
+                        "desc": "Connect ports to explicitly requested physical ports",
+                        "name": "explicit",
+                        "value": "3"
                     }
                 ]
             },
index 06c3ad0..59682bb 100644 (file)
@@ -37,6 +37,9 @@ gst_jack_connect_get_type (void)
       {GST_JACK_CONNECT_AUTO_FORCED,
             "Automatically connect ports to as many physical ports as possible",
           "auto-forced"},
+      {GST_JACK_CONNECT_EXPLICIT,
+            "Connect ports to explicitly requested physical ports",
+          "explicit"},
       {0, NULL, NULL},
     };
     GType tmp = g_enum_register_static ("GstJackConnect", jack_connect_enums);
index ad238fd..84693f6 100644 (file)
@@ -45,7 +45,17 @@ GST_ELEMENT_REGISTER_DECLARE (jackaudiosink);
 typedef enum {
   GST_JACK_CONNECT_NONE,
   GST_JACK_CONNECT_AUTO,
-  GST_JACK_CONNECT_AUTO_FORCED
+  GST_JACK_CONNECT_AUTO_FORCED,
+
+  /**
+   * GstJackConnect::explicit
+   *
+   * In this mode, the element will try to connect to explicitly requested
+   * port specified by "port-names".
+   *
+   * Since: 1.20
+   */
+  GST_JACK_CONNECT_EXPLICIT,
 } GstJackConnect;
 
 /**
index 3d0dbc1..5b483a8 100644 (file)
@@ -635,3 +635,54 @@ gst_jack_audio_client_get_transport_state (GstJackAudioClient * client)
   client->conn->transport_state = GST_STATE_VOID_PENDING;
   return state;
 }
+
+/**
+ * gst_jack_audio_client_get_port_names_from_string:
+ * @jclient: a jack_client_t handle
+ * @port_names: comma-separated jack port name(s)
+ * @port_flags: JackPortFlags
+ *
+ * Returns: a newly-allocated %NULL-terminated array of strings or %NULL
+ *   if @port_names contains invalid port name. Use g_strfreev() to free it.
+ */
+gchar **
+gst_jack_audio_client_get_port_names_from_string (jack_client_t * jclient,
+    const gchar * port_names, gint port_flags)
+{
+  gchar **p = NULL;
+  guint i, len;
+
+  g_return_val_if_fail (jclient != NULL, NULL);
+
+  if (!port_names)
+    return NULL;
+
+  p = g_strsplit (port_names, ",", 0);
+  len = g_strv_length (p);
+
+  if (len < 1)
+    goto invalid;
+
+  for (i = 0; i < len; i++) {
+    jack_port_t *port = jack_port_by_name (jclient, p[i]);
+    int flags;
+
+    if (!port) {
+      GST_WARNING ("Couldn't get jack port by name %s", p[i]);
+      goto invalid;
+    }
+
+    flags = jack_port_flags (port);
+    if ((flags & port_flags) != port_flags) {
+      GST_WARNING ("Port flags 0x%x doesn't match expected flags 0x%x",
+          flags, port_flags);
+      goto invalid;
+    }
+  }
+
+  return p;
+
+invalid:
+  g_strfreev (p);
+  return NULL;
+}
index bf4d93e..455edd2 100644 (file)
@@ -56,6 +56,10 @@ gboolean              gst_jack_audio_client_set_active     (GstJackAudioClient *
 
 GstState              gst_jack_audio_client_get_transport_state (GstJackAudioClient *client);
 
+gchar **              gst_jack_audio_client_get_port_names_from_string (jack_client_t *jclient,
+                                                                        const gchar   *port_names,
+                                                                        gint port_flags);
+
 G_END_DECLS
 
 #endif /* __GST_JACK_AUDIO_CLIENT_H__ */
index ccdca5a..4d18f7a 100644 (file)
@@ -401,7 +401,6 @@ gst_jack_ring_buffer_acquire (GstAudioRingBuffer * buf,
 {
   GstJackAudioSink *sink;
   GstJackRingBuffer *abuf;
-  const char **ports;
   gint sample_rate, buffer_size;
   gint i, rate, bpf, channels, res;
   jack_client_t *client;
@@ -459,18 +458,39 @@ gst_jack_ring_buffer_acquire (GstAudioRingBuffer * buf,
   /* if we need to automatically connect the ports, do so now. We must do this
    * after activating the client. */
   if (sink->connect == GST_JACK_CONNECT_AUTO
-      || sink->connect == GST_JACK_CONNECT_AUTO_FORCED) {
+      || sink->connect == GST_JACK_CONNECT_AUTO_FORCED
+      || sink->connect == GST_JACK_CONNECT_EXPLICIT) {
+    const char **available_ports = NULL;
+    const char **jack_ports = NULL;
+    char **user_ports = NULL;
+
     /* find all the physical input ports. A physical input port is a port
      * associated with a hardware device. Someone needs connect to a physical
      * port in order to hear something. */
-    if (sink->port_pattern == NULL) {
-      ports = jack_get_ports (client, NULL, NULL,
-          JackPortIsPhysical | JackPortIsInput);
-    } else {
-      ports = jack_get_ports (client, sink->port_pattern, NULL,
-          JackPortIsInput);
+    if (sink->port_names) {
+      user_ports = gst_jack_audio_client_get_port_names_from_string (client,
+          sink->port_names, JackPortIsInput);
+
+      if (user_ports)
+        available_ports = (const char **) user_ports;
     }
-    if (ports == NULL) {
+
+    if (!available_ports && sink->connect == GST_JACK_CONNECT_EXPLICIT)
+      goto wrong_port_names;
+
+    if (!available_ports) {
+      if (!sink->port_pattern) {
+        jack_ports = jack_get_ports (client, NULL, NULL,
+            JackPortIsPhysical | JackPortIsInput);
+      } else {
+        jack_ports = jack_get_ports (client, sink->port_pattern, NULL,
+            JackPortIsInput);
+      }
+
+      available_ports = jack_ports;
+    }
+
+    if (!available_ports) {
       /* no ports? fine then we don't do anything except for posting a warning
        * message. */
       GST_ELEMENT_WARNING (sink, RESOURCE, NOT_FOUND, (NULL),
@@ -480,7 +500,7 @@ gst_jack_ring_buffer_acquire (GstAudioRingBuffer * buf,
 
     for (i = 0; i < channels; i++) {
       /* stop when all input ports are exhausted */
-      if (ports[i] == NULL) {
+      if (!available_ports[i]) {
         /* post a warning that we could not connect all ports */
         GST_ELEMENT_WARNING (sink, RESOURCE, NOT_FOUND, (NULL),
             ("No more physical ports, leaving some ports unconnected"));
@@ -489,11 +509,18 @@ gst_jack_ring_buffer_acquire (GstAudioRingBuffer * buf,
       GST_DEBUG_OBJECT (sink, "try connecting to %s",
           jack_port_name (sink->ports[i]));
       /* connect the port to a physical port */
-      res = jack_connect (client, jack_port_name (sink->ports[i]), ports[i]);
-      if (res != 0 && res != EEXIST)
+      res = jack_connect (client,
+          jack_port_name (sink->ports[i]), available_ports[i]);
+      if (res != 0 && res != EEXIST) {
+        jack_free (jack_ports);
+        g_strfreev (user_ports);
+
         goto cannot_connect;
+      }
     }
-    jack_free (ports);
+
+    jack_free (jack_ports);
+    g_strfreev (user_ports);
   }
 done:
 
@@ -528,7 +555,12 @@ cannot_connect:
     GST_ELEMENT_ERROR (sink, RESOURCE, SETTINGS, (NULL),
         ("Could not connect output ports to physical ports (%d:%s)",
             res, g_strerror (res)));
-    jack_free (ports);
+    return FALSE;
+  }
+wrong_port_names:
+  {
+    GST_ELEMENT_ERROR (sink, RESOURCE, SETTINGS, (NULL),
+        ("Invalid port-names was provided"));
     return FALSE;
   }
 }
@@ -699,6 +731,7 @@ enum
   PROP_PORT_PATTERN,
   PROP_TRANSPORT,
   PROP_LOW_LATENCY,
+  PROP_PORT_NAMES,
   PROP_LAST
 };
 
@@ -807,6 +840,19 @@ gst_jack_audio_sink_class_init (GstJackAudioSinkClass * klass)
           GST_PARAM_MUTABLE_READY | G_PARAM_READWRITE |
           G_PARAM_STATIC_STRINGS));
 
+  /**
+   * GstJackAudioSink:port-names:
+   *
+   * Comma-separated list of port name including "client_name:" prefix
+   *
+   * Since: 1.20
+   */
+  g_object_class_install_property (gobject_class, PROP_PORT_NAMES,
+      g_param_spec_string ("port-names", "Port Names",
+          "Comma-separated list of port name including \"client_name:\" prefix",
+          NULL, GST_PARAM_MUTABLE_READY | G_PARAM_READWRITE |
+          G_PARAM_STATIC_STRINGS));
+
   gst_element_class_set_static_metadata (gstelement_class, "Audio Sink (Jack)",
       "Sink/Audio", "Output audio to a JACK server",
       "Wim Taymans <wim.taymans@gmail.com>");
@@ -857,6 +903,8 @@ gst_jack_audio_sink_dispose (GObject * object)
     sink->port_pattern = NULL;
   }
 
+  g_clear_pointer (&sink->port_names, g_free);
+
   G_OBJECT_CLASS (parent_class)->dispose (object);
 }
 
@@ -896,6 +944,10 @@ gst_jack_audio_sink_set_property (GObject * object, guint prop_id,
     case PROP_LOW_LATENCY:
       sink->low_latency = g_value_get_boolean (value);
       break;
+    case PROP_PORT_NAMES:
+      g_free (sink->port_names);
+      sink->port_names = g_value_dup_string (value);
+      break;
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
       break;
@@ -932,6 +984,9 @@ gst_jack_audio_sink_get_property (GObject * object, guint prop_id,
     case PROP_LOW_LATENCY:
       g_value_set_boolean (value, sink->low_latency);
       break;
+    case PROP_PORT_NAMES:
+      g_value_set_string (value, sink->port_names);
+      break;
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
       break;
@@ -950,14 +1005,42 @@ gst_jack_audio_sink_getcaps (GstBaseSink * bsink, GstCaps * filter)
   if (sink->client == NULL)
     goto no_client;
 
+  if (sink->connect == GST_JACK_CONNECT_EXPLICIT && !sink->port_names)
+    goto no_port_names;
+
   client = gst_jack_audio_client_get_client (sink->client);
 
-  if (sink->connect == GST_JACK_CONNECT_AUTO) {
+  if (sink->connect == GST_JACK_CONNECT_AUTO ||
+      sink->connect == GST_JACK_CONNECT_EXPLICIT) {
+    max = 0;
+
+    if (sink->port_names) {
+      gchar **user_ports =
+          gst_jack_audio_client_get_port_names_from_string (client,
+          sink->port_names, JackPortIsInput);
+
+      if (user_ports) {
+        max = g_strv_length (user_ports);
+      } else {
+        GST_ELEMENT_WARNING (sink, RESOURCE, NOT_FOUND,
+            ("Invalid \"port-names\" was requested"),
+            ("Requested \"port-names\" %s contains invalid name",
+                sink->port_names));
+      }
+
+      g_strfreev (user_ports);
+    }
+
+    if (max > 0)
+      goto found;
+
+    if (sink->connect == GST_JACK_CONNECT_EXPLICIT)
+      goto no_port_names;
+
     /* get a port count, this is the number of channels we can automatically
      * connect. */
     ports = jack_get_ports (client, NULL, NULL,
         JackPortIsPhysical | JackPortIsInput);
-    max = 0;
     if (ports != NULL) {
       for (; ports[max]; max++);
       jack_free (ports);
@@ -968,7 +1051,13 @@ gst_jack_audio_sink_getcaps (GstBaseSink * bsink, GstCaps * filter)
      * pads. */
     max = G_MAXINT;
   }
-  min = MIN (1, max);
+
+found:
+  if (sink->connect == GST_JACK_CONNECT_EXPLICIT) {
+    min = max;
+  } else {
+    min = MIN (1, max);
+  }
 
   rate = jack_get_sample_rate (client);
 
@@ -996,6 +1085,13 @@ no_client:
     /* base class will get template caps for us when we return NULL */
     return NULL;
   }
+no_port_names:
+  {
+    GST_ELEMENT_ERROR (sink, RESOURCE, SETTINGS,
+        ("User must provide valid port names"),
+        ("\"port-names\" contains invalid name or NULL string"));
+    return NULL;
+  }
 }
 
 static GstAudioRingBuffer *
index ffba824..088289d 100644 (file)
@@ -56,6 +56,7 @@ struct _GstJackAudioSink {
   gchar           *port_pattern;
   guint            transport;
   gboolean         low_latency;
+  gchar           *port_names;
 
   /* our client */
   GstJackAudioClient *client;
index 11f4f91..40784fa 100644 (file)
@@ -407,7 +407,6 @@ gst_jack_ring_buffer_acquire (GstAudioRingBuffer * buf,
 {
   GstJackAudioSrc *src;
   GstJackRingBuffer *abuf;
-  const char **ports;
   gint sample_rate, buffer_size;
   gint i, bpf, rate, channels, res;
   jack_client_t *client;
@@ -467,20 +466,38 @@ gst_jack_ring_buffer_acquire (GstAudioRingBuffer * buf,
   /* if we need to automatically connect the ports, do so now. We must do this
    * after activating the client. */
   if (src->connect == GST_JACK_CONNECT_AUTO
-      || src->connect == GST_JACK_CONNECT_AUTO_FORCED) {
+      || src->connect == GST_JACK_CONNECT_AUTO_FORCED
+      || src->connect == GST_JACK_CONNECT_EXPLICIT) {
+    const char **available_ports = NULL;
+    const char **jack_ports = NULL;
+    char **user_ports = NULL;
+
     /* find all the physical output ports. A physical output port is a port
      * associated with a hardware device. Someone needs connect to a physical
      * port in order to capture something. */
 
-    if (src->port_pattern == NULL) {
-      ports = jack_get_ports (client, NULL, NULL,
-          JackPortIsPhysical | JackPortIsOutput);
-    } else {
-      ports = jack_get_ports (client, src->port_pattern, NULL,
-          JackPortIsOutput);
+    if (src->port_names) {
+      user_ports = gst_jack_audio_client_get_port_names_from_string (client,
+          src->port_names, JackPortIsOutput);
+
+      if (user_ports)
+        available_ports = (const char **) user_ports;
     }
 
-    if (ports == NULL) {
+    if (!available_ports && src->connect == GST_JACK_CONNECT_EXPLICIT)
+      goto wrong_port_names;
+
+    if (!available_ports) {
+      if (!src->port_pattern) {
+        jack_ports = jack_get_ports (client, NULL, NULL,
+            JackPortIsPhysical | JackPortIsOutput);
+      } else {
+        jack_ports = jack_get_ports (client, src->port_pattern, NULL,
+            JackPortIsOutput);
+      }
+    }
+
+    if (!available_ports) {
       /* no ports? fine then we don't do anything except for posting a warning
        * message. */
       GST_ELEMENT_WARNING (src, RESOURCE, NOT_FOUND, (NULL),
@@ -490,7 +507,7 @@ gst_jack_ring_buffer_acquire (GstAudioRingBuffer * buf,
 
     for (i = 0; i < channels; i++) {
       /* stop when all output ports are exhausted */
-      if (ports[i] == NULL) {
+      if (!available_ports[i]) {
         /* post a warning that we could not connect all ports */
         GST_ELEMENT_WARNING (src, RESOURCE, NOT_FOUND, (NULL),
             ("No more physical ports, leaving some ports unconnected"));
@@ -500,11 +517,18 @@ gst_jack_ring_buffer_acquire (GstAudioRingBuffer * buf,
           jack_port_name (src->ports[i]));
 
       /* connect the physical port to a port */
-      res = jack_connect (client, ports[i], jack_port_name (src->ports[i]));
-      if (res != 0 && res != EEXIST)
+      res = jack_connect (client,
+          available_ports[i], jack_port_name (src->ports[i]));
+      if (res != 0 && res != EEXIST) {
+        jack_free (jack_ports);
+        g_strfreev (user_ports);
+
         goto cannot_connect;
+      }
     }
-    jack_free (ports);
+
+    jack_free (jack_ports);
+    g_strfreev (user_ports);
   }
 done:
 
@@ -539,7 +563,12 @@ cannot_connect:
     GST_ELEMENT_ERROR (src, RESOURCE, SETTINGS, (NULL),
         ("Could not connect input ports to physical ports (%d:%s)",
             res, g_strerror (res)));
-    jack_free (ports);
+    return FALSE;
+  }
+wrong_port_names:
+  {
+    GST_ELEMENT_ERROR (src, RESOURCE, SETTINGS, (NULL),
+        ("Invalid port-names was provided"));
     return FALSE;
   }
 }
@@ -701,6 +730,7 @@ enum
   PROP_PORT_PATTERN,
   PROP_TRANSPORT,
   PROP_LOW_LATENCY,
+  PROP_PORT_NAMES,
   PROP_LAST
 };
 
@@ -825,6 +855,19 @@ gst_jack_audio_src_class_init (GstJackAudioSrcClass * klass)
           GST_PARAM_MUTABLE_READY | G_PARAM_READWRITE |
           G_PARAM_STATIC_STRINGS));
 
+  /**
+   * GstJackAudioSrc:port-names:
+   *
+   * Comma-separated list of port name including "client_name:" prefix
+   *
+   * Since: 1.20
+   */
+  g_object_class_install_property (gobject_class, PROP_PORT_NAMES,
+      g_param_spec_string ("port-names", "Port Names",
+          "Comma-separated list of port name including \"client_name:\" prefix",
+          NULL, GST_PARAM_MUTABLE_READY | G_PARAM_READWRITE |
+          G_PARAM_STATIC_STRINGS));
+
   gst_element_class_add_static_pad_template (gstelement_class, &src_factory);
 
   gst_element_class_set_static_metadata (gstelement_class,
@@ -875,6 +918,8 @@ gst_jack_audio_src_dispose (GObject * object)
     src->port_pattern = NULL;
   }
 
+  g_clear_pointer (&src->port_names, g_free);
+
   G_OBJECT_CLASS (parent_class)->dispose (object);
 }
 
@@ -912,6 +957,10 @@ gst_jack_audio_src_set_property (GObject * object, guint prop_id,
     case PROP_LOW_LATENCY:
       src->low_latency = g_value_get_boolean (value);
       break;
+    case PROP_PORT_NAMES:
+      g_free (src->port_names);
+      src->port_names = g_value_dup_string (value);
+      break;
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
       break;
@@ -946,6 +995,9 @@ gst_jack_audio_src_get_property (GObject * object, guint prop_id,
     case PROP_LOW_LATENCY:
       g_value_set_boolean (value, src->low_latency);
       break;
+    case PROP_PORT_NAMES:
+      g_value_set_string (value, src->port_names);
+      break;
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
       break;
@@ -964,14 +1016,42 @@ gst_jack_audio_src_getcaps (GstBaseSrc * bsrc, GstCaps * filter)
   if (src->client == NULL)
     goto no_client;
 
+  if (src->connect == GST_JACK_CONNECT_EXPLICIT && !src->port_names)
+    goto no_port_names;
+
   client = gst_jack_audio_client_get_client (src->client);
 
-  if (src->connect == GST_JACK_CONNECT_AUTO) {
+  if (src->connect == GST_JACK_CONNECT_AUTO ||
+      src->connect == GST_JACK_CONNECT_EXPLICIT) {
+    max = 0;
+
+    if (src->port_names) {
+      gchar **user_ports =
+          gst_jack_audio_client_get_port_names_from_string (client,
+          src->port_names, JackPortIsOutput);
+
+      if (user_ports) {
+        max = g_strv_length (user_ports);
+      } else {
+        GST_ELEMENT_WARNING (src, RESOURCE, NOT_FOUND,
+            ("Invalid \"port-names\" was requested"),
+            ("Requested \"port-names\" %s contains invalid name",
+                src->port_names));
+      }
+
+      g_strfreev (user_ports);
+    }
+
+    if (max > 0)
+      goto found;
+
+    if (src->connect == GST_JACK_CONNECT_EXPLICIT)
+      goto no_port_names;
+
     /* get a port count, this is the number of channels we can automatically
      * connect. */
     ports = jack_get_ports (client, NULL, NULL,
         JackPortIsPhysical | JackPortIsOutput);
-    max = 0;
     if (ports != NULL) {
       for (; ports[max]; max++);
 
@@ -983,7 +1063,13 @@ gst_jack_audio_src_getcaps (GstBaseSrc * bsrc, GstCaps * filter)
      * pads. */
     max = G_MAXINT;
   }
-  min = MIN (1, max);
+
+found:
+  if (src->connect == GST_JACK_CONNECT_EXPLICIT) {
+    min = max;
+  } else {
+    min = MIN (1, max);
+  }
 
   rate = jack_get_sample_rate (client);
 
@@ -1011,6 +1097,13 @@ no_client:
     /* base class will get template caps for us when we return NULL */
     return NULL;
   }
+no_port_names:
+  {
+    GST_ELEMENT_ERROR (src, RESOURCE, SETTINGS,
+        ("User must provide valid port names"),
+        ("\"port-names\" contains invalid name or NULL string"));
+    return NULL;
+  }
 }
 
 static GstAudioRingBuffer *
index d6b08b9..3657c60 100644 (file)
@@ -73,6 +73,7 @@ struct _GstJackAudioSrc
     gchar           *port_pattern;
     guint            transport;
     gboolean         low_latency;
+    gchar           *port_names;
 
     /* our client */
     GstJackAudioClient *client;