avdtpsrc: Add support for AVRCP metadata
authorArun Raghavan <arun@arunraghavan.net>
Thu, 15 Sep 2016 12:19:15 +0000 (17:49 +0530)
committerArun Raghavan <arun@arunraghavan.net>
Sat, 24 Sep 2016 17:54:28 +0000 (23:24 +0530)
Metadata from AVRCP is emitted as tags, and the duration from AVRCP is
used in queries by avdtpsrc.

sys/bluez/Makefile.am
sys/bluez/gstavdtpsrc.c
sys/bluez/gstavdtpsrc.h
sys/bluez/gstavrcputil.c [new file with mode: 0644]
sys/bluez/gstavrcputil.h [new file with mode: 0644]
sys/bluez/org.bluez.xml

index c6b13db..7823d29 100644 (file)
@@ -5,7 +5,8 @@ libgstbluez_la_SOURCES = \
        gsta2dpsink.c \
        gstavdtpsink.c \
        gstavdtpsrc.c \
-       gstavdtputil.c
+       gstavdtputil.c \
+       gstavrcputil.c
 
 nodist_libgstbluez_la_SOURCES = \
        $(BUILT_SOURCES)
index 00c807a..1c68e45 100644 (file)
@@ -68,6 +68,8 @@ static void gst_avdtp_src_set_property (GObject * object, guint prop_id,
     const GValue * value, GParamSpec * pspec);
 
 static GstCaps *gst_avdtp_src_getcaps (GstBaseSrc * bsrc, GstCaps * filter);
+static gboolean gst_avdtp_src_query (GstPad * pad, GstObject * parent,
+    GstQuery * query);
 static gboolean gst_avdtp_src_start (GstBaseSrc * bsrc);
 static gboolean gst_avdtp_src_stop (GstBaseSrc * bsrc);
 static GstFlowReturn gst_avdtp_src_create (GstBaseSrc * bsrc, guint64 offset,
@@ -117,9 +119,14 @@ gst_avdtp_src_init (GstAvdtpSrc * avdtpsrc)
 {
   avdtpsrc->poll = gst_poll_new (TRUE);
 
+  avdtpsrc->duration = GST_CLOCK_TIME_NONE;
+
   gst_base_src_set_format (GST_BASE_SRC (avdtpsrc), GST_FORMAT_TIME);
   gst_base_src_set_live (GST_BASE_SRC (avdtpsrc), TRUE);
   gst_base_src_set_do_timestamp (GST_BASE_SRC (avdtpsrc), TRUE);
+
+  gst_pad_set_query_function (GST_BASE_SRC_PAD (avdtpsrc),
+      GST_DEBUG_FUNCPTR (gst_avdtp_src_query));
 }
 
 static void
@@ -169,6 +176,35 @@ gst_avdtp_src_set_property (GObject * object, guint prop_id,
   }
 }
 
+static gboolean
+gst_avdtp_src_query (GstPad * pad, GstObject * parent, GstQuery * query)
+{
+  GstAvdtpSrc *avdtpsrc = GST_AVDTP_SRC (gst_pad_get_parent_element (pad));
+  gboolean ret = FALSE;
+
+  switch (GST_QUERY_TYPE (query)) {
+    case GST_QUERY_DURATION:{
+      GstFormat format;
+
+      if (avdtpsrc->duration != GST_CLOCK_TIME_NONE) {
+        gst_query_parse_duration (query, &format, NULL);
+
+        if (format == GST_FORMAT_TIME) {
+          gst_query_set_duration (query, format, (gint64) avdtpsrc->duration);
+          ret = TRUE;
+        }
+      }
+
+      break;
+    }
+
+    default:
+      ret = gst_pad_query_default (pad, parent, query);
+  }
+
+  return ret;
+}
+
 static GstCaps *
 gst_avdtp_src_getcaps (GstBaseSrc * bsrc, GstCaps * filter)
 {
@@ -247,6 +283,54 @@ gst_avdtp_src_getcaps (GstBaseSrc * bsrc, GstCaps * filter)
   return ret;
 }
 
+static void
+avrcp_metadata_cb (GstAvrcpConnection * avrcp, GstTagList * taglist,
+    gpointer user_data)
+{
+  GstAvdtpSrc *src = GST_AVDTP_SRC (user_data);
+  guint64 duration;
+
+  if (gst_tag_list_get_uint64 (taglist, GST_TAG_DURATION, &duration)) {
+    src->duration = duration;
+    gst_element_post_message (GST_ELEMENT (src),
+        gst_message_new_duration_changed (GST_OBJECT (src)));
+  }
+
+  gst_pad_push_event (GST_BASE_SRC_PAD (src),
+      gst_event_new_tag (gst_tag_list_copy (taglist)));
+  gst_element_post_message (GST_ELEMENT (src),
+      gst_message_new_tag (GST_OBJECT (src), taglist));
+}
+
+static void
+gst_avdtp_src_start_avrcp (GstAvdtpSrc * src)
+{
+  gchar *path, **strv;
+  int i;
+
+  /* Strip out the /fdX in /org/bluez/dev_.../fdX */
+  strv = g_strsplit (src->conn.transport, "/", -1);
+
+  for (i = 0; strv[i]; i++);
+  g_return_if_fail (i > 0);
+
+  g_free (strv[i - 1]);
+  strv[i - 1] = NULL;
+
+  path = g_strjoinv ("/", strv);
+  g_strfreev (strv);
+
+  src->avrcp = gst_avrcp_connection_new (path, avrcp_metadata_cb, src, NULL);
+
+  g_free (path);
+}
+
+static void
+gst_avdtp_src_stop_avrcp (GstAvdtpSrc * src)
+{
+  gst_avrcp_connection_free (src->avrcp);
+}
+
 static gboolean
 gst_avdtp_src_start (GstBaseSrc * bsrc)
 {
@@ -291,6 +375,8 @@ gst_avdtp_src_start (GstBaseSrc * bsrc)
 
   g_atomic_int_set (&avdtpsrc->unlocked, FALSE);
 
+  gst_avdtp_src_start_avrcp (avdtpsrc);
+
   return TRUE;
 
 fail:
@@ -306,6 +392,7 @@ gst_avdtp_src_stop (GstBaseSrc * bsrc)
   gst_poll_remove_fd (avdtpsrc->poll, &avdtpsrc->pfd);
   gst_poll_set_flushing (avdtpsrc->poll, TRUE);
 
+  gst_avdtp_src_stop_avrcp (avdtpsrc);
   gst_avdtp_connection_release (&avdtpsrc->conn);
 
   if (avdtpsrc->dev_caps) {
index 67027fa..334ff15 100644 (file)
@@ -27,6 +27,7 @@
 #include <gst/gst.h>
 #include <gst/base/gstbasesrc.h>
 #include "gstavdtputil.h"
+#include "gstavrcputil.h"
 
 G_BEGIN_DECLS
 #define GST_TYPE_AVDTP_SRC \
@@ -54,9 +55,13 @@ struct _GstAvdtpSrc
   GstAvdtpConnection conn;
   GstCaps *dev_caps;
 
+  GstAvrcpConnection *avrcp;
+
   GstPoll *poll;
   GstPollFD pfd;
   volatile gint unlocked;
+
+  GstClockTime duration;
 };
 
 GType gst_avdtp_src_get_type (void);
diff --git a/sys/bluez/gstavrcputil.c b/sys/bluez/gstavrcputil.c
new file mode 100644 (file)
index 0000000..efce32d
--- /dev/null
@@ -0,0 +1,333 @@
+/*
+ * Copyright (C) 2015 Arun Raghavan <git@arunraghavan.net>
+ *
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "gstavrcputil.h"
+#include "bluez.h"
+
+#include <gio/gio.h>
+
+#define BLUEZ_NAME "org.bluez"
+#define BLUEZ_PATH "/"
+#define BLUEZ_MEDIA_PLAYER_IFACE BLUEZ_NAME ".MediaPlayer1"
+
+struct _GstAvrcpConnection
+{
+  GMainContext *context;
+  GMainLoop *mainloop;
+  GThread *thread;
+
+  gchar *dev_path;
+  GDBusObjectManager *manager;
+  BluezMediaPlayer1 *player;
+
+  GstAvrcpMetadataCb cb;
+  gpointer user_data;
+  GDestroyNotify user_data_free_cb;
+};
+
+static const char *
+tag_from_property (const char *name)
+{
+  if (g_str_equal (name, "Title"))
+    return GST_TAG_TITLE;
+  else if (g_str_equal (name, "Artist"))
+    return GST_TAG_ARTIST;
+  else if (g_str_equal (name, "Album"))
+    return GST_TAG_ALBUM;
+  else if (g_str_equal (name, "Genre"))
+    return GST_TAG_GENRE;
+  else if (g_str_equal (name, "NumberOfTracks"))
+    return GST_TAG_TRACK_COUNT;
+  else if (g_str_equal (name, "TrackNumber"))
+    return GST_TAG_TRACK_NUMBER;
+  else if (g_str_equal (name, "Duration"))
+    return GST_TAG_DURATION;
+  else
+    return NULL;
+}
+
+static GstTagList *
+tag_list_from_variant (GVariant * properties, gboolean track)
+{
+  const gchar *name, *s;
+  GVariant *value;
+  GVariantIter *iter;
+  GstTagList *taglist = NULL;
+
+  iter = g_variant_iter_new (properties);
+
+  if (track)
+    taglist = gst_tag_list_new_empty ();
+
+  /* The properties are in two levels -- at the top level we have the position
+   * and the 'track'. The 'track' is another level of {sv} so we recurse one
+   * level to pick up the actual track data. We get the taglist from the
+   * recursive call, and ignore the position for now. */
+
+  while (g_variant_iter_next (iter, "{&sv}", &name, &value)) {
+    if (!track && g_str_equal (name, "Track")) {
+      /* Top level property */
+      taglist = tag_list_from_variant (value, TRUE);
+
+    } else if (track) {
+      /* If we get here, we are in the recursive call and we're dealing with
+       * properties under "Track" */
+      GType type;
+      const gchar *tag;
+      guint i;
+      guint64 i64;
+
+      tag = tag_from_property (name);
+      if (!tag)
+        goto next;
+
+      type = gst_tag_get_type (tag);
+
+      switch (type) {
+        case G_TYPE_STRING:
+          s = g_variant_get_string (value, NULL);
+          if (s && s[0] != '\0')
+            gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, tag, s, NULL);
+          break;
+
+        case G_TYPE_UINT:
+          i = g_variant_get_uint32 (value);
+          if (i > 0)
+            gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, tag, i, NULL);
+          break;
+
+        case G_TYPE_UINT64:
+          /* If we're here, the tag is 'duration' */
+          i64 = g_variant_get_uint32 (value);
+          if (i64 > 0 && i64 != (guint32) (-1)) {
+            gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, tag,
+                i64 * GST_MSECOND, NULL);
+          }
+          break;
+
+        default:
+          GST_WARNING ("Unknown property: %s", name);
+          break;
+      }
+    }
+
+  next:
+    g_variant_unref (value);
+  }
+
+  g_variant_iter_free (iter);
+
+  if (taglist && gst_tag_list_is_empty (taglist)) {
+    gst_tag_list_unref (taglist);
+    taglist = NULL;
+  }
+
+  return taglist;
+}
+
+static void
+player_property_changed_cb (GDBusProxy * proxy, GVariant * properties,
+    GStrv invalid, gpointer user_data)
+{
+  GstAvrcpConnection *avrcp = (GstAvrcpConnection *) user_data;
+  GstTagList *taglist;
+
+  taglist = tag_list_from_variant (properties, FALSE);
+
+  if (taglist)
+    avrcp->cb (avrcp, taglist, avrcp->user_data);
+}
+
+static GstTagList *
+player_get_taglist (BluezMediaPlayer1 * player)
+{
+  GstTagList *taglist = NULL;
+  GVariant *track;
+
+  track = bluez_media_player1_get_track (player);
+  if (track)
+    taglist = tag_list_from_variant (track, TRUE);
+
+  return taglist;
+}
+
+static void
+gst_avrcp_connection_set_player (GstAvrcpConnection * avrcp,
+    BluezMediaPlayer1 * player)
+{
+  GstTagList *taglist;
+
+  if (avrcp->player)
+    g_object_unref (avrcp->player);
+
+  if (!player) {
+    avrcp->player = NULL;
+    return;
+  }
+
+  avrcp->player = g_object_ref (player);
+
+  g_signal_connect (player, "g-properties-changed",
+      G_CALLBACK (player_property_changed_cb), avrcp);
+
+  taglist = player_get_taglist (avrcp->player);
+
+  if (taglist)
+    avrcp->cb (avrcp, taglist, avrcp->user_data);
+}
+
+static BluezMediaPlayer1 *
+media_player_from_dbus_object (GDBusObject * object)
+{
+  return (BluezMediaPlayer1 *) g_dbus_object_get_interface (object,
+      BLUEZ_MEDIA_PLAYER_IFACE);
+}
+
+static GType
+manager_proxy_type_func (GDBusObjectManagerClient * manager,
+    const gchar * object_path, const gchar * interface_name, gpointer user_data)
+{
+  if (!interface_name)
+    return G_TYPE_DBUS_OBJECT_PROXY;
+
+  if (g_str_equal (interface_name, BLUEZ_MEDIA_PLAYER_IFACE))
+    return BLUEZ_TYPE_MEDIA_PLAYER1_PROXY;
+
+  return G_TYPE_DBUS_PROXY;
+}
+
+static void
+manager_object_added_cb (GDBusObjectManager * manager,
+    GDBusObject * object, gpointer user_data)
+{
+  GstAvrcpConnection *avrcp = (GstAvrcpConnection *) user_data;
+  BluezMediaPlayer1 *player;
+
+  if (!(player = media_player_from_dbus_object (object)))
+    return;
+
+  gst_avrcp_connection_set_player (avrcp, player);
+}
+
+static void
+manager_object_removed_cb (GDBusObjectManager * manager,
+    GDBusObject * object, gpointer user_data)
+{
+  GstAvrcpConnection *avrcp = (GstAvrcpConnection *) user_data;
+  BluezMediaPlayer1 *player;
+
+  if (!(player = media_player_from_dbus_object (object)))
+    return;
+
+  if (player == avrcp->player)
+    gst_avrcp_connection_set_player (avrcp, NULL);
+}
+
+static void
+manager_ready_cb (GObject * object, GAsyncResult * res, gpointer user_data)
+{
+  GstAvrcpConnection *avrcp = (GstAvrcpConnection *) user_data;
+  GList *objects, *i;
+  GError *err = NULL;
+
+  avrcp->manager = g_dbus_object_manager_client_new_for_bus_finish (res, &err);
+  if (!avrcp->manager) {
+    GST_WARNING ("Could not create ObjectManager proxy: %s", err->message);
+    g_error_free (err);
+    return;
+  }
+
+  g_signal_connect (avrcp->manager, "object-added",
+      G_CALLBACK (manager_object_added_cb), avrcp);
+  g_signal_connect (avrcp->manager, "object-removed",
+      G_CALLBACK (manager_object_removed_cb), avrcp);
+
+  objects = g_dbus_object_manager_get_objects (avrcp->manager);
+
+  for (i = objects; i; i = i->next) {
+    BluezMediaPlayer1 *player =
+        media_player_from_dbus_object (G_DBUS_OBJECT (i->data));
+
+    if (player && g_str_equal (avrcp->dev_path,
+            bluez_media_player1_get_device (player))) {
+      gst_avrcp_connection_set_player (avrcp, player);
+      break;
+    }
+  }
+
+  g_list_free_full (objects, g_object_unref);
+}
+
+GstAvrcpConnection *
+gst_avrcp_connection_new (const gchar * dev_path, GstAvrcpMetadataCb cb,
+    gpointer user_data, GDestroyNotify user_data_free_cb)
+{
+  GstAvrcpConnection *avrcp;
+
+  avrcp = g_new0 (GstAvrcpConnection, 1);
+
+  avrcp->cb = cb;
+  avrcp->user_data = user_data;
+  avrcp->user_data_free_cb = user_data_free_cb;
+
+  avrcp->context = g_main_context_new ();
+  avrcp->mainloop = g_main_loop_new (avrcp->context, FALSE);
+
+  avrcp->dev_path = g_strdup (dev_path);
+
+  g_main_context_push_thread_default (avrcp->context);
+
+  g_dbus_object_manager_client_new_for_bus (G_BUS_TYPE_SYSTEM,
+      G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_NONE, BLUEZ_NAME, BLUEZ_PATH,
+      manager_proxy_type_func, NULL, NULL, NULL, manager_ready_cb, avrcp);
+
+  g_main_context_pop_thread_default (avrcp->context);
+
+  avrcp->thread = g_thread_new ("gstavrcp", (GThreadFunc) g_main_loop_run,
+      avrcp->mainloop);
+
+  return avrcp;
+}
+
+void
+gst_avrcp_connection_free (GstAvrcpConnection * avrcp)
+{
+  g_main_loop_quit (avrcp->mainloop);
+  g_main_loop_unref (avrcp->mainloop);
+
+  g_main_context_unref (avrcp->context);
+
+  g_thread_join (avrcp->thread);
+
+  if (avrcp->player)
+    g_object_unref (avrcp->player);
+
+  if (avrcp->manager)
+    g_object_unref (avrcp->manager);
+
+  if (avrcp->user_data_free_cb)
+    avrcp->user_data_free_cb (avrcp->user_data);
+
+  g_free (avrcp->dev_path);
+  g_free (avrcp);
+}
diff --git a/sys/bluez/gstavrcputil.h b/sys/bluez/gstavrcputil.h
new file mode 100644 (file)
index 0000000..b59aa3c
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2015 Arun Raghavan <git@arunraghavan.net>
+ *
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#ifndef __GST_AVRCP_UTIL_H
+#define __GST_AVRCP_UTIL_H
+
+#include <gst/gst.h>
+
+typedef struct _GstAvrcpConnection GstAvrcpConnection;
+
+typedef void (*GstAvrcpMetadataCb) (GstAvrcpConnection *, GstTagList *,
+    gpointer);
+
+GstAvrcpConnection *
+gst_avrcp_connection_new (const gchar * dev_path, GstAvrcpMetadataCb cb,
+    gpointer user_data, GDestroyNotify user_data_free_cb);
+
+void gst_avrcp_connection_free (GstAvrcpConnection * avrcp);
+
+#endif /* __GST_AVRCP_UTIL_H */
index ff52ee3..facae70 100644 (file)
     <property name="Delay" type="q" access="read"></property>
     <property name="Volume" type="q" access="readwrite"></property>
 </interface>
+
+<interface name="org.bluez.MediaPlayer1">
+  <method name="Play"/>
+  <method name="Pause"/>
+  <method name="Stop"/>
+  <method name="Next"/>
+  <method name="Previous"/>
+  <method name="FastForward"/>
+  <method name="Rewind"/>
+  <property name="Name" type="s" access="read"/>
+  <property name="Type" type="s" access="read"/>
+  <property name="Subtype" type="s" access="read"/>
+  <property name="Position" type="u" access="read"/>
+  <property name="Status" type="s" access="read"/>
+  <property name="Equalizer" type="s" access="readwrite"/>
+  <property name="Repeat" type="s" access="readwrite"/>
+  <property name="Shuffle" type="s" access="readwrite"/>
+  <property name="Scan" type="s" access="readwrite"/>
+  <property name="Track" type="a{sv}" access="read"/>
+  <property name="Device" type="o" access="read"/>
+  <property name="Browsable" type="b" access="read"/>
+  <property name="Searchable" type="b" access="read"/>
+  <property name="Playlist" type="o" access="read"/>
+</interface>
 </node>