2024-10-13 15:21:29 +00:00
|
|
|
enum PlaybinState {
|
|
|
|
STOPPED,
|
|
|
|
PAUSED,
|
|
|
|
PLAYING,
|
|
|
|
}
|
|
|
|
|
2024-10-16 03:58:40 +00:00
|
|
|
class Playbin : GLib.Object {
|
2024-10-12 12:28:05 +00:00
|
|
|
// dynamic: undocumented vala feature
|
|
|
|
// lets us access the about-to-finish signal
|
2024-10-13 15:21:29 +00:00
|
|
|
private dynamic Gst.Element playbin = Gst.ElementFactory.make ("playbin3", null);
|
2024-10-12 12:28:05 +00:00
|
|
|
|
|
|
|
public double volume {
|
|
|
|
get {
|
|
|
|
return ((Gst.Audio.StreamVolume) this.playbin).get_volume (Gst.Audio.StreamVolumeFormat.CUBIC);
|
|
|
|
}
|
|
|
|
set {
|
|
|
|
((Gst.Audio.StreamVolume) this.playbin).set_volume (Gst.Audio.StreamVolumeFormat.CUBIC, value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public bool mute {
|
2024-10-13 09:56:28 +00:00
|
|
|
get { return this.playbin.mute; }
|
|
|
|
set { this.playbin.mute = value; }
|
2024-10-12 12:28:05 +00:00
|
|
|
}
|
|
|
|
|
2024-10-13 15:21:29 +00:00
|
|
|
public PlaybinState state { get; private set; default = PlaybinState.STOPPED; }
|
|
|
|
|
2024-10-16 11:02:32 +00:00
|
|
|
public Subsonic.Song? current_song { get; private set; default = null; }
|
2024-10-16 10:37:39 +00:00
|
|
|
|
2024-10-16 09:11:52 +00:00
|
|
|
// true if a timer should update the position property
|
2024-10-13 17:00:47 +00:00
|
|
|
private bool update_position = false;
|
2024-10-15 20:45:16 +00:00
|
|
|
public int64 position { get; private set; default = 0; }
|
2024-10-16 10:03:32 +00:00
|
|
|
public int64 duration { get; private set; default = 1; } // if 0, the seekbar vanishes
|
2024-10-13 17:00:47 +00:00
|
|
|
|
2024-10-16 11:02:32 +00:00
|
|
|
public Subsonic.Client api { get; set; default = null; }
|
2024-10-13 15:21:29 +00:00
|
|
|
|
2024-10-15 20:45:16 +00:00
|
|
|
// sent when a new song starts playing
|
2024-10-16 09:11:52 +00:00
|
|
|
// continues: whether the track is a gapless continuation
|
2024-10-16 10:37:39 +00:00
|
|
|
public signal void now_playing (bool continues);
|
2024-10-16 10:15:00 +00:00
|
|
|
public signal void stopped ();
|
2024-10-15 20:45:16 +00:00
|
|
|
|
2024-10-16 09:11:52 +00:00
|
|
|
// the index of the track in the play queue that is currently playing
|
|
|
|
// must equal play queue len iff state is STOPPED
|
|
|
|
public uint current_position { get; private set; }
|
2024-10-15 11:27:47 +00:00
|
|
|
|
2024-10-16 09:11:52 +00:00
|
|
|
// whether we are expecting a gapless continuation next
|
2024-10-15 11:27:47 +00:00
|
|
|
private bool next_gapless;
|
2024-10-12 12:28:05 +00:00
|
|
|
|
2024-10-13 09:56:28 +00:00
|
|
|
private void source_setup (Gst.Element playbin, dynamic Gst.Element source) {
|
2024-10-15 21:16:43 +00:00
|
|
|
source.user_agent = Audrey.Const.user_agent;
|
2024-10-12 21:11:55 +00:00
|
|
|
}
|
|
|
|
|
2024-10-15 20:45:16 +00:00
|
|
|
// ASSUMPTION: about-to-finish will be signalled exactly once per track
|
|
|
|
// even if seeking backwards after
|
2024-10-16 09:11:52 +00:00
|
|
|
private GLib.AsyncQueue<string> next_uri = new GLib.AsyncQueue<string> ();
|
2024-10-15 20:45:16 +00:00
|
|
|
|
|
|
|
private ListModel _play_queue = null;
|
|
|
|
private ulong _play_queue_items_changed;
|
|
|
|
public ListModel play_queue {
|
|
|
|
get { return _play_queue; }
|
|
|
|
set {
|
|
|
|
if (_play_queue != null) {
|
|
|
|
SignalHandler.disconnect (_play_queue, _play_queue_items_changed);
|
|
|
|
}
|
|
|
|
_play_queue = value;
|
|
|
|
_play_queue_items_changed = value.items_changed.connect (on_play_queue_items_changed);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// called when uri can be switched for gapless playback
|
|
|
|
// need async queue because this might be called from a gstreamer thread
|
|
|
|
private void about_to_finish (dynamic Gst.Element playbin) {
|
|
|
|
print ("about to finish\n");
|
|
|
|
|
|
|
|
// will block if the next uri isn't ready yet
|
|
|
|
// leaves the queue empty as per the ASSUMPTION above
|
|
|
|
string? next_uri = this.next_uri.pop ();
|
|
|
|
|
|
|
|
if (next_uri != "") {
|
|
|
|
playbin.uri = next_uri;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-15 20:29:14 +00:00
|
|
|
private void on_play_queue_items_changed (ListModel play_queue, uint position, uint removed, uint added) {
|
|
|
|
if (this.state == PlaybinState.STOPPED) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-10-16 09:11:52 +00:00
|
|
|
if (this.current_position >= position) {
|
|
|
|
if (this.current_position < position+removed) {
|
2024-10-15 20:29:14 +00:00
|
|
|
// current track was removed, start playing something else
|
|
|
|
// TODO check if it was actually reordered
|
|
|
|
|
2024-10-16 09:11:52 +00:00
|
|
|
if (position == play_queue.get_n_items ()) {
|
|
|
|
this.stop ();
|
|
|
|
} else {
|
|
|
|
this.select_track (position);
|
|
|
|
}
|
2024-10-15 20:29:14 +00:00
|
|
|
} else {
|
|
|
|
// unaffected
|
|
|
|
// fix up playing index though
|
2024-10-16 09:11:52 +00:00
|
|
|
this.current_position = this.current_position + added - removed;
|
2024-10-15 20:29:14 +00:00
|
|
|
}
|
2024-10-16 09:11:52 +00:00
|
|
|
} else if (this.current_position+1 == position) {
|
2024-10-15 20:29:14 +00:00
|
|
|
// next track was changed
|
|
|
|
// try to fix up gapless transition
|
|
|
|
string? next_uri = this.next_uri.try_pop ();
|
|
|
|
if (next_uri != null) {
|
|
|
|
// we're in luck, about-to-finish hasn't been triggered yet
|
|
|
|
// we can get away with replacing it
|
2024-10-16 09:11:52 +00:00
|
|
|
if (this.current_position+1 < play_queue.get_n_items ()) {
|
2024-10-16 11:02:32 +00:00
|
|
|
var song = (Subsonic.Song) play_queue.get_item (this.current_position+1);
|
2024-10-15 20:29:14 +00:00
|
|
|
this.next_uri.push (this.api.stream_uri (song.id));
|
|
|
|
} else {
|
|
|
|
this.next_uri.push ("");
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// about-to-finish already triggered
|
|
|
|
// we'll need to stop the new track when it starts playing
|
2024-10-15 20:45:16 +00:00
|
|
|
// but stream-start should already be able to take care of that
|
2024-10-15 20:29:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-10-15 11:27:47 +00:00
|
|
|
|
2024-10-15 20:29:14 +00:00
|
|
|
public Playbin () {
|
2024-10-15 11:27:47 +00:00
|
|
|
this.next_uri.push ("");
|
|
|
|
|
2024-10-13 09:56:28 +00:00
|
|
|
this.playbin.source_setup.connect (this.source_setup);
|
|
|
|
this.playbin.about_to_finish.connect (this.about_to_finish);
|
2024-10-12 12:28:05 +00:00
|
|
|
|
2024-10-15 20:29:14 +00:00
|
|
|
// regularly update position
|
2024-10-12 18:17:31 +00:00
|
|
|
Timeout.add (500, () => {
|
2024-10-13 17:00:47 +00:00
|
|
|
if (this.update_position) {
|
|
|
|
int64 new_position;
|
|
|
|
if (this.playbin.query_position (Gst.Format.TIME, out new_position)) {
|
2024-10-16 10:15:00 +00:00
|
|
|
if (new_position > this.duration) this.position = this.duration;
|
|
|
|
else this.position = new_position;
|
2024-10-13 17:00:47 +00:00
|
|
|
} else {
|
|
|
|
this.position = 0;
|
|
|
|
}
|
2024-10-12 12:28:05 +00:00
|
|
|
}
|
|
|
|
|
2024-10-12 18:17:31 +00:00
|
|
|
// keep rerunning
|
2024-10-12 12:28:05 +00:00
|
|
|
return true;
|
|
|
|
});
|
|
|
|
|
2024-10-13 09:56:28 +00:00
|
|
|
var bus = this.playbin.get_bus ();
|
|
|
|
bus.add_signal_watch ();
|
2024-10-12 12:28:05 +00:00
|
|
|
|
2024-10-13 09:56:28 +00:00
|
|
|
bus.message["error"].connect ((message) => {
|
|
|
|
Error err;
|
|
|
|
string? debug;
|
|
|
|
message.parse_error (out err, out debug);
|
|
|
|
error ("gst playbin bus error: %s", err.message);
|
|
|
|
});
|
2024-10-12 12:28:05 +00:00
|
|
|
|
2024-10-13 09:56:28 +00:00
|
|
|
bus.message["warning"].connect ((message) => {
|
|
|
|
Error err;
|
|
|
|
string? debug;
|
|
|
|
message.parse_error (out err, out debug);
|
|
|
|
warning ("gst playbin bus warning: %s", err.message);
|
|
|
|
});
|
|
|
|
|
|
|
|
bus.message["stream-start"].connect ((message) => {
|
2024-10-15 20:29:14 +00:00
|
|
|
int64 duration;
|
|
|
|
assert (this.playbin.query_duration (Gst.Format.TIME, out duration));
|
2024-10-16 10:03:32 +00:00
|
|
|
this.duration = duration;
|
2024-10-13 17:00:47 +00:00
|
|
|
|
2024-10-16 20:24:48 +00:00
|
|
|
// cancel any queued seeks
|
|
|
|
this.queued_seek = -1;
|
|
|
|
this.update_position = true;
|
|
|
|
|
2024-10-13 17:00:47 +00:00
|
|
|
this.position = 0;
|
|
|
|
|
2024-10-16 08:48:52 +00:00
|
|
|
bool continues = this.next_gapless;
|
2024-10-15 11:27:47 +00:00
|
|
|
if (this.next_gapless) {
|
|
|
|
// advance position in play queue
|
2024-10-16 09:11:52 +00:00
|
|
|
this.current_position += 1;
|
2024-10-13 15:21:29 +00:00
|
|
|
} else {
|
2024-10-15 11:27:47 +00:00
|
|
|
this.next_gapless = true;
|
|
|
|
}
|
|
|
|
|
2024-10-16 11:02:32 +00:00
|
|
|
var now_playing = (Subsonic.Song) play_queue.get_item (this.current_position);
|
2024-10-15 20:45:16 +00:00
|
|
|
if (this.api.stream_uri (now_playing.id) == (string) this.playbin.current_uri) {
|
2024-10-16 10:03:32 +00:00
|
|
|
if (continues) {
|
2024-10-16 10:37:39 +00:00
|
|
|
this.current_song = now_playing;
|
|
|
|
this.now_playing (true);
|
2024-10-16 10:03:32 +00:00
|
|
|
}
|
2024-10-15 11:27:47 +00:00
|
|
|
|
2024-10-16 09:11:52 +00:00
|
|
|
if (this.current_position+1 < play_queue.get_n_items ()) {
|
2024-10-16 11:02:32 +00:00
|
|
|
var song = (Subsonic.Song) play_queue.get_item (this.current_position+1);
|
2024-10-15 20:45:16 +00:00
|
|
|
this.next_uri.push (this.api.stream_uri (song.id));
|
|
|
|
} else {
|
|
|
|
this.next_uri.push ("");
|
|
|
|
}
|
2024-10-15 11:27:47 +00:00
|
|
|
} else {
|
2024-10-15 20:45:16 +00:00
|
|
|
// edge case
|
2024-10-15 20:48:41 +00:00
|
|
|
// just flush everything and pray next stream-start is fine
|
2024-10-16 09:11:52 +00:00
|
|
|
this.select_track (this.current_position);
|
2024-10-13 15:21:29 +00:00
|
|
|
}
|
2024-10-12 12:28:05 +00:00
|
|
|
});
|
2024-10-13 16:04:59 +00:00
|
|
|
|
2024-10-13 17:00:47 +00:00
|
|
|
bus.message["state-changed"].connect ((message) => {
|
|
|
|
if (message.src != this.playbin) return;
|
|
|
|
|
|
|
|
Gst.State new_state;
|
|
|
|
message.parse_state_changed (null, out new_state, null);
|
|
|
|
|
2024-10-16 20:24:48 +00:00
|
|
|
if (new_state == Gst.State.PLAYING) {
|
|
|
|
if (queued_seek != -1) {
|
|
|
|
if (this.playbin.seek_simple (Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE, this.queued_seek)) {
|
|
|
|
this.queued_seek = -1;
|
|
|
|
} else {
|
|
|
|
warning ("could not reapply queued seek after state changed changed to playing, retrying later");
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
this.update_position = true;
|
|
|
|
}
|
|
|
|
}
|
2024-10-13 16:04:59 +00:00
|
|
|
});
|
2024-10-13 09:56:28 +00:00
|
|
|
|
|
|
|
bus.message["eos"].connect ((message) => {
|
2024-10-16 10:15:00 +00:00
|
|
|
this.stop ();
|
2024-10-13 09:56:28 +00:00
|
|
|
});
|
2024-10-12 12:28:05 +00:00
|
|
|
}
|
|
|
|
|
2024-10-16 20:24:48 +00:00
|
|
|
private int64 queued_seek = -1;
|
|
|
|
|
2024-10-13 10:40:29 +00:00
|
|
|
public void seek (int64 position) {
|
2024-10-16 20:24:48 +00:00
|
|
|
this.position = position;
|
|
|
|
this.update_position = false;
|
2024-10-16 19:45:50 +00:00
|
|
|
if (!this.playbin.seek_simple (Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE, position)) {
|
2024-10-16 20:24:48 +00:00
|
|
|
// try to queue this seek for later
|
|
|
|
queued_seek = position;
|
2024-10-12 12:28:05 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-16 09:11:52 +00:00
|
|
|
// manually changes which track in the play queue to play
|
|
|
|
public void select_track (uint position)
|
|
|
|
requires (position < this.play_queue.get_n_items ())
|
|
|
|
{
|
2024-10-15 11:27:47 +00:00
|
|
|
this.state = PlaybinState.PLAYING;
|
|
|
|
|
2024-10-16 09:11:52 +00:00
|
|
|
this.current_position = position;
|
2024-10-13 10:40:29 +00:00
|
|
|
this.playbin.set_state (Gst.State.READY);
|
2024-10-16 11:02:32 +00:00
|
|
|
var song = (Subsonic.Song) this.play_queue.get_item (position);
|
2024-10-16 10:03:32 +00:00
|
|
|
this.playbin.uri = this.api.stream_uri (song.id);
|
2024-10-13 10:40:29 +00:00
|
|
|
this.playbin.set_state (Gst.State.PLAYING);
|
2024-10-15 11:27:47 +00:00
|
|
|
this.next_gapless = false;
|
|
|
|
|
2024-10-15 20:45:16 +00:00
|
|
|
// make sure the queue is empty, so next stream-changed can fix it up
|
|
|
|
this.next_uri.try_pop ();
|
|
|
|
// if it was already empty then uhhhh if theres any problems then
|
|
|
|
// playbin.uri wont match up with the current track's stream uri and we can
|
|
|
|
// fix it there
|
2024-10-16 10:03:32 +00:00
|
|
|
|
2024-10-16 10:37:39 +00:00
|
|
|
this.current_song = song;
|
2024-10-16 10:03:32 +00:00
|
|
|
this.position = 0;
|
2024-10-16 19:26:31 +00:00
|
|
|
this.duration = song.duration * Gst.SECOND - 1;
|
2024-10-16 10:51:01 +00:00
|
|
|
this.now_playing (false);
|
2024-10-12 12:28:05 +00:00
|
|
|
}
|
2024-10-12 13:36:47 +00:00
|
|
|
|
|
|
|
public void pause () {
|
2024-10-13 15:21:29 +00:00
|
|
|
assert (this.state != PlaybinState.STOPPED);
|
2024-10-12 13:36:47 +00:00
|
|
|
this.playbin.set_state (Gst.State.PAUSED);
|
2024-10-13 15:21:29 +00:00
|
|
|
this.state = PlaybinState.PAUSED;
|
2024-10-12 13:36:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public void play () {
|
2024-10-13 15:21:29 +00:00
|
|
|
assert (this.state != PlaybinState.STOPPED);
|
2024-10-12 13:36:47 +00:00
|
|
|
this.playbin.set_state (Gst.State.PLAYING);
|
2024-10-13 15:21:29 +00:00
|
|
|
this.state = PlaybinState.PLAYING;
|
|
|
|
}
|
2024-10-16 09:11:52 +00:00
|
|
|
|
|
|
|
public void stop () {
|
|
|
|
this.playbin.set_state (Gst.State.READY);
|
|
|
|
this.state = PlaybinState.STOPPED;
|
|
|
|
this.current_position = this.play_queue.get_n_items ();
|
2024-10-16 10:15:00 +00:00
|
|
|
this.stopped ();
|
|
|
|
|
|
|
|
this.position = 0;
|
|
|
|
this.duration = 1;
|
2024-10-16 10:49:06 +00:00
|
|
|
this.current_song = null;
|
2024-10-16 09:11:52 +00:00
|
|
|
}
|
2024-10-12 12:28:05 +00:00
|
|
|
}
|