From bf6f7c39d41700f6e01b0e6d534cd6706e458b4c Mon Sep 17 00:00:00 2001 From: "Zeeshan Ali (Khattak)" Date: Sat, 28 Aug 2010 04:23:24 +0300 Subject: [PATCH] mpris: Add MPRIS2-based MediaRenderer plugin 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 | 9 ++ src/plugins/Makefile.am | 5 + src/plugins/mpris/Makefile.am | 39 +++++ src/plugins/mpris/rygel-mpris-interfaces.vala | 50 ++++++ src/plugins/mpris/rygel-mpris-player.vala | 183 ++++++++++++++++++++++ src/plugins/mpris/rygel-mpris-plugin-factory.vala | 140 +++++++++++++++++ src/plugins/mpris/rygel-mpris-plugin.vala | 91 +++++++++++ 7 files changed, 517 insertions(+) create mode 100644 src/plugins/mpris/Makefile.am create mode 100644 src/plugins/mpris/rygel-mpris-interfaces.vala create mode 100644 src/plugins/mpris/rygel-mpris-player.vala create mode 100644 src/plugins/mpris/rygel-mpris-plugin-factory.vala create mode 100644 src/plugins/mpris/rygel-mpris-plugin.vala diff --git a/configure.ac b/configure.ac index f52d51b..1031dcc 100644 --- a/configure.ac +++ b/configure.ac @@ -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} " diff --git a/src/plugins/Makefile.am b/src/plugins/Makefile.am index f6ed4a3..ccbeee6 100644 --- a/src/plugins/Makefile.am +++ b/src/plugins/Makefile.am @@ -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 index 0000000..17a25f6 --- /dev/null +++ b/src/plugins/mpris/Makefile.am @@ -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 index 0000000..407dad8 --- /dev/null +++ b/src/plugins/mpris/rygel-mpris-interfaces.vala @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2009,2010 Nokia Corporation. + * + * Author: Zeeshan Ali (Khattak) + * + * + * 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 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 index 0000000..5033704 --- /dev/null +++ b/src/plugins/mpris/rygel-mpris-player.vala @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2008 OpenedHand Ltd. + * Copyright (C) 2009 Nokia Corporation. + * + * Author: Jorn Baayen + * Zeeshan Ali (Khattak) + * + * + * 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 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 index 0000000..06d8367 --- /dev/null +++ b/src/plugins/mpris/rygel-mpris-plugin-factory.vala @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2009 Zeeshan Ali (Khattak) . + * Copyright (C) 2009,2010 Nokia Corporation. + * + * Author: Zeeshan Ali (Khattak) + * + * + * 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 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 index 0000000..1bc5a4d --- /dev/null +++ b/src/plugins/mpris/rygel-mpris-plugin.vala @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2008 Zeeshan Ali (Khattak) . + * Copyright (C) 2008 Nokia Corporation. + * + * Author: Zeeshan Ali (Khattak) + * + * + * 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; + } + } +} + -- 2.7.4