mpris: Add MPRIS2-based MediaRenderer plugin
authorZeeshan Ali (Khattak) <zeeshanak@gnome.org>
Sat, 28 Aug 2010 01:23:24 +0000 (04:23 +0300)
committerZeeshan Ali (Khattak) <zeeshanak@gnome.org>
Sun, 29 Aug 2010 22:08:45 +0000 (01:08 +0300)
This plugin turns any media player that implements MPRIS2 D-Bus interface
into a UPnP MediaRenderer.

http://www.mpris.org/2.0/spec/

configure.ac
src/plugins/Makefile.am
src/plugins/mpris/Makefile.am [new file with mode: 0644]
src/plugins/mpris/rygel-mpris-interfaces.vala [new file with mode: 0644]
src/plugins/mpris/rygel-mpris-player.vala [new file with mode: 0644]
src/plugins/mpris/rygel-mpris-plugin-factory.vala [new file with mode: 0644]
src/plugins/mpris/rygel-mpris-plugin.vala [new file with mode: 0644]

index f52d51b..1031dcc 100644 (file)
@@ -70,6 +70,7 @@ if test ! -e src/rygel/rygel_vala.stamp -o \
         ! -e src/plugins/media-export/librygel_media_export_la_vala.stamp -o \
         ! -e src/plugins/mediathek/librygel_mediathek_la_vala.stamp -o \
         ! -e src/plugins/gst-launch/librygel_gst_launch_la_vala.stamp -o \
+        ! -e src/plugins/mpris/librygel_mpris_la_vala.stamp -o \
         ! -e src/plugins/external/librygel_external_la_vala.stamp ; then
     enable_vala=yes
 fi
@@ -208,6 +209,11 @@ AC_ARG_ENABLE(external-plugin,
        [  --enable-external-plugin          build External plugin],,
         enable_external_plugin=yes)
 
