Skip to content

Instantly share code, notes, and snippets.

@shinyquagsire23
Created June 25, 2014 05:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shinyquagsire23/10b900caf9605d5f4348 to your computer and use it in GitHub Desktop.
Save shinyquagsire23/10b900caf9605d5f4348 to your computer and use it in GitHub Desktop.
mpris.vala
/*
* Copyright 2011-2013 Jiří Janoušek <janousek.jiri@gmail.com>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
// TODO: How to skip notifications?
using Diorite;
using Diorite.Logger;
using Diorite.Widgets;
using Nuvola.Actions;
namespace Nuvola.Extensions.Mpris{
public Nuvola.ExtensionInfo get_info(){
return {
/// Name of a plugin providing MPRIS interface
_("Remote Player Interface"),
Config.VERSION,
/// Description of a plugin providing MPRIS interface
_("<p><a href=\"http://specifications.freedesktop.org/mpris-spec/latest/\">Media Player Remote Interfacing Specification (MPRIS)</a> is used by applets, widgets and other applications (called MPRIS clients) to control running media players.</p> <p><a href=\"https://extensions.gnome.org/extension/55/media-player-indicator/\">GNOME Shell Media player extension</a> and <a href=\"http://www.omgubuntu.co.uk/tag/soundmenu/\">Ubuntu sound indicator</a> are good examples of MPRIS clients.</p>"),
"Jiří Janoušek",
typeof(Extension),
true
};
}
/**
* MPRIS manager launches MPRIS application and MPRIS player objects
*/
public class Extension: Nuvola.Extension{
private weak Diorite.Application app;
private weak Nuvola.Player player;
private weak Diorite.Actions actions;
private ApplicationProxy mpris_app;
private PlayerProxy mpris_player;
private uint player_proxy_id = 0;
private uint application_proxy_id = 0;
private uint owner_id = 0;
private unowned DBusConnection conn;
/**
* {@inheritDoc}
*/
public override void load(ObjectContainer objects) throws ExtensionError{
// Get dependencies, throws ExtensionError
app = objects.get<Diorite.Application>("application");
player = objects.get<Player>("player");
actions = objects.get<Diorite.Actions>("actions");
string application_bus = "org.mpris.MediaPlayer2." + app.app_name;
owner_id = Bus.own_name(BusType.SESSION, application_bus,
BusNameOwnerFlags.NONE,
on_bus_acquired, on_name_acquired, on_name_lost);
if(owner_id == 0){
critical("Unable to obtain bus name %s", application_bus);
var err = new ErrorDialog("Error occurred",
"Sound Menu integration failed.");
err.run();
}
}
/**
* {@inheritDoc}
*/
public override void unload(){
mpris_app = null;
mpris_player = null;
if(player_proxy_id > 0){
conn.unregister_object(player_proxy_id);
player_proxy_id = 0;
}
if(application_proxy_id > 0){
conn.unregister_object(application_proxy_id);
application_proxy_id = 0;
}
if(owner_id > 0){
Bus.unown_name(owner_id);
owner_id = 0;
}
}
/**
* Register application and player MPRIS objects
*/
private void on_bus_acquired(DBusConnection conn, string name) {
/* register MPRIS DBus objects */
this.conn = conn;
debug("Bus acquired: %s, registering objects", name);
try {
mpris_app = new ApplicationProxy(app);
application_proxy_id = conn.register_object("/org/mpris/MediaPlayer2", mpris_app);
mpris_player = new PlayerProxy(player, actions, conn);
player_proxy_id = conn.register_object("/org/mpris/MediaPlayer2", mpris_player);
}
catch(IOError e) {
critical("Unable to register objects: %s", e.message);
var err = new ErrorDialog("Error occurred",
"Sound Menu integration failed.");
err.run();
}
}
private void on_name_acquired(DBusConnection connection, string name){
debug("Bus name acquired: %s", name);
}
private void on_name_lost(DBusConnection connection, string name){
critical("Bus name lost: %s", name);
}
}
/**
* MPRIS DBus wrapper for Application class.
*
* [[http://www.mpris.org/2.1/spec/Root_Node.html|Specification]]
*/
[DBus(name = "org.mpris.MediaPlayer2")]
public class ApplicationProxy: GLib.Object{
private weak Diorite.Application app;
/**
* Constructor
*
* @param app application object to be wrapped
*/
public ApplicationProxy(Diorite.Application app){
this.app = app;
}
#if DEBUG_MEMORY
~ApplicationProxy(){
debug("~ApplicationProxy");
}
#endif
/* Properties: http://www.mpris.org/2.1/spec/Root_Node.html#properties */
[Description(nick = "Quit action availability", blurb = "If false, calling Quit will have no effect, and may raise a NotSupported error. If true, calling Quit will cause the media application to attempt to quit (although it may still be prevented from quitting by the user, for example). ")]
public bool can_quit{
get{return true;}
}
[Description(nick = "Raise action availability", blurb = "If false, calling Raise will have no effect, and may raise a NotSupported error. If true, calling Raise will cause the media application to attempt to bring its user interface to the front, although it may be prevented from doing so (by the window manager, for example). ")]
public bool can_raise{
get{ return true; }
}
[Description(nick = "Track list availability", blurb = "Indicates whether the /org/mpris/MediaPlayer2 object implements the org.mpris.MediaPlayer2.TrackList interface. ")]
public bool has_track_list{
get{ return false; }
}
[Description(nick = "Application name", blurb = "A friendly name to identify the media player to users.")]
public string identity{
owned get{ return app.display_name; }
}
[Description(nick = "Desktop entry name", blurb = "The basename of an installed .desktop file which complies with the Desktop entry specification, with the '.desktop' extension stripped.")]
public string desktop_entry{
owned get{ return app.desktop_entry; }
}
[Description(nick = "The URI schemes supported by the media player.", blurb = "This can be viewed as protocols supported by the player in almost all cases. Almost every media player will include support for the 'file' scheme. Other common schemes are 'http' and 'rtsp'. ")]
public string[] supported_uri_schemes{
owned get { return {}; }
}
[Description(nick = "The mime-types supported by the media player. ", blurb = "The mime-types supported by the media player. Mime-types should be in the standard format (eg: audio/mpeg or application/ogg). ")]
public string[] SupportedMimeTypes{
owned get{ return {}; }
}
/* Methods: http://www.mpris.org/2.1/spec/Root_Node.html#methods */
/**
* Brings the media player's user interface to the front using any appropriate
*/
public void raise(){
app.activate();
}
/**
* Causes the media player to stop running.
*/
public void quit(){
app.quit();
}
}
/**
* MPRIS DBus object handling playback.
*
* [[http://www.mpris.org/2.1/spec/Player_Node.html|Specification]]
*/
[DBus(name = "org.mpris.MediaPlayer2.Player")]
public class PlayerProxy : GLib.Object{
private weak Nuvola.Player player;
private weak Diorite.Actions actions;
private DBusConnection conn;
private HashTable<string,Variant> song_info;
private static const string ACTIONS[4] = {PLAY, PAUSE, PREV_SONG, NEXT_SONG};
/**
* Constructs MPRIS Player
*
* @param player Player object to be wrapped
* @param conn active DBus connection
*/
public PlayerProxy(Nuvola.Player player, Diorite.Actions actions, DBusConnection conn){
this.player = player;
this.actions = actions;
this.conn = conn;
this.song_info = new HashTable<string,Variant>(null, null);
this.notify.connect(this.send_property_change);
this.player.notify["playback-state"].connect(this.on_playback_state_changed);
foreach(var name in ACTIONS){
var action = actions.get_action(name);
if(action != null){
action.notify["sensitive"].connect(on_actions_changed);
update_action(action);
}
}
this.player.song_changed.connect(this.on_song_changed);
on_song_changed(player.song, player.artist, player.album, player.album_art);
update_playback_state();
}
#if DEBUG_MEMORY
~PlayerProxy(){
debug("~PlayerProxy");
}
#endif
/**
* Emits PropertiesChanged signal used by the MPRIS to notify clients of changes.
*
* @param p Param info
*/
private void send_property_change (ParamSpec p) {
var builder = new VariantBuilder (VariantType.ARRAY);
var invalid_builder = new VariantBuilder (new VariantType ("as"));
Variant i;
switch(p.name){
case "Metadata":
i = this.Metadata;
builder.add ("{sv}", "Metadata", i);
break;
case "can-go-previous":
i = this.can_go_previous;
builder.add ("{sv}", "CanGoPrevious", i);
break;
case "can-go-next":
i = this.can_go_next;
builder.add ("{sv}", "CanGoNext", i);
break;
case "can-play":
i = this.can_play;
builder.add ("{sv}", "CanPlay", i);
break;
case "can-pause":
i = this.can_pause;
builder.add ("{sv}", "CanPause", i);
break;
case "playback-status":
i = this.playback_status;
builder.add ("{sv}", "PlaybackStatus", i);
break;
default:
critical("Unhandled property: %s", p.name);
return;
}
debug("MPRIS Player: %s changed", p.name);
var v = new Variant("(sa{sv}as)", "org.mpris.MediaPlayer2.Player",
builder, invalid_builder);
try {
conn.emit_signal(null,
"/org/mpris/MediaPlayer2",
"org.freedesktop.DBus.Properties",
"PropertiesChanged", v);
}
catch(Error e){
warning("Unable to emit PropertiesChanged signal: %s", e.message);
}
}
/**
* Updates information passed over DBus and sends notification.
*/
private void on_song_changed(string? song, string? artist, string? album, string? album_art){
debug("MPRIS: song changed");
var info = new HashTable<string,Variant>(null, null);
if(artist != null)
{
string[] artistArray = {artist};
info.insert("xesam:artist", artistArray);
}
if (album != null)
info.insert("xesam:album", album);
if (song != null)
info.insert("xesam:title", song);
if (album_art != null)
info.insert("mpris:artUrl", "file://" + album_art);
Variant variant = "1";
info.insert("mpris:trackid", variant);
this.Metadata = info;
}
/**
* Reflects changes in playback state
*/
private void on_playback_state_changed(GLib.Object o, ParamSpec p){
update_playback_state();
}
private void update_playback_state()
{
playback_status = player.playback_state == STATE_NONE ? "Stopped"
: (player.playback_state == STATE_PAUSED ? "Paused" : "Playing");
}
/**
* Reflects changes in playback state
*/
private void on_actions_changed(GLib.Object o, ParamSpec p){
var action = o as Diorite.Action;
if(action != null){
update_action(action);
}
}
private void update_action(Diorite.Action action){
switch(action.name){
case PLAY:
can_play = action.sensitive;
break;
case PAUSE:
can_pause = action.sensitive;
break;
case PREV_SONG:
can_go_previous = action.sensitive;
break;
case NEXT_SONG:
can_go_next = action.sensitive;
break;
default:
critical("Unexpected action: %s", action.name);
break;
}
}
/* Properties: http://www.mpris.org/2.1/spec/Player_Node.html#properties */
[Description(nick = "The current playback status.", blurb = "The current playback status. May be 'Playing', 'Paused' or 'Stopped'.")]
public string playback_status { owned get; private set; default = "Stopped";}
//~ [Description(nick = "The current loop / repeat status", blurb = "May be: 'None' if the playback will stop when there are no more tracks to play. 'Track' if the current track will start again from the beginning once it has finished playing. 'Playlist' if the playback loops through a list of tracks.")]
//~ public string LoopStatus{
//~ owned get{
//~ return "None";
//~ return "Track";
//~ return "Playlist";
//~ }
//~ set{
//~ switch(value){
//~ case("None"):
//~ break;
//~ case("Track"):
//~ break;
//~ case("Playlist"):
//~ break;
//~ default:
//~ break;
//~ }
//~ }
//~ }
// Doesn't compile
//~ [Description(nick = "The current playback rate.", blurb = "The current playback rate.")]
//~ public double Rate{
//~ get{
//~ return (double)1.0;
//~ }
//~ set{
//~ }
//~ }
//~
//~ [Description(nick = "Minimum playback rate.", blurb = "Minimum playback rate.")]
//~ public double MinimumRate{
//~ get{
//~ return (double)1.0;
//~ }
//~ }
//~
//~ [Description(nick = "Maximum playback rate.", blurb = "Maximum playback rate.")]
//~ public double MaximumRate{
//~ get{
//~ return (double)1.0;
//~ }
//~ }
//~ [Description(nick = "Shuffle state", blurb = "A value of false indicates that playback is progressing linearly through a playlist, while true means playback is progressing through a playlist in some other order.")]
//~ public bool Shuffle{
//~ get{ return true; }
//~ set{}
//~ }
[Description(nick = "The metadata of the current element.", blurb = "The metadata of the current element.")]
public HashTable<string,Variant>? Metadata{
owned get{
return this.song_info;
}
private set{
this.song_info = value;
}
}
//~ [Description(nick = "The volume level.", blurb = "The volume level.")]
//~ public double Volume{
//~ owned get{
//~ return (double)1.0;
//~ }
//~ set {
//~
//~ }
//~ }
//~ [Description(nick = "The current track position in microseconds.", blurb = "The current track position in microseconds.")]
//~ public int64 Position{
//~ get{
//~ return (int64)1;
//~ }
//~ }
[Description(nick = "Next song action availability", blurb = "Whether the client can call the Next method on this interface and expect the current track to change.")]
public bool can_go_next{ get; private set; default = false; }
[Description(nick = "Previous song action availability", blurb = "Whether the client can call the Previous method on this interface and expect the current track to change.")]
public bool can_go_previous{ get; private set; default = false; }
[Description(nick = "Play action availability", blurb = "Whether playback can be started using Play or PlayPause.")]
public bool can_play{ get; private set; default = false; }
[Description(nick = "Pause action availability", blurb = "Whether playback can be paused using Pause or PlayPause.")]
public bool can_pause{ get; private set; default = false; }
[Description(nick = "Seek action availability", blurb = "Whether the client can control the playback position using Seek and SetPosition. This may be different for different tracks.")]
public bool CanSeek{
get{
return false;
}
}
[Description(nick = "Whether the media player may be controlled.", blurb = "Whether the media player may be controlled over this interface.")]
public bool CanControl{
get{
return true;
}
}
/* Signals: http://www.mpris.org/2.1/spec/Player_Node.html#signals */
/**
* Indicates that the track position has changed in a way that is inconsistant with the current playing state.
* @param Position The new position, in microseconds.
*/
public signal void Seeked(int64 Position);
/* Methods: http://www.mpris.org/2.1/spec/Player_Node.html#methods */
/**
* Skips to the next track in the tracklist.
*
* If there is no next track (and endless playback and track repeat are both off),
* playback will be stopped. If playback is paused or stopped, it remains that way.
*/
public void next(){
this.player.next_song();
}
/***
* Skips to the previous track in the tracklist.
*
* If there is no previous track (and endless playback and track repeat are both off),
* playback will be stopped. If playback is paused or stopped, it remains that way.
*/
public void previous(){
this.player.previous_song();
}
/**
* Pauses playback.
*
* If playback is already paused, this has no effect. Calling Play after this should
* cause playback to start again from the same position.
*/
public void pause(){
this.player.pause();
}
/**
* Pauses playback.
* If playback is already paused, resumes playback.
* If playback is stopped, starts playback.
*/
public void play_pause(){
this.player.toggle_play();
}
/**
* Stops playback.
*
* If playback is already stopped, this has no effect. Calling Play after this should
* cause playback to start again from the beginning of the track.
*/
public void stop(){
this.player.stop();
}
/**
* Starts or resumes playback.
*
* If already playing, this has no effect.
* If there is no track to play, this has no effect.
*/
public void play(){
this.player.play();
}
/**
* Seeks forward in the current track by the specified number of microseconds.
*
* A negative value seeks back. If this would mean seeking back further than the start
* of the track, the position is set to 0. If the value passed in would mean seeking
* beyond the end of the track, acts like a call to Next. If the CanSeek property
* is false, this has no effect.
*
* @param Offset The number of microseconds to seek forward.
*/
public void seek(int64 Offset) {
return;
}
/**
* Sets the current track position in microseconds.
*
* If the Position argument is less than 0, does nothing.
* If the Position argument is greater than the track length, does nothing.
* If the CanSeek property is false, this has no effect.
*
* @param TrackId The currently playing track's identifier. If this does not match
* the id of the currently-playing track, the call is ignored as "stale".
* @param Position Track position in microseconds.This must be between 0 and <track_length>.
*/
public void SetPosition(string TrackId, int64 Position) {
}
/**
* Not implemented.
* @param Uri Uri of the track to load.
*/
public void OpenUri(string Uri) {
return;
}
}
} // namespace Nuvola.Extensions.Mpris
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment