Skip to content

Commit

Permalink
Add API for track position/seek
Browse files Browse the repository at this point in the history
Issue: #155

JavaScript API:

  * MediaPlayer.setTrackPosition(position) - to update current track
    position
  * MediaPlayer.setTrack(track) - supports track.length property
  * New action PlayerAction.SEEK - to seek in a desired position in the
    track, called with the new position as parameter
  * New flag MediaPlayer.setCanSeek(state) - to enable remote seeking

Nuvola Runtime:

  * Developer sidebar updated to show track position and length
  * Test service got progress bar
  * MPRIS plugin updated

IPC API:
  * /nuvola/mediaplayer/track-position - get track position
  * /nuvola/mediaplayer/set-track-position - set track position
  * /nuvola/mediaplayer/track-position-changed - notification

Signed-off-by: Jiří Janoušek <[email protected]>
  • Loading branch information
jiri-janousek committed Jun 11, 2017
1 parent 0deb2a9 commit 347085e
Show file tree
Hide file tree
Showing 10 changed files with 308 additions and 56 deletions.
49 changes: 48 additions & 1 deletion src/mainjs/mediaplayer.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2014-2016 Jiří Janoušek <[email protected]>
* Copyright 2014-2017 Jiří Janoušek <[email protected]>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
Expand Down Expand Up @@ -69,6 +69,10 @@ var PlayerAction = {
* Show playback notification
*/
PLAYBACK_NOTIFICATION: "playback-notification",
/**
* Seek to a new position
*/
SEEK: "seek",
}

/**
Expand Down Expand Up @@ -138,6 +142,7 @@ MediaPlayer.$init = function()
this._artworkLoop = 0;
this._baseActions = [PlayerAction.TOGGLE_PLAY, PlayerAction.PLAY, PlayerAction.PAUSE, PlayerAction.PREV_SONG, PlayerAction.NEXT_SONG];
this._notification = null;
this._trackPosition = 0;
Nuvola.core.connect("InitAppRunner", this);
Nuvola.core.connect("InitWebWorker", this);
}
Expand All @@ -152,9 +157,11 @@ MediaPlayer.$init = function()
* @param String|null track.album track album
* @param String|null track.artLocation URL of album/track artwork
* @param double|null track.rating track rating from `0.0` to `1.0`. *This item is ignored prior API 3.1.*
* @param double|null track.length track length as a string (`HH:MM:SS.xxx`, e.g. `1:25.54`) or number of microseconds. *This item is ignored prior API 4.5.*
*/
MediaPlayer.setTrack = function(track)
{
track.length = Nuvola.parseTimeUsec(track.length);
var changed = Nuvola.objectDiff(this._track, track);

if (!changed.length)
Expand All @@ -180,6 +187,25 @@ MediaPlayer.setTrack = function(track)
}
}

/**
* Set current playback position
*
* If the current position is the same as the previous one, this method does nothing.
*
* @since API 4.5
*
* @param String|Number position the current track position as a string (`HH:MM:SS.xxx`, e.g. `1:25.54`) or number of microseconds.
*/
MediaPlayer.setTrackPosition = function (position)
{
var position = Nuvola.parseTimeUsec(position);
if (this._trackPosition != position)
{
this._trackPosition = position;
Nuvola._callIpcMethodAsync("/nuvola/mediaplayer/set-track-position", position);
}
}

/**
* Set current playback state
*
Expand Down Expand Up @@ -302,6 +328,25 @@ MediaPlayer.setCanRate = function(canRate)
}
}

/**
* Set whether it is possible to seek to a specific position of the track
*
* If the argument is same as in the previous call, this method does nothing.
*
* @since API 4.5
*
* @param Boolean canSeek true if remote seeking should be allowed
*/
MediaPlayer.setCanSeek = function(canSeek)
{
if (this._canSeek !== canSeek)
{
this._canSeek = canSeek;
Nuvola.actions.setEnabled(PlayerAction.SEEK, !!canSeek);
this._setFlag("can-seek", !!canSeek);
}
}

/**
* Add actions for media player capabilities
*
Expand Down Expand Up @@ -341,6 +386,7 @@ MediaPlayer._onInitAppRunner = function(emitter)
Nuvola.actions.addAction("playback", "win", PlayerAction.STOP, "Stop", null, "media-playback-stop", null);
Nuvola.actions.addAction("playback", "win", PlayerAction.PREV_SONG, "Previous song", null, "media-skip-backward", null);
Nuvola.actions.addAction("playback", "win", PlayerAction.NEXT_SONG, "Next song", null, "media-skip-forward", null);
Nuvola.actions.addAction("playback", "win", PlayerAction.SEEK, "Seek", null, null, 0);
// FIXME: remove action if notifications compoment is disabled
Nuvola.actions.addAction("playback", "win", PlayerAction.PLAYBACK_NOTIFICATION, "Show playback notification", null, null, null);

Expand Down Expand Up @@ -411,6 +457,7 @@ MediaPlayer._sendDevelInfo = function()
"artist": this._track.artist || null,
"album": this._track.album || null,
"rating": rating,
"length": this._track.length || 0,
"artworkLocation": this._track.artLocation || null,
"artworkFile": this._artworkFile || null,
"playbackActions": this._baseActions.concat(this._extraActions),
Expand Down
33 changes: 32 additions & 1 deletion src/mainjs/utils.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2014 Jiří Janoušek <[email protected]>
* Copyright 2014-2017 Jiří Janoušek <[email protected]>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
Expand Down Expand Up @@ -152,3 +152,34 @@ Nuvola.objectDiff = function(object1, object2)

return changes;
}

/**
* Parse time as number of microseconds
*
* @param String time time expression `HH:MM:SS'
* @return the time in microseconds
*/
Nuvola.parseTimeUsec = function(time)
{
if (!time)
return 0;
if (time * 1 === time)
return time;
var parts = time.split(":");
var seconds = 0;
var item = parts.pop();
if (item !== undefined)
{
seconds = 1 * item;
item = parts.pop();
if (item !== undefined)
{
seconds += 60 * item;
item = parts.pop();
if (item !== undefined)
tseconds += 60 * 60 * item;
}
}
return seconds !== NaN ? seconds * 1000 * 1000 : 0;

}
32 changes: 30 additions & 2 deletions src/nuvolakit-runner/bindings/MediaPlayerBinding.vala
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2014-2015 Jiří Janoušek <[email protected]>
* Copyright 2014-2017 Jiří Janoušek <[email protected]>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
Expand Down Expand Up @@ -27,6 +27,7 @@ using Diorite;
public class Nuvola.MediaPlayerBinding: ModelBinding<MediaPlayerModel>
{
private const string TRACK_INFO_CHANGED = "track-info-changed";
private const string TRACK_POSITION_CHANGED = "track-position-changed";

public MediaPlayerBinding(Drt.ApiRouter router, WebWorker web_worker, MediaPlayerModel model)
{
Expand All @@ -53,12 +54,21 @@ public class Nuvola.MediaPlayerBinding: ModelBinding<MediaPlayerModel>
new Drt.StringParam("artworkLocation", false, true),
new Drt.StringParam("artworkFile", false, true),
new Drt.DoubleParam("rating", false, 0.0),
new Drt.DoubleParam("length", false, 0.0),
new Drt.StringArrayParam("playbackActions", false),
});
bind("set-track-position", Drt.ApiFlags.PRIVATE|Drt.ApiFlags.WRITABLE, null, handle_set_track_position, {
new Drt.DoubleParam("position", false, 0.0)

});
bind("track-info", Drt.ApiFlags.READABLE, "Returns information about currently playing track.",
handle_get_track_info, null);
bind("track-position", Drt.ApiFlags.READABLE, "Returns information about current track position.",
handle_get_track_position, null);
add_notification(TRACK_INFO_CHANGED, Drt.ApiFlags.WRITABLE|Drt.ApiFlags.SUBSCRIBE,
"Sends a notification when track info is changed.");
add_notification(TRACK_POSITION_CHANGED, Drt.ApiFlags.WRITABLE|Drt.ApiFlags.SUBSCRIBE,
"Sends a notification when track position is changed.");
model.set_rating.connect(on_set_rating);
}

Expand All @@ -72,7 +82,8 @@ public class Nuvola.MediaPlayerBinding: ModelBinding<MediaPlayerModel>
var artwork_location = params.pop_string();
var artwork_file = params.pop_string();
var rating = params.pop_double();
model.set_track_info(title, artist, album, state, artwork_location, artwork_file, rating);
var length = params.pop_double();
model.set_track_info(title, artist, album, state, artwork_location, artwork_file, rating, (int) length);

SList<string> playback_actions = null;
var actions = params.pop_strv();
Expand All @@ -99,6 +110,21 @@ public class Nuvola.MediaPlayerBinding: ModelBinding<MediaPlayerModel>
return builder.end();
}

private Variant? handle_set_track_position(GLib.Object source, Drt.ApiParams? params) throws Diorite.MessageError
{
check_not_empty();
var position = params.pop_double();
model.track_position = (int) position;
emit(TRACK_POSITION_CHANGED);
return new Variant.boolean(true);
}

private Variant? handle_get_track_position(GLib.Object source, Drt.ApiParams? params) throws Diorite.MessageError
{
check_not_empty();
return new Variant.double((double) model.track_position);
}

private Variant? handle_set_flag(GLib.Object source, Drt.ApiParams? params) throws Diorite.MessageError
{
check_not_empty();
Expand All @@ -113,6 +139,7 @@ public class Nuvola.MediaPlayerBinding: ModelBinding<MediaPlayerModel>
case "can-pause":
case "can-stop":
case "can-rate":
case "can-seek":
handled = true;
GLib.Value value = GLib.Value(typeof(bool));
value.set_boolean(state);
Expand All @@ -137,6 +164,7 @@ public class Nuvola.MediaPlayerBinding: ModelBinding<MediaPlayerModel>
case "can-pause":
case "can-stop":
case "can-rate":
case "can-seek":
GLib.Value value = GLib.Value(typeof(bool));
model.@get_property(name, ref value);
return new Variant.boolean(value.get_boolean());
Expand Down
15 changes: 13 additions & 2 deletions src/nuvolakit-runner/components/developer/DeveloperSidebar.vala
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2014-2015 Jiří Janoušek <[email protected]>
* Copyright 2014-2017 Jiří Janoušek <[email protected]>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
Expand Down Expand Up @@ -42,6 +42,7 @@ public class DeveloperSidebar: Gtk.ScrolledWindow
private Diorite.Actions? actions_reg;
private Gtk.Grid grid;
private Gtk.Image? artwork = null;
private Gtk.Label? position = null;
private Gtk.Label? song = null;
private Gtk.Label? artist = null;
private Gtk.Label? album = null;
Expand All @@ -63,9 +64,14 @@ public class DeveloperSidebar: Gtk.ScrolledWindow
artwork = new Gtk.Image();
clear_artwork(false);
grid.add(artwork);
position = new Gtk.Label(
Utils.format_track_time(player.track_position) + " / " + Utils.format_track_time(player.track_length));
position.set_line_wrap(true);
position.halign = Gtk.Align.CENTER;
grid.attach_next_to(position, artwork, Gtk.PositionType.BOTTOM, 1, 1);
var label = new HeaderLabel("Song");
label.halign = Gtk.Align.START;
grid.attach_next_to(label, artwork, Gtk.PositionType.BOTTOM, 1, 1);
grid.attach_next_to(label, position, Gtk.PositionType.BOTTOM, 1, 1);
song = new Gtk.Label(player.title ?? "(null)");
song.set_line_wrap(true);
song.halign = Gtk.Align.START;
Expand Down Expand Up @@ -162,6 +168,11 @@ public class DeveloperSidebar: Gtk.ScrolledWindow
case "state":
state.label = player.state ?? "(null)";
break;
case "track-length":
case "track-position":
position.label = Utils.format_track_time(player.track_position)
+ " / " + Utils.format_track_time(player.track_length);
break;
case "rating":
rating.label = player.rating >= 0.0 ? player.rating.to_string() : "(null)";
break;
Expand Down
17 changes: 13 additions & 4 deletions src/nuvolakit-runner/components/mediaplayer/MediaPlayer.vala
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2014-2015 Jiří Janoušek <[email protected]>
* Copyright 2014-2017 Jiří Janoušek <[email protected]>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
Expand Down Expand Up @@ -31,12 +31,15 @@ public class Nuvola.MediaPlayer: GLib.Object, Nuvola.MediaPlayerModel
public string? state {get; set; default = null;}
public string? artwork_location {get; set; default = null;}
public string? artwork_file {get; set; default = null;}
public int track_length {get; set; default = 0;}
public int track_position {get; set; default = 0;}
public bool can_go_next {get; set; default = false;}
public bool can_go_previous {get; set; default = false;}
public bool can_play {get; set; default = false;}
public bool can_pause {get; set; default = false;}
public bool can_stop {get; set; default = false;}
public bool can_rate {get; set; default = false;}
public bool can_seek {get; set; default = false;}
public SList<string> playback_actions {get; owned set;}
private Diorite.Actions actions;

Expand All @@ -47,7 +50,7 @@ public class Nuvola.MediaPlayer: GLib.Object, Nuvola.MediaPlayerModel

protected void handle_set_track_info(
string? title, string? artist, string? album, string? state, string? artwork_location, string? artwork_file,
double rating)
double rating, int length)
{
this.title = title;
this.artist = artist;
Expand All @@ -56,6 +59,7 @@ public class Nuvola.MediaPlayer: GLib.Object, Nuvola.MediaPlayerModel
this.state = state;
this.artwork_location = artwork_location;
this.artwork_file = artwork_file;
this.track_length = length;
}

public void play()
Expand Down Expand Up @@ -88,9 +92,14 @@ public class Nuvola.MediaPlayer: GLib.Object, Nuvola.MediaPlayerModel
activate_action("next-song");
}

private void activate_action(string name)
public void seek(int64 position)
{
if (!actions.activate_action(name))
activate_action("seek", position);
}

private void activate_action(string name, Variant? parameter=null)
{
if (!actions.activate_action(name, parameter))
critical("Failed to activate action '%s'.", name);
}
}
Loading

0 comments on commit 347085e

Please sign in to comment.