+# Build MPRIS2 plugin
+AC_ARG_ENABLE(mpris-plugin,
+       [  --enable-mpris-plugin             build MPRIS2 plugin],,
+        enable_mpris_plugin=yes)
+
 # Build GstRenderer plugin
 AC_ARG_ENABLE(playbin-plugin,
        [  --enable-playbin_plugin          build Gstreamer Playbin plugin],,
@@ -234,6 +240,7 @@ AM_CONDITIONAL([BUILD_TRACKER_PLUGIN],
 AM_CONDITIONAL([BUILD_MEDIATHEK_PLUGIN], [test "x$enable_mediathek_plugin" = "xyes"])
 AM_CONDITIONAL([BUILD_MEDIA_EXPORT_PLUGIN], [test "x$enable_media_export_plugin" = "xyes"])
 AM_CONDITIONAL([BUILD_EXTERNAL_PLUGIN], [test "x$enable_external_plugin" = "xyes"])
+AM_CONDITIONAL([BUILD_MPRIS_PLUGIN], [test "x$enable_mpris_plugin" = "xyes"])
 AM_CONDITIONAL([BUILD_PLAYBIN_PLUGIN], [test "x$enable_playbin_plugin" = "xyes"])
 AM_CONDITIONAL([BUILD_GST_LAUNCH_PLUGIN], [test "x$enable_gst_launch_plugin" = "xyes"])
 
@@ -275,6 +282,7 @@ src/ui/Makefile
 src/plugins/Makefile
 src/plugins/media-export/Makefile
 src/plugins/external/Makefile
+src/plugins/mpris/Makefile
 src/plugins/gst-launch/Makefile
 src/plugins/mediathek/Makefile
 src/plugins/tracker/Makefile
@@ -314,6 +322,7 @@ echo "
         mediathek:              ${enable_mediathek_plugin}
         media-export            ${enable_media_export_plugin}
         external:               ${enable_external_plugin}
+        MPRIS2:                 ${enable_mpris_plugin}
         gst-launch:             ${enable_gst_launch_plugin}
         playbin:                ${enable_playbin_plugin}
 "
index f6ed4a3..ccbeee6 100644 (file)
@@ -18,6 +18,10 @@ if BUILD_EXTERNAL_PLUGIN
 EXTERNAL_PLUGIN = external
 endif
 
+if BUILD_MPRIS_PLUGIN
+MPRIS_PLUGIN = mpris
+endif
+
 if BUILD_GST_LAUNCH_PLUGIN
 GST_LAUNCH_PLUGIN = gst-launch
 endif
@@ -31,6 +35,7 @@ SUBDIRS = $(TEST_PLUGIN) \
          $(MEDIATHEK_PLUGIN) \
          $(MEDIA_EXPORT_PLUGIN) \
          $(EXTERNAL_PLUGIN) \
+         $(MPRIS_PLUGIN) \
          $(GST_LAUNCH_PLUGIN) \
           $(PLAYBIN)
 
diff --git a/src/plugins/mpris/Makefile.am b/src/plugins/mpris/Makefile.am
new file mode 100644 (file)
index 0000000..17a25f6
--- /dev/null
@@ -0,0 +1,39 @@
+if UNINSTALLED
+shareddir = $(abs_top_builddir)/data
+else
+shareddir = $(datadir)/rygel
+endif
+
+plugindir = $(libdir)/rygel-1.0
+
+plugin_LTLIBRARIES = librygel-mpris.la
+
+AM_CFLAGS = $(LIBGUPNP_CFLAGS) \
+           $(LIBGUPNP_AV_CFLAGS) \
+           $(GEE_CFLAGS) \
+           $(LIBGSTREAMER_CFLAGS) \
+           $(LIBDBUS_GLIB_CFLAGS) \
+           $(UUID_CFLAGS) \
+           -I$(top_srcdir)/src/rygel -DDATA_DIR='"$(shareddir)"' \
+           -include config.h
+
+librygel_mpris_la_SOURCES = rygel-mpris-player.vala \
+                           rygel-mpris-plugin.vala \
+                           rygel-mpris-plugin-factory.vala \
+                           rygel-mpris-interfaces.vala
+
+librygel_mpris_la_VALAFLAGS = --vapidir=$(top_srcdir)/src/rygel \
+                             --vapidir=$(srcdir) \
+                             --pkg rygel-1.0 --pkg rygel-build-config \
+                             --pkg gupnp-1.0 --pkg gupnp-av-1.0 \
+                             --pkg gee-1.0 --pkg gstreamer-0.10 -g
+
+librygel_mpris_la_LIBADD = $(LIBGUPNP_LIBS) \
+                          $(LIBGUPNP_AV_LIBS) \
+                          $(LIBDBUS_GLIB_LIBS) \
+                          $(GEE_LIBS) \
+                          $(LIBGSTREAMER_LIBS) \
+                          $(UUID_LIBS)
+librygel_mpris_la_LDFLAGS = -shared -fPIC -module -avoid-version
+
+MAINTAINERCLEANFILES = Makefile.in
diff --git a/src/plugins/mpris/rygel-mpris-interfaces.vala b/src/plugins/mpris/rygel-mpris-interfaces.vala
new file mode 100644 (file)
index 0000000..407dad8
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2009,2010 Nokia Corporation.
+ *
+ * Author: Zeeshan Ali (Khattak) <zeeshanak@gnome.org>
+ *                               <zeeshan.ali@nokia.com>
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel 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 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel 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 program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+using DBus;
+
+[DBus (name = "org.mpris.MediaPlayer2")]
+public interface Rygel.MPRIS.MediaPlayerProxy : DBus.Object {
+    public const string IFACE = "org.mpris.MediaPlayer2";
+
+    public abstract string identity { owned get; }
+    public abstract string[] supported_uri_schemes { owned get; }
+    public abstract string[] supported_mime_types { owned get; }
+}
+
+[DBus (name = "org.mpris.MediaPlayer2.Player")]
+public interface Rygel.MPRIS.MediaPlayer.PlayerProxy : DBus.Object {
+    public const string IFACE = "org.mpris.MediaPlayer2.Player";
+
+    public abstract string playback_status { owned get; }
+    public abstract double volume { get; set; }
+    public abstract int64 position { get; }
+    public abstract HashTable<string,Value?> metadata { owned get; }
+
+    public abstract void pause () throws DBus.Error;
+    public abstract void play_pause () throws DBus.Error;
+    public abstract void stop () throws DBus.Error;
+    public abstract void play () throws DBus.Error;
+    public abstract void seek (int64 offset) throws DBus.Error;
+    public abstract void open_uri (string uri) throws DBus.Error;
+}
diff --git a/src/plugins/mpris/rygel-mpris-player.vala b/src/plugins/mpris/rygel-mpris-player.vala
new file mode 100644 (file)
index 0000000..5033704
--- /dev/null
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2008 OpenedHand Ltd.
+ * Copyright (C) 2009 Nokia Corporation.
+ *
+ * Author: Jorn Baayen <jorn@openedhand.com>
+ *         Zeeshan Ali (Khattak) <zeeshanak@gnome.org>
+ *                               <zeeshan.ali@nokia.com>
+ *
+ * Rygel 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 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel 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 program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+using Rygel.MPRIS;
+using Rygel.MPRIS.MediaPlayer;
+using FreeDesktop;
+
+public class Rygel.MPRIS.Player : GLib.Object, Rygel.MediaPlayer {
+    private string[] protocols;
+    private string[] mime_types;
+
+    private PlayerProxy actual_player;
+    private Properties properties;
+
+    public string playback_state {
+        owned get {
+            return this.mpris_to_upnp_state (actual_player.playback_status);
+        }
+
+        set {
+            debug (_("Changing playback state to %s.."), value);
+
+            /* FIXME: Do something about errors below */
+            switch (value) {
+            case "STOPPED":
+                try {
+                    this.actual_player.stop ();
+                } catch (Error error) {}
+
+                break;
+            case "PAUSED_PLAYBACK":
+                try {
+                    this.actual_player.pause ();
+                } catch (Error error) {}
+
+                break;
+            case "PLAYING":
+                try {
+                    this.actual_player.play ();
+                } catch (Error error) {}
+
+                break;
+            default:
+                assert_not_reached ();
+            }
+        }
+    }
+
+    public string? uri {
+        owned get {
+            var val = this.actual_player.metadata.lookup ("xesam:url");
+
+            if (val != null) {
+                return (string) val;
+            } else {
+                return null;
+            }
+        }
+
+        set {
+            try {
+                this.actual_player.open_uri (value);
+            } catch (Error error) {}
+        }
+    }
+
+    public double volume {
+        get {
+            return this.actual_player.volume;
+        }
+
+        set {
+            this.actual_player.volume = value;
+        }
+    }
+
+    public int64 duration {
+        get {
+            var val = this.actual_player.metadata.lookup ("mpris:length");
+            int64 dur = 0;
+
+            if (val != null) {
+                dur = (int64) val * 1000;
+            }
+
+            return dur;
+        }
+    }
+
+    public int64 position {
+        get {
+            return this.actual_player.position * 1000;
+        }
+    }
+
+    public Player (PlayerProxy actual_player,
+                   Properties  properties,
+                   string[]    mime_types,
+                   string[]    protocols) {
+        this.actual_player = actual_player;
+        this.properties = properties;
+        this.mime_types = mime_types;
+        this.protocols = protocols;
+
+        this.properties.properties_changed.connect (this.on_properties_changed);
+    }
+
+    public bool seek (Gst.ClockTime time) {
+        var ret = false;
+
+        try {
+            this.actual_player.seek (time / 1000);
+            ret = true;
+        } catch (Error error) {}
+
+        return ret;
+    }
+
+    public string[] get_protocols () {
+        return this.protocols;
+    }
+
+    public string[] get_mime_types () {
+        return this.mime_types;
+    }
+
+    private string mpris_to_upnp_state (string state) {
+        switch (state) {
+        case "Stopped":
+            return "STOPPED";
+        case "Paused":
+            return "PAUSED_PLAYBACK";
+        case "Playing":
+            return "PLAYING";
+        default:
+            assert_not_reached ();
+        }
+    }
+
+    private void on_properties_changed (string                   iface,
+                                        HashTable<string,Value?> changed,
+                                        string[]                 invalidated) {
+        if (changed.lookup ("PlaybackStatus") != null) {
+            this.notify_property ("playback-state");
+        }
+
+        if (changed.lookup ("Volume") != null) {
+            this.notify_property ("volume");
+        }
+
+        if (changed.lookup ("Metadata") != null) {
+            var metadata = this.actual_player.metadata;
+
+            if (metadata.lookup ("xesam:url") != null) {
+                this.notify_property ("uri");
+            }
+
+            if (metadata.lookup ("mpris:length") != null) {
+                this.notify_property ("duration");
+            }
+        }
+    }
+}
diff --git a/src/plugins/mpris/rygel-mpris-plugin-factory.vala b/src/plugins/mpris/rygel-mpris-plugin-factory.vala
new file mode 100644 (file)
index 0000000..06d8367
--- /dev/null
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2009 Zeeshan Ali (Khattak) <zeeshanak@gnome.org>.
+ * Copyright (C) 2009,2010 Nokia Corporation.
+ *
+ * Author: Zeeshan Ali (Khattak) <zeeshanak@gnome.org>
+ *                               <zeeshan.ali@nokia.com>
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel 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 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel 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 program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+using Rygel;
+using Gee;
+using FreeDesktop;
+
+private MPRIS.PluginFactory plugin_factory;
+
+public void module_init (PluginLoader loader) {
+    try {
+        plugin_factory = new MPRIS.PluginFactory (loader);
+    } catch (DBus.Error error) {
+        critical ("Failed to fetch list of MPRIS services: %s\n",
+                  error.message);
+    }
+}
+
+public class Rygel.MPRIS.PluginFactory {
+    private const string DBUS_SERVICE = "org.freedesktop.DBus";
+    private const string DBUS_OBJECT = "/org/freedesktop/DBus";
+
+    private const string SERVICE_PREFIX = "org.mpris.MediaPlayer2.";
+    private const string MEDIA_PLAYER_PATH = "/org/mpris/MediaPlayer2";
+
+    DBusObject      dbus_obj;
+    DBus.Connection connection;
+    PluginLoader    loader;
+
+    public PluginFactory (PluginLoader loader) throws DBus.Error {
+        this.connection = DBus.Bus.get (DBus.BusType.SESSION);
+
+        this.dbus_obj = this.connection.get_object (DBUS_SERVICE, DBUS_OBJECT)
+                        as DBusObject;
+        this.loader = loader;
+
+        this.load_plugins.begin ();
+    }
+
+    private async void load_plugins () throws DBus.Error {
+        var services = yield this.dbus_obj.list_names ();
+
+        foreach (var service in services) {
+            if (service.has_prefix (SERVICE_PREFIX) &&
+                this.loader.get_plugin_by_name (service) == null) {
+                yield this.load_plugin (service);
+            }
+        }
+
+        yield this.load_activatable_plugins ();
+    }
+
+    private async void load_activatable_plugins () throws DBus.Error {
+        var services = yield this.dbus_obj.list_activatable_names ();
+
+        foreach (var service in services) {
+            if (service.has_prefix (SERVICE_PREFIX) &&
+                this.loader.get_plugin_by_name (service) == null) {
+                yield this.load_plugin (service);
+            }
+        }
+
+        this.dbus_obj.name_owner_changed.connect (this.name_owner_changed);
+    }
+
+    private void name_owner_changed (DBusObject dbus_obj,
+                                     string     name,
+                                     string     old_owner,
+                                     string     new_owner) {
+        var plugin = this.loader.get_plugin_by_name (name);
+
+        if (plugin != null) {
+            if (old_owner != "" && new_owner == "") {
+                debug ("Service '%s' going down, marking it as unavailable",
+                       name);
+                plugin.available = false;
+            } else if (old_owner == "" && new_owner != "") {
+                debug ("Service '%s' up again, marking it as available", name);
+                plugin.available = true;
+            }
+        } else if (name.has_prefix (SERVICE_PREFIX)) {
+                // Ah, new plugin available, lets use it
+                this.load_plugin.begin (name);
+        }
+    }
+
+    private async void load_plugin (string service_name) {
+        // Create proxy to MediaObject iface to get the display name through
+        var props = this.connection.get_object (service_name, MEDIA_PLAYER_PATH)
+                    as Properties;
+
+        HashTable<string,Value?> props_hash;
+
+        try {
+            props_hash = yield props.get_all (MediaPlayerProxy.IFACE);
+        } catch (DBus.Error err) {
+            warning ("Failed to fetch properties of plugin %s: %s.",
+                     service_name,
+                     err.message);
+
+            return;
+        }
+
+        var title = (string) props_hash.lookup ("Identity");
+        if (title == null) {
+            title = service_name;
+        }
+
+        var mime_types = (string[]) props_hash.lookup ("SupportedMimeTypes");
+        var schemes = (string[]) props_hash.lookup ("SupportedUriSchemes");
+
+        var plugin = new MPRIS.Plugin (service_name,
+                                       title,
+                                       mime_types,
+                                       schemes);
+
+        this.loader.add_plugin (plugin);
+    }
+}
diff --git a/src/plugins/mpris/rygel-mpris-plugin.vala b/src/plugins/mpris/rygel-mpris-plugin.vala
new file mode 100644 (file)
index 0000000..1bc5a4d
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2008 Zeeshan Ali (Khattak) <zeeshanak@gnome.org>.
+ * Copyright (C) 2008 Nokia Corporation.
+ *
+ * Author: Zeeshan Ali (Khattak) <zeeshanak@gnome.org>
+ *                               <zeeshan.ali@nokia.com>
+ *
+ * This file is part of Rygel.
+ *
+ * Rygel 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 of the License, or
+ * (at your option) any later version.
+ *
+ * Rygel 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 program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+using DBus;
+using Rygel.MPRIS;
+using Rygel.MPRIS.MediaPlayer;
+
+public class Rygel.MPRIS.Plugin : Rygel.MediaRendererPlugin {
+    private const string MEDIA_PLAYER_PATH = "/org/mpris/MediaPlayer2";
+
+    private PlayerProxy actual_player;
+    private FreeDesktop.Properties properties;
+
+    private string[] mime_types;
+    private string[] protocols;
+
+    public Plugin (string   service_name,
+                   string   title,
+                   string[] mime_types,
+                   string[] schemes) {
+        base (service_name, title);
+
+        this.mime_types = mime_types;
+        this.protocols = this.schemes_to_protocols (schemes);
+
+        try {
+            var connection = DBus.Bus.get (DBus.BusType.SESSION);
+
+            // Create proxy to MediaPlayer.Player iface
+            this.actual_player = connection.get_object (service_name,
+                                                        MEDIA_PLAYER_PATH)
+                                 as PlayerProxy;
+            // Create proxy to FreeDesktop.Properties iface
+            this.properties = connection.get_object (service_name,
+                                                     MEDIA_PLAYER_PATH)
+                              as FreeDesktop.Properties;
+        } catch (GLib.Error err) {
+            critical ("Failed to connect to session bus: %s", err.message);
+        }
+    }
+
+    public override Rygel.MediaPlayer? get_player () {
+        return new MPRIS.Player (this.actual_player,
+                                 this.properties,
+                                 this.mime_types,
+                                 this.protocols);
+    }
+
+    private string[] schemes_to_protocols (string[] schemes) {
+        var protocols = new string[schemes.length];
+
+        for (var i = 0; i < schemes.length; i++) {
+            protocols[i] = this.scheme_to_protocol (schemes[i]);
+        }
+
+        return protocols;
+    }
+
+    private string scheme_to_protocol (string scheme) {
+        switch (scheme) {
+        case "http":
+            return "http-get";
+        case "file":
+            return "internal";
+        default:
+            return scheme;
+        }
+    }
+}
+