2024-10-19 11:29:39 +00:00
|
|
|
public enum PlaybinState {
|
2024-10-13 15:21:29 +00:00
|
|
|
STOPPED,
|
|
|
|
PAUSED,
|
|
|
|
PLAYING,
|
|
|
|
}
|
|
|
|
|
2024-10-25 20:09:57 +00:00
|
|
|
private struct CommandCallback {
|
|
|
|
unowned SourceFunc callback;
|
|
|
|
int error;
|
|
|
|
}
|
|
|
|
|
2024-10-19 11:29:39 +00:00
|
|
|
public class Playbin : GLib.Object {
|
2024-10-17 10:05:20 +00:00
|
|
|
private Mpv.Handle mpv = new Mpv.Handle ();
|
2024-10-12 12:28:05 +00:00
|
|
|
|
2024-10-17 10:05:20 +00:00
|
|
|
public PlaybinState state { get; private set; default = PlaybinState.STOPPED; }
|
|
|
|
|
|
|
|
private int _volume = 100;
|
|
|
|
public int volume {
|
|
|
|
get { return _volume; }
|
2024-10-12 12:28:05 +00:00
|
|
|
set {
|
2024-10-18 19:54:12 +00:00
|
|
|
var ret = mpv.set_property_int64 ("volume", value);
|
|
|
|
if (ret >= 0) {
|
|
|
|
_volume = value;
|
|
|
|
} else {
|
|
|
|
warning ("failed to set volume: %s", ret.to_string ());
|
|
|
|
}
|
2024-10-12 12:28:05 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-17 10:05:20 +00:00
|
|
|
public bool _mute = false;
|
2024-10-12 12:28:05 +00:00
|
|
|
public bool mute {
|
2024-10-17 10:05:20 +00:00
|
|
|
get { return _mute; }
|
|
|
|
set {
|
2024-10-18 19:54:12 +00:00
|
|
|
var ret = mpv.set_property_flag ("mute", value);
|
|
|
|
if (ret >= 0) {
|
|
|
|
_mute = value;
|
|
|
|
} else {
|
|
|
|
warning ("failed to set mute status: %s", ret.to_string ());
|
|
|
|
}
|
2024-10-17 10:05:20 +00:00
|
|
|
}
|
2024-10-12 12:28:05 +00:00
|
|
|
}
|
|
|
|
|
2024-10-20 11:17:40 +00:00
|
|
|
// invariant: equal to play queue length iff state is STOPPED
|
2024-10-17 10:05:20 +00:00
|
|
|
public uint play_queue_position { get; private set; }
|
2024-10-18 20:17:04 +00:00
|
|
|
|
2024-10-20 11:17:40 +00:00
|
|
|
// signalled when a new track is current
|
|
|
|
public signal void new_track ();
|
|
|
|
// signalled when the last track is over
|
2024-10-18 20:32:21 +00:00
|
|
|
public signal void stopped ();
|
2024-10-16 10:37:39 +00:00
|
|
|
|
2024-10-20 11:17:40 +00:00
|
|
|
// these are mostly synced with mpv
|
2024-10-17 10:05:20 +00:00
|
|
|
public double position { get; private set; default = 0.0; }
|
|
|
|
public double duration { get; private set; default = 0.0; }
|
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-20 11:17:40 +00:00
|
|
|
public ListStore _play_queue;
|
|
|
|
public ListModel play_queue { get { return this._play_queue; } }
|
2024-10-15 20:45:16 +00:00
|
|
|
|
2024-10-20 11:17:40 +00:00
|
|
|
// try to prevent wait_event to be called twice
|
|
|
|
private bool is_handling_event = false;
|
2024-10-17 06:06:09 +00:00
|
|
|
|
2024-10-25 20:09:57 +00:00
|
|
|
private async Mpv.Error mpv_command_async (string[] args) {
|
|
|
|
CommandCallback cc = {};
|
|
|
|
|
|
|
|
this.mpv.command_async ((uint64) &cc, args);
|
|
|
|
|
|
|
|
cc.callback = this.mpv_command_async.callback;
|
|
|
|
yield;
|
|
|
|
return cc.error;
|
|
|
|
}
|
|
|
|
|
2024-10-17 10:05:20 +00:00
|
|
|
public Playbin () {
|
2024-10-20 11:17:40 +00:00
|
|
|
this._play_queue = new ListStore (typeof (Subsonic.Song));
|
2024-10-19 11:29:39 +00:00
|
|
|
|
2024-10-17 20:08:21 +00:00
|
|
|
assert (this.mpv.initialize () >= 0);
|
|
|
|
assert (this.mpv.set_property_string ("user-agent", Audrey.Const.user_agent) >= 0);
|
|
|
|
assert (this.mpv.set_property_string ("video", "no") >= 0);
|
|
|
|
assert (this.mpv.set_property_string ("prefetch-playlist", "yes") >= 0);
|
|
|
|
assert (this.mpv.set_property_string ("gapless-audio", "yes") >= 0);
|
|
|
|
assert (this.mpv.observe_property (0, "time-pos", Mpv.Format.DOUBLE) >= 0);
|
|
|
|
assert (this.mpv.observe_property (1, "duration", Mpv.Format.DOUBLE) >= 0);
|
|
|
|
assert (this.mpv.observe_property (2, "playlist-pos", Mpv.Format.INT64) >= 0);
|
2024-10-20 11:17:40 +00:00
|
|
|
assert (this.mpv.observe_property (3, "pause", Mpv.Format.FLAG) >= 0);
|
2024-10-17 10:05:20 +00:00
|
|
|
|
|
|
|
this.mpv.wakeup_callback = () => {
|
|
|
|
Idle.add (() => {
|
2024-10-20 11:17:40 +00:00
|
|
|
if (this.is_handling_event) return false;
|
|
|
|
this.is_handling_event = true;
|
|
|
|
|
2024-10-17 10:05:20 +00:00
|
|
|
while (true) {
|
|
|
|
var event = this.mpv.wait_event (0.0);
|
|
|
|
if (event.event_id == Mpv.EventId.NONE) break;
|
|
|
|
|
|
|
|
switch (event.event_id) {
|
|
|
|
case Mpv.EventId.PROPERTY_CHANGE:
|
2024-10-17 20:32:51 +00:00
|
|
|
var data = event.parse_property ();
|
2024-10-17 10:05:20 +00:00
|
|
|
switch (event.reply_userdata) {
|
|
|
|
case 0:
|
|
|
|
assert (data.name == "time-pos");
|
|
|
|
if (data.format == Mpv.Format.NONE) {
|
|
|
|
this.position = 0.0;
|
|
|
|
} else {
|
2024-10-17 20:32:51 +00:00
|
|
|
this.position = data.parse_double ();
|
2024-10-17 10:05:20 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 1:
|
|
|
|
assert (data.name == "duration");
|
|
|
|
if (data.format == Mpv.Format.NONE) {
|
2024-10-20 12:45:37 +00:00
|
|
|
// this.duration = 0.0; i think this prevents the fallback below from working
|
2024-10-17 10:05:20 +00:00
|
|
|
} else {
|
2024-10-17 20:32:51 +00:00
|
|
|
this.duration = data.parse_double ();
|
2024-10-17 10:05:20 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 2:
|
2024-10-20 11:17:40 +00:00
|
|
|
// here as a sanity check
|
|
|
|
// should always match our own play_queu_position/state
|
2024-10-17 10:05:20 +00:00
|
|
|
assert (data.name == "playlist-pos");
|
2024-10-20 11:17:40 +00:00
|
|
|
int64 playlist_pos = data.parse_int64 ();
|
|
|
|
if (playlist_pos < 0) {
|
|
|
|
if (this.state != PlaybinState.STOPPED) {
|
|
|
|
error ("mpv has no current playlist entry, but we think it's index %u", this.play_queue_position);
|
|
|
|
}
|
|
|
|
assert (this.play_queue_position == this.play_queue.get_n_items ());
|
2024-10-17 10:05:20 +00:00
|
|
|
} else {
|
2024-10-20 11:17:40 +00:00
|
|
|
if (this.state == PlaybinState.STOPPED) {
|
|
|
|
error ("mpv is at playlist entry %u, but we're stopped", (uint) playlist_pos);
|
|
|
|
}
|
|
|
|
if (this.play_queue_position != (uint) playlist_pos) {
|
|
|
|
error ("mpv is at playlist entry %u, but we think it's %u", (uint) playlist_pos, this.play_queue_position);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 3:
|
|
|
|
// also here as a sanity check
|
|
|
|
// should always match our own state
|
|
|
|
assert (data.name == "pause");
|
|
|
|
bool pause = data.parse_flag ();
|
|
|
|
if (pause && this.state != PlaybinState.PAUSED) {
|
|
|
|
error (@"mpv is paused, but we are @(this.state)");
|
|
|
|
}
|
|
|
|
if (!pause && this.state == PlaybinState.PAUSED) {
|
|
|
|
error ("mpv is not paused, but we are paused");
|
2024-10-17 10:05:20 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
assert (false);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
2024-10-18 20:27:24 +00:00
|
|
|
case Mpv.EventId.START_FILE:
|
|
|
|
debug ("START_FILE received");
|
2024-10-20 11:26:36 +00:00
|
|
|
|
|
|
|
// estimate duration from api data
|
|
|
|
// while mpv doesn't know it
|
|
|
|
this.duration = ((Subsonic.Song) this._play_queue.get_item (this.play_queue_position)).duration;
|
|
|
|
|
2024-10-20 11:17:40 +00:00
|
|
|
this.new_track ();
|
2024-10-17 10:05:20 +00:00
|
|
|
break;
|
2024-10-18 20:13:28 +00:00
|
|
|
|
|
|
|
case Mpv.EventId.END_FILE:
|
|
|
|
var data = event.parse_end_file ();
|
2024-10-20 11:32:15 +00:00
|
|
|
debug (@"END_FILE received (reason: $(data.reason))");
|
|
|
|
|
2024-10-18 20:13:28 +00:00
|
|
|
if (data.error < 0) {
|
|
|
|
warning ("playback of track aborted: %s", data.error.to_string ());
|
|
|
|
}
|
2024-10-20 11:17:40 +00:00
|
|
|
|
2024-10-20 11:32:15 +00:00
|
|
|
if (data.reason == Mpv.EndFileReason.EOF) {
|
|
|
|
// assume this is a proper transition
|
2024-10-20 11:17:40 +00:00
|
|
|
this.play_queue_position += 1;
|
2024-10-20 11:32:15 +00:00
|
|
|
|
|
|
|
if (this.play_queue_position == this._play_queue.get_n_items ()) {
|
|
|
|
// reached the end (?)
|
|
|
|
this.state = PlaybinState.STOPPED;
|
|
|
|
this.stopped ();
|
|
|
|
}
|
2024-10-20 11:17:40 +00:00
|
|
|
}
|
|
|
|
|
2024-10-18 20:13:28 +00:00
|
|
|
break;
|
2024-10-25 20:09:57 +00:00
|
|
|
|
|
|
|
case Mpv.EventId.COMMAND_REPLY:
|
|
|
|
unowned CommandCallback *cc = (CommandCallback *) event.reply_userdata;
|
|
|
|
cc.error = event.error;
|
|
|
|
cc.callback ();
|
|
|
|
break;
|
2024-10-17 10:05:20 +00:00
|
|
|
|
|
|
|
default:
|
2024-10-17 20:32:51 +00:00
|
|
|
// ignore by default
|
2024-10-17 10:05:20 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-20 11:17:40 +00:00
|
|
|
this.is_handling_event = false;
|
2024-10-17 10:05:20 +00:00
|
|
|
return false;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
public void seek (double position) {
|
2024-10-20 11:35:11 +00:00
|
|
|
var rc = this.mpv.command ({"seek", position.to_string (), "absolute"});
|
|
|
|
if (rc < 0) {
|
|
|
|
warning (@"could not seek to $position: $rc");
|
|
|
|
} else {
|
|
|
|
this.position = 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-17 20:08:21 +00:00
|
|
|
assert (this.mpv.command ({"playlist-play-index", position.to_string ()}) >= 0);
|
2024-10-15 11:27:47 +00:00
|
|
|
this.state = PlaybinState.PLAYING;
|
2024-10-20 11:17:40 +00:00
|
|
|
this.play_queue_position = position;
|
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);
|
|
|
|
this.state = PlaybinState.PAUSED;
|
2024-10-19 03:09:24 +00:00
|
|
|
debug ("setting state to paused");
|
2024-10-18 08:57:16 +00:00
|
|
|
// TODO: abstract away this handling around mpv api a bit for auto debug printing
|
|
|
|
var ret = this.mpv.set_property_flag("pause", true);
|
|
|
|
if (ret != 0) {
|
2024-10-19 03:09:24 +00:00
|
|
|
debug (@"failed to set state to paused ($(ret)): $(ret.to_string())");
|
2024-10-18 08:57:16 +00:00
|
|
|
}
|
2024-10-12 13:36:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public void play () {
|
2024-10-13 15:21:29 +00:00
|
|
|
assert (this.state != PlaybinState.STOPPED);
|
|
|
|
this.state = PlaybinState.PLAYING;
|
2024-10-19 03:09:24 +00:00
|
|
|
debug ("setting state to playing");
|
2024-10-18 08:57:16 +00:00
|
|
|
var ret = this.mpv.set_property_flag("pause", false);
|
|
|
|
if (ret != 0) {
|
2024-10-19 03:09:24 +00:00
|
|
|
debug (@"failed to set state to playing ($(ret)): $(ret.to_string())");
|
2024-10-18 08:57:16 +00:00
|
|
|
}
|
2024-10-13 15:21:29 +00:00
|
|
|
}
|
2024-10-16 09:11:52 +00:00
|
|
|
|
2024-10-20 11:17:40 +00:00
|
|
|
public void go_to_next_track ()
|
|
|
|
requires (this.state != PlaybinState.STOPPED)
|
|
|
|
{
|
|
|
|
if (this.play_queue_position+1 < this._play_queue.get_n_items ()) {
|
|
|
|
this.play_queue_position += 1;
|
|
|
|
assert (this.mpv.command ({"playlist-next-playlist"}) >= 0);
|
|
|
|
} else {
|
|
|
|
warning ("tried to skip forward at end of play queue, ignoring");
|
|
|
|
}
|
2024-10-17 10:05:20 +00:00
|
|
|
}
|
|
|
|
|
2024-10-20 11:17:40 +00:00
|
|
|
public void go_to_prev_track ()
|
|
|
|
requires (this.state != PlaybinState.STOPPED)
|
|
|
|
{
|
|
|
|
if (this.play_queue_position > 0) {
|
|
|
|
this.play_queue_position -= 1;
|
|
|
|
assert (this.mpv.command ({"playlist-prev-playlist"}) >= 0);
|
|
|
|
} else {
|
|
|
|
warning ("tried to skip to prev track at start of play queue, ignoring");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public void remove_track (uint position)
|
|
|
|
requires (position < this._play_queue.get_n_items ())
|
|
|
|
{
|
2024-10-20 11:45:47 +00:00
|
|
|
assert (this.mpv.command({"playlist-remove", position.to_string ()}) >= 0);
|
|
|
|
this._play_queue.remove (position);
|
|
|
|
if (this.play_queue_position > position) this.play_queue_position -= 1;
|
|
|
|
if (this.play_queue_position == this._play_queue.get_n_items ()) {
|
|
|
|
// we just killed the last track
|
|
|
|
this.state = PlaybinState.STOPPED;
|
|
|
|
this.stopped ();
|
|
|
|
}
|
2024-10-20 11:17:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public void clear () {
|
|
|
|
assert (this.mpv.command({"playlist-clear"}) >= 0);
|
|
|
|
if (this.state != PlaybinState.STOPPED) {
|
|
|
|
assert (this.mpv.command({"playlist-remove", "current"}) >= 0);
|
|
|
|
}
|
|
|
|
this.state = PlaybinState.STOPPED;
|
|
|
|
this._play_queue.remove_all ();
|
|
|
|
this.play_queue_position = 0;
|
|
|
|
|
|
|
|
this.stopped ();
|
|
|
|
}
|
|
|
|
|
|
|
|
public void append_track (Subsonic.Song song) {
|
2024-10-25 20:09:57 +00:00
|
|
|
assert (this.mpv.command ({
|
2024-10-20 11:17:40 +00:00
|
|
|
"loadfile",
|
|
|
|
this.api.stream_uri (song.id),
|
|
|
|
"append",
|
|
|
|
}) >= 0);
|
|
|
|
if (this.state == STOPPED) this.play_queue_position += 1;
|
|
|
|
this._play_queue.append (song);
|
2024-10-16 09:11:52 +00:00
|
|
|
}
|
2024-10-20 12:10:02 +00:00
|
|
|
|
2024-10-25 20:09:57 +00:00
|
|
|
public async void append_track_async (Subsonic.Song song) {
|
|
|
|
var err = yield this.mpv_command_async ({
|
|
|
|
"loadfile",
|
|
|
|
this.api.stream_uri (song.id),
|
|
|
|
"append",
|
|
|
|
});
|
|
|
|
assert (err >= 0);
|
|
|
|
|
|
|
|
if (this.state == STOPPED) this.play_queue_position += 1;
|
|
|
|
this._play_queue.append (song);
|
|
|
|
}
|
|
|
|
|
2024-10-20 12:10:02 +00:00
|
|
|
public void move_track (uint from, uint to)
|
|
|
|
requires (from < this._play_queue.get_n_items ())
|
|
|
|
requires (to < this._play_queue.get_n_items ())
|
|
|
|
{
|
|
|
|
debug (@"moving track $from to $to");
|
|
|
|
|
|
|
|
if (from < to) {
|
|
|
|
// why offset to? because if the playlist is 01234,
|
|
|
|
// mpv takes "move 1 to 3" to mean 02134, not 02314
|
|
|
|
// that is, the target is a "gap", not a playlist entry
|
|
|
|
// from -> 0 1 2 3 4 5
|
|
|
|
// to -> 0 1 2 3 4 5 6
|
|
|
|
assert(this.mpv.command({
|
|
|
|
"playlist-move",
|
|
|
|
from.to_string (),
|
|
|
|
(to+1).to_string (),
|
|
|
|
}) >= 0);
|
|
|
|
|
|
|
|
// F0123T -> 0123TF
|
|
|
|
var additions = new Object[to-from+1];
|
|
|
|
for (uint i = from+1; i < to; i += 1) {
|
|
|
|
additions[i-from-1] = this._play_queue.get_item (i);
|
|
|
|
}
|
|
|
|
additions[to-from-1] = this._play_queue.get_item (to);
|
|
|
|
additions[to-from] = this._play_queue.get_item (from);
|
|
|
|
this._play_queue.splice(from, to-from+1, additions);
|
|
|
|
|
|
|
|
if (this.play_queue_position == from) this.play_queue_position = to;
|
|
|
|
else if (this.play_queue_position > from && this.play_queue_position <= to) this.play_queue_position -= 1;
|
|
|
|
} else if (from > to) {
|
|
|
|
assert(this.mpv.command({
|
|
|
|
"playlist-move",
|
|
|
|
from.to_string (),
|
|
|
|
to.to_string (),
|
|
|
|
}) >= 0);
|
|
|
|
|
|
|
|
// T0123F -> FT0123
|
|
|
|
var additions = new Object[from-to+1];
|
|
|
|
additions[0] = this._play_queue.get_item (from);
|
|
|
|
for (uint i = to; i < from; i += 1) {
|
|
|
|
additions[i-to+1] = this._play_queue.get_item (i);
|
|
|
|
}
|
|
|
|
this._play_queue.splice (to, from-to+1, additions);
|
|
|
|
|
|
|
|
if (this.play_queue_position == from) this.play_queue_position = to;
|
|
|
|
else if (this.play_queue_position >= to && this.play_queue_position < from) this.play_queue_position += 1;
|
|
|
|
}
|
|
|
|
}
|
2024-10-12 12:28:05 +00:00
|
|
|
}
|