ext: add libmicrodns-based mdns device provider
authorMathieu Duponchelle <mathieu@centricular.com>
Mon, 16 Sep 2019 23:52:54 +0000 (01:52 +0200)
committerMathieu Duponchelle <mduponchelle1@gmail.com>
Tue, 8 Oct 2019 12:28:01 +0000 (12:28 +0000)
The provider for now only detects and handles rtsp devices, but
more protocols should be easy to add.

ext/mdns/gstmicrodns.c [new file with mode: 0644]
ext/mdns/gstmicrodnsdevice.c [new file with mode: 0644]
ext/mdns/gstmicrodnsdevice.h [new file with mode: 0644]
ext/mdns/meson.build [new file with mode: 0644]
ext/meson.build
meson_options.txt

diff --git a/ext/mdns/gstmicrodns.c b/ext/mdns/gstmicrodns.c
new file mode 100644 (file)
index 0000000..f4cf35f
--- /dev/null
@@ -0,0 +1,40 @@
+/* GStreamer
+ * Copyright (C) 2019 Mathieu Duponchelle <mathieu@centricular.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "gstmicrodnsdevice.h"
+
+static gboolean
+plugin_init (GstPlugin * plugin)
+{
+  if (!gst_device_provider_register (plugin, "microdnsdeviceprovider",
+          GST_RANK_PRIMARY, GST_TYPE_MDNS_DEVICE_PROVIDER))
+    return FALSE;
+
+  return TRUE;
+}
+
+GST_PLUGIN_DEFINE (GST_VERSION_MAJOR,
+    GST_VERSION_MINOR,
+    microdns,
+    "libmicrodns plugin library",
+    plugin_init, VERSION, "LGPL", GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN)
diff --git a/ext/mdns/gstmicrodnsdevice.c b/ext/mdns/gstmicrodnsdevice.c
new file mode 100644 (file)
index 0000000..180cd82
--- /dev/null
@@ -0,0 +1,430 @@
+/* GStreamer
+ * Copyright (C) 2019 Mathieu Duponchelle <mathieu@centricular.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#include <microdns/microdns.h>
+
+#include "gstmicrodnsdevice.h"
+
+typedef struct _ListenerContext ListenerContext;
+
+struct _GstMDNSDeviceProvider
+{
+  GstDeviceProvider parent;
+  ListenerContext *current_ctx;
+};
+
+#define LISTEN_INTERVAL_SECONDS 2
+
+/* #GstDeviceProvider.stop() is synchronous, but libmicrodns' stop mechanism
+ * isn't, as it polls and queries the application's provided stop function
+ * before each new loop crank. This means there can potentially exist N
+ * contexts at any given time, if the provider is started and stopped in
+ * rapid succession. At most one of them can be active however (stop == false),
+ * with the other N - 1 in the process of stopping (stop == true).
+ *
+ * Additionally, mdns_listen() is a blocking call, thus the need to run it in
+ * its own thread.
+ */
+struct _ListenerContext
+{
+  GMutex lock;
+  GstDeviceProvider *provider;
+
+  /* The following fields are protected by @lock */
+  bool stop;
+  GHashTable *devices;
+  GSequence *last_seen_devices;
+};
+
+G_DEFINE_TYPE (GstMDNSDeviceProvider, gst_mdns_device_provider,
+    GST_TYPE_DEVICE_PROVIDER);
+
+struct _GstMDNSDevice
+{
+  GstDevice parent;
+
+  GstURIType uri_type;
+  gchar *uri;
+  GSequenceIter *iter;
+  gint64 last_seen;
+};
+
+G_DEFINE_TYPE (GstMDNSDevice, gst_mdns_device, GST_TYPE_DEVICE);
+
+static gint
+cmp_last_seen (GstMDNSDevice * a, GstMDNSDevice * b,
+    gpointer G_GNUC_UNUSED user_data)
+{
+  if (a->last_seen < b->last_seen)
+    return -1;
+  if (a->last_seen == b->last_seen)
+    return 0;
+  return 1;
+}
+
+static gint
+cmp_last_seen_iter (GSequenceIter * ia, GSequenceIter * ib, gpointer user_data)
+{
+  return cmp_last_seen (GST_MDNS_DEVICE (g_sequence_get (ia)),
+      GST_MDNS_DEVICE (g_sequence_get (ib)), user_data);
+}
+
+static void
+gst_mdns_device_finalize (GObject * object)
+{
+  GstMDNSDevice *self = GST_MDNS_DEVICE (object);
+
+  g_free (self->uri);
+
+  G_OBJECT_CLASS (gst_mdns_device_parent_class)->finalize (object);
+}
+
+static GstElement *
+gst_mdns_device_create_element (GstDevice * device, const gchar * name)
+{
+  GstMDNSDevice *self = GST_MDNS_DEVICE (device);
+  GstElement *ret;
+  GError *err = NULL;
+
+  ret = gst_element_make_from_uri (self->uri_type, self->uri, name, &err);
+
+  if (!ret) {
+    GST_ERROR_OBJECT (self, "Failed to create element for URI %s: %s",
+        self->uri, err->message);
+    g_clear_error (&err);
+  }
+
+  return ret;
+}
+
+static void
+gst_mdns_device_init (GstMDNSDevice * self)
+{
+}
+
+static void
+gst_mdns_device_class_init (GstMDNSDeviceClass * klass)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+  GstDeviceClass *gst_device_class = GST_DEVICE_CLASS (klass);
+
+  gobject_class->finalize = GST_DEBUG_FUNCPTR (gst_mdns_device_finalize);
+
+  gst_device_class->create_element =
+      GST_DEBUG_FUNCPTR (gst_mdns_device_create_element);
+}
+
+/* Slightly unoptimized, ideally add gst_element_factory_from_uri */
+static GstElementFactory *
+get_factory_for_uri (GstURIType type, const gchar * uri)
+{
+  GError *err = NULL;
+  GstElementFactory *ret = NULL;
+  GstElement *elem = gst_element_make_from_uri (type, uri, NULL, &err);
+
+  if (!elem) {
+    GST_LOG ("Failed to make element from uri: %s", err->message);
+    g_clear_error (&err);
+    goto done;
+  }
+
+  ret = gst_element_get_factory (elem);
+
+  gst_object_unref (elem);
+
+done:
+  return ret;
+}
+
+static GstDevice *
+gst_mdns_device_new (GstElementFactory * factory, const gchar * name,
+    const gchar * uri)
+{
+  GstDevice *ret = NULL;
+  const GList *templates;
+  GstCaps *caps;
+
+  templates = gst_element_factory_get_static_pad_templates (factory);
+  caps = gst_static_pad_template_get_caps ((GstStaticPadTemplate *)
+      templates->data);
+
+  ret = GST_DEVICE (g_object_new (GST_TYPE_MDNS_DEVICE,
+          "display-name", name,
+          "device-class", gst_element_factory_get_metadata (factory, "klass"),
+          "caps", caps, NULL));
+
+  GST_MDNS_DEVICE (ret)->uri = g_strdup (uri);
+  GST_MDNS_DEVICE (ret)->uri_type = gst_element_factory_get_uri_type (factory);
+
+  gst_caps_unref (caps);
+
+  return ret;
+}
+
+static void
+remove_old_devices (ListenerContext * ctx)
+{
+  GstMDNSDeviceProvider *self = GST_MDNS_DEVICE_PROVIDER (ctx->provider);
+  GstClockTime now = g_get_monotonic_time ();
+  GSequenceIter *iter = g_sequence_get_begin_iter (ctx->last_seen_devices);
+
+  while (!g_sequence_iter_is_end (iter)) {
+    GstMDNSDevice *dev = GST_MDNS_DEVICE (g_sequence_get (iter));
+    GstClockTime age = now - dev->last_seen;
+
+    GST_LOG_OBJECT (self,
+        "Device %" GST_PTR_FORMAT " last seen %" GST_TIME_FORMAT " ago", dev,
+        GST_TIME_ARGS (age));
+
+    if (age > 4 * LISTEN_INTERVAL_SECONDS * G_USEC_PER_SEC) {
+      GSequenceIter *next = g_sequence_iter_next (iter);
+
+      GST_INFO_OBJECT (self, "Removing device %" GST_PTR_FORMAT, dev);
+
+      gst_device_provider_device_remove (ctx->provider, GST_DEVICE (dev));
+      g_hash_table_remove (ctx->devices, dev->uri);
+      g_sequence_remove (iter);
+      iter = next;
+    } else {
+      GST_LOG_OBJECT (self, "Keeping device %" GST_PTR_FORMAT, dev);
+      iter = g_sequence_get_end_iter (ctx->last_seen_devices);
+    }
+  }
+}
+
+static bool
+stop (void *p_cookie)
+{
+  bool ret;
+  ListenerContext *ctx = (ListenerContext *) p_cookie;
+
+  g_mutex_lock (&ctx->lock);
+  ret = ctx->stop;
+
+  if (!ctx->stop) {
+    remove_old_devices (ctx);
+  }
+
+  g_mutex_unlock (&ctx->lock);
+
+  return ret;
+}
+
+static void
+callback (void *p_cookie, gint status, const struct rr_entry *entry)
+{
+  ListenerContext *ctx = (ListenerContext *) p_cookie;
+  gchar err[128];
+  const struct rr_entry *tmp;
+  GHashTable *srvs = g_hash_table_new (g_str_hash, g_str_equal);
+  GstMDNSDeviceProvider *self = GST_MDNS_DEVICE_PROVIDER (ctx->provider);
+
+  g_mutex_lock (&ctx->lock);
+
+  if (ctx->stop)
+    goto done;
+
+  GST_DEBUG_OBJECT (self, "received new entries");
+
+  if (status < 0) {
+    mdns_strerror (status, err, sizeof (err));
+    GST_ERROR ("MDNS error: %s", err);
+    goto done;
+  }
+
+  for (tmp = entry; tmp; tmp = tmp->next) {
+    if (tmp->type == RR_SRV) {
+      g_hash_table_insert (srvs, (gpointer) tmp->name, (gpointer) tmp);
+    }
+  }
+
+  for (tmp = entry; tmp; tmp = tmp->next) {
+    if (tmp->type == RR_TXT) {
+      const struct rr_entry *srv;
+
+      srv = (const struct rr_entry *) g_hash_table_lookup (srvs, tmp->name);
+
+      if (!srv) {
+        GST_LOG_OBJECT (self, "No SRV associated with TXT entry for %s",
+            tmp->name);
+        continue;
+      }
+
+      if (g_str_has_suffix (tmp->name, "._rtsp._tcp.local")) {
+        gchar *path = NULL;
+        gchar *uri;
+        struct rr_data_txt *tmp_txt;
+        GstMDNSDevice *dev;
+
+        for (tmp_txt = tmp->data.TXT; tmp_txt; tmp_txt = tmp_txt->next) {
+          if (g_str_has_prefix (tmp_txt->txt, "path=")) {
+            path = tmp_txt->txt + 5;
+          }
+        }
+
+        if (path) {
+          uri =
+              g_strdup_printf ("rtsp://%s:%d/%s", srv->data.SRV.target,
+              srv->data.SRV.port, path);
+        } else {
+          uri =
+              g_strdup_printf ("rtsp://%s:%d", srv->data.SRV.target,
+              srv->data.SRV.port);
+        }
+
+        dev = GST_MDNS_DEVICE (g_hash_table_lookup (ctx->devices, uri));
+
+        GST_LOG_OBJECT (self, "Saw device at uri %s", uri);
+
+        if (dev) {
+          dev->last_seen = g_get_monotonic_time ();
+          GST_LOG_OBJECT (self,
+              "updating last_seen for device %" GST_PTR_FORMAT ": %"
+              G_GINT64_FORMAT, dev, dev->last_seen);
+          g_sequence_sort_changed_iter (dev->iter, cmp_last_seen_iter, NULL);
+        } else {
+          GstElementFactory *factory;
+          gchar *display_name;
+
+          if (!(factory = get_factory_for_uri (GST_URI_SRC, uri))) {
+            GST_LOG_OBJECT (self,
+                "Not registering device %s as no compatible factory was found",
+                tmp->name);
+            goto done;
+          }
+
+          display_name = g_strndup (tmp->name, strlen (tmp->name) - 17);
+          dev =
+              GST_MDNS_DEVICE (gst_mdns_device_new (factory, display_name,
+                  uri));
+          g_free (display_name);
+          dev->last_seen = g_get_monotonic_time ();
+          GST_INFO_OBJECT (self,
+              "Saw new device %" GST_PTR_FORMAT " at %" G_GINT64_FORMAT
+              " with factory %" GST_PTR_FORMAT, dev, dev->last_seen, factory);
+          dev->iter =
+              g_sequence_insert_sorted (ctx->last_seen_devices, (gpointer) dev,
+              (GCompareDataFunc) cmp_last_seen, NULL);
+          g_hash_table_insert (ctx->devices, g_strdup (uri),
+              gst_object_ref (dev));
+          gst_device_provider_device_add (ctx->provider, GST_DEVICE (dev));
+        }
+
+        g_free (uri);
+      } else {
+        GST_LOG_OBJECT (self, "unknown protocol for %s", tmp->name);
+        continue;
+      }
+    }
+  }
+
+done:
+  g_hash_table_unref (srvs);
+  g_mutex_unlock (&ctx->lock);
+}
+
+static gpointer
+_listen (ListenerContext * ctx)
+{
+  gint r = 0;
+  gchar err[128];
+  struct mdns_ctx *mctx;
+  const gchar *ppsz_names[] = { "_rtsp._tcp.local" };
+  gint i_nb_names = 1;
+
+  if ((r = mdns_init (&mctx, MDNS_ADDR_IPV4, MDNS_PORT)) < 0)
+    goto err;
+
+  GST_INFO_OBJECT (ctx->provider, "Start listening");
+
+  if ((r = mdns_listen (mctx, ppsz_names, i_nb_names, RR_PTR,
+              LISTEN_INTERVAL_SECONDS, stop, callback, ctx)) < 0) {
+    mdns_destroy (mctx);
+    goto err;
+  }
+
+done:
+  GST_INFO_OBJECT (ctx->provider, "Done listening");
+
+  g_sequence_free (ctx->last_seen_devices);
+  g_hash_table_unref (ctx->devices);
+  g_mutex_clear (&ctx->lock);
+  g_free (ctx);
+
+  return NULL;
+
+err:
+  if (r < 0) {
+    mdns_strerror (r, err, sizeof (err));
+    GST_ERROR ("MDNS error: %s", err);
+  }
+
+  goto done;
+}
+
+static gboolean
+gst_mdns_device_provider_start (GstDeviceProvider * provider)
+{
+  GstMDNSDeviceProvider *self = GST_MDNS_DEVICE_PROVIDER (provider);
+  ListenerContext *ctx = g_new0 (ListenerContext, 1);
+
+  g_mutex_init (&ctx->lock);
+  ctx->provider = provider;
+  ctx->devices =
+      g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
+  ctx->last_seen_devices = g_sequence_new (NULL);
+  self->current_ctx = ctx;
+
+  g_thread_new (NULL, (GThreadFunc) _listen, ctx);
+
+  return TRUE;
+}
+
+static void
+gst_mdns_device_provider_stop (GstDeviceProvider * provider)
+{
+  GstMDNSDeviceProvider *self = GST_MDNS_DEVICE_PROVIDER (provider);
+
+  g_assert (self->current_ctx);
+
+  g_mutex_lock (&self->current_ctx->lock);
+  self->current_ctx->stop = true;
+  g_mutex_unlock (&self->current_ctx->lock);
+
+  self->current_ctx = NULL;
+}
+
+static void
+gst_mdns_device_provider_init (GstMDNSDeviceProvider * self)
+{
+}
+
+static void
+gst_mdns_device_provider_class_init (GstMDNSDeviceProviderClass * klass)
+{
+  GstDeviceProviderClass *dm_class = GST_DEVICE_PROVIDER_CLASS (klass);
+
+  dm_class->start = GST_DEBUG_FUNCPTR (gst_mdns_device_provider_start);
+  dm_class->stop = GST_DEBUG_FUNCPTR (gst_mdns_device_provider_stop);
+
+  gst_device_provider_class_set_static_metadata (dm_class,
+      "MDNS Device Provider", "Source/Network",
+      "List and provides MDNS-advertised source devices",
+      "Mathieu Duponchelle <mathieu@centricular.com>");
+}
diff --git a/ext/mdns/gstmicrodnsdevice.h b/ext/mdns/gstmicrodnsdevice.h
new file mode 100644 (file)
index 0000000..d80f2cd
--- /dev/null
@@ -0,0 +1,39 @@
+/* GStreamer
+ * Copyright (C) 2019 Mathieu Duponchelle <mathieu@centricular.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#ifndef __GST_MICRODNS_DEVICE_H__
+#define __GST_MICRODNS_DEVICE_H__
+
+#include <gst/gst.h>
+
+G_BEGIN_DECLS
+
+#define GST_TYPE_MDNS_DEVICE_PROVIDER gst_mdns_device_provider_get_type()
+
+G_DECLARE_FINAL_TYPE (GstMDNSDeviceProvider, gst_mdns_device_provider, GST,
+    MDNS_DEVICE_PROVIDER, GstDeviceProvider);
+
+#define GST_TYPE_MDNS_DEVICE gst_mdns_device_get_type()
+
+G_DECLARE_FINAL_TYPE (GstMDNSDevice, gst_mdns_device, GST, MDNS_DEVICE,
+    GstDevice);
+
+G_END_DECLS
+
+#endif /* __GST_MICRODNS_DEVICE_H__ */
diff --git a/ext/mdns/meson.build b/ext/mdns/meson.build
new file mode 100644 (file)
index 0000000..2fad76d
--- /dev/null
@@ -0,0 +1,17 @@
+microdns_dep = dependency('microdns', required: get_option('microdns'),
+  fallback: ['libmicrodns', 'mdns_dep'])
+
+if microdns_dep.found()
+  incdirs = [configinc]
+
+  gstmicrodns = library('gstmicrodns',
+    ['gstmicrodns.c', 'gstmicrodnsdevice.c'],
+    c_args : gst_plugins_bad_args,
+    include_directories : incdirs,
+    dependencies : [gst_dep, microdns_dep],
+    install : true,
+    install_dir : plugins_install_dir,
+  )
+  pkgconfig.generate(gstmicrodns, install_dir : plugins_pkgconfig_install_dir)
+  plugins += [gstmicrodns]
+endif
index 1d3320c..cd7c657 100644 (file)
@@ -26,6 +26,7 @@ subdir('ladspa')
 subdir('libde265')
 subdir('libmms')
 subdir('lv2')
+subdir('mdns')
 subdir('modplug')
 subdir('mpeg2enc')
 subdir('mplex')
index ab98016..9388011 100644 (file)
@@ -113,6 +113,7 @@ option('ladspa', type : 'feature', value : 'auto', description : 'LADSPA plugin
 option('libde265', type : 'feature', value : 'auto', description : 'HEVC/H.265 video decoder plugin')
 option('libmms', type : 'feature', value : 'auto', description : 'Microsoft multimedia server network source plugin')
 option('lv2', type : 'feature', value : 'auto', description : 'LV2 audio plugin bridge')
+option('microdns', type : 'feature', value : 'auto', description : 'libmicrodns-based device provider')
 option('modplug', type : 'feature', value : 'auto', description : 'ModPlug audio decoder plugin')
 option('mpeg2enc', type : 'feature', value : 'auto', description : 'mpeg2enc video encoder plugin')
 option('mplex', type : 'feature', value : 'auto', description : 'mplex audio/video multiplexer plugin')