2024-10-13 15:21:29 +00:00
|
|
|
enum PlaybinState {
|
|
|
|
STOPPED,
|
|
|
|
PAUSED,
|
|
|
|
PLAYING,
|
|
|
|
}
|
|
|
|
|
2024-10-17 10:05:20 +00:00
|
|
|
errordomain PlaybinError {
|
|
|
|
MPV,
|
|
|
|
}
|
|
|
|
|
|
|
|
private void check_mpv_error (int ec) throws PlaybinError {
|
|
|
|
if (ec < 0) {
|
|
|
|
throw new PlaybinError.MPV ("%s", Mpv.error_string (ec));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private class SourceFuncWrapper {
|
|
|
|
public SourceFunc inner;
|
|
|
|
|
|
|
|
public SourceFuncWrapper () {
|
|
|
|
this.inner = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-16 03:58:40 +00:00
|
|
|
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-17 10:05:20 +00:00
|
|
|
_volume = value;
|
|
|
|
mpv_set_property_int64.begin ("volume", value);
|
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 {
|
|
|
|
_mute = value;
|
|
|
|
mpv_set_property_flag.begin ("mute", value);
|
|
|
|
}
|
2024-10-12 12:28:05 +00:00
|
|
|
}
|
|
|
|
|
2024-10-17 10:05:20 +00:00
|
|
|
public uint play_queue_position { get; private set; }
|
|
|
|
public Subsonic.Song? song { get; private set; }
|
|
|
|
public signal void now_playing ();
|
2024-10-16 10:37:39 +00:00
|
|
|
|
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-15 20:45:16 +00:00
|
|
|
private ListModel _play_queue = null;
|
|
|
|
public ListModel play_queue {
|
|
|
|
get { return _play_queue; }
|
|
|
|
set {
|
2024-10-17 10:05:20 +00:00
|
|
|
assert (_play_queue == null); // only set this once
|
2024-10-15 20:45:16 +00:00
|
|
|
_play_queue = value;
|
2024-10-17 10:05:20 +00:00
|
|
|
value.items_changed.connect (on_play_queue_items_changed);
|
2024-10-15 20:45:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-17 10:05:20 +00:00
|
|
|
private void on_play_queue_items_changed (ListModel play_queue, uint position, uint removed, uint added) {
|
|
|
|
// FIXME: these should prolly be chained
|
2024-10-15 20:45:16 +00:00
|
|
|
|
2024-10-17 10:05:20 +00:00
|
|
|
for (uint i = 0; i < removed; i += 1) {
|
|
|
|
this.mpv_command.begin ({"playlist-remove", position.to_string ()});
|
2024-10-15 20:45:16 +00:00
|
|
|
}
|
|
|
|
|
2024-10-17 10:05:20 +00:00
|
|
|
for (uint i = 0; i < added; i += 1) {
|
|
|
|
this.mpv_command.begin ({"loadfile", this.api.stream_uri (((Subsonic.Song) play_queue.get_item (position+i)).id), "insert-at-play", (position+i).to_string ()});
|
2024-10-15 20:29:14 +00:00
|
|
|
}
|
2024-10-17 10:05:20 +00:00
|
|
|
}
|
2024-10-15 20:29:14 +00:00
|
|
|
|
2024-10-17 10:05:20 +00:00
|
|
|
private SourceFuncWrapper[] mpv_command_callbacks = {};
|
|
|
|
private int[] mpv_command_error = {};
|
2024-10-15 20:29:14 +00:00
|
|
|
|
2024-10-17 10:05:20 +00:00
|
|
|
private async void mpv_command (string[] args) throws Error {
|
|
|
|
int userdata = -1;
|
|
|
|
for (int i = 0; i < this.mpv_command_callbacks.length; i += 1) {
|
|
|
|
if (this.mpv_command_callbacks[i].inner == null) {
|
|
|
|
userdata = i;
|
|
|
|
break;
|
2024-10-15 20:29:14 +00:00
|
|
|
}
|
|
|
|
}
|
2024-10-17 10:05:20 +00:00
|
|
|
if (userdata == -1) {
|
|
|
|
userdata = this.mpv_command_callbacks.length;
|
|
|
|
this.mpv_command_callbacks += new SourceFuncWrapper ();
|
|
|
|
this.mpv_command_error += 0;
|
|
|
|
}
|
2024-10-15 11:27:47 +00:00
|
|
|
|
2024-10-17 10:05:20 +00:00
|
|
|
check_mpv_error (this.mpv.command_async ((uint64) userdata, args));
|
|
|
|
this.mpv_command_callbacks[userdata].inner = this.mpv_command.callback;
|
|
|
|
yield;
|
2024-10-17 06:06:09 +00:00
|
|
|
|
2024-10-17 10:05:20 +00:00
|
|
|
check_mpv_error (this.mpv_command_error[userdata]);
|
|
|
|
}
|
2024-10-15 11:27:47 +00:00
|
|
|
|
2024-10-17 10:05:20 +00:00
|
|
|
private SourceFuncWrapper[] mpv_set_property_callbacks = {};
|
|
|
|
private int[] mpv_set_property_error = {};
|
2024-10-12 12:28:05 +00:00
|
|
|
|
2024-10-17 10:05:20 +00:00
|
|
|
private async void mpv_set_property (string name, Mpv.Format format, void *data) throws Error {
|
|
|
|
int userdata = -1;
|
|
|
|
for (int i = 0; i < this.mpv_set_property_callbacks.length; i += 1) {
|
|
|
|
if (this.mpv_set_property_callbacks[i].inner == null) {
|
|
|
|
userdata = i;
|
|
|
|
break;
|
2024-10-12 12:28:05 +00:00
|
|
|
}
|
2024-10-17 10:05:20 +00:00
|
|
|
}
|
|
|
|
if (userdata == -1) {
|
|
|
|
userdata = this.mpv_set_property_callbacks.length;
|
|
|
|
this.mpv_set_property_callbacks += new SourceFuncWrapper ();
|
|
|
|
this.mpv_set_property_error += 0;
|
|
|
|
}
|
2024-10-12 12:28:05 +00:00
|
|
|
|
2024-10-17 10:05:20 +00:00
|
|
|
check_mpv_error (this.mpv.set_property_async ((uint64) userdata, name, format, data));
|
|
|
|
this.mpv_set_property_callbacks[userdata].inner = this.mpv_set_property.callback;
|
|
|
|
yield;
|
2024-10-15 11:27:47 +00:00
|
|
|
|
2024-10-17 10:05:20 +00:00
|
|
|
check_mpv_error (this.mpv_set_property_error[userdata]);
|
|
|
|
}
|
2024-10-15 11:27:47 +00:00
|
|
|
|
2024-10-17 10:05:20 +00:00
|
|
|
private async void mpv_set_property_int64 (string name, int64 value) throws Error {
|
|
|
|
yield this.mpv_set_property (name, Mpv.Format.INT64, &value);
|
2024-10-12 12:28:05 +00:00
|
|
|
}
|
|
|
|
|
2024-10-17 10:05:20 +00:00
|
|
|
private async void mpv_set_property_flag (string name, bool value) throws Error {
|
|
|
|
int flag = value ? 1 : 0;
|
|
|
|
yield this.mpv_set_property (name, Mpv.Format.FLAG, &flag);
|
|
|
|
}
|
2024-10-17 06:06:09 +00:00
|
|
|
|
2024-10-17 10:05:20 +00:00
|
|
|
public Playbin () {
|
|
|
|
try {
|
|
|
|
check_mpv_error (this.mpv.initialize ());
|
|
|
|
check_mpv_error (this.mpv.set_property_string ("user-agent", Audrey.Const.user_agent));
|
|
|
|
check_mpv_error (this.mpv.set_property_string ("video", "no"));
|
|
|
|
check_mpv_error (this.mpv.set_property_string ("prefetch-playlist", "yes"));
|
|
|
|
check_mpv_error (this.mpv.set_property_string ("gapless-audio", "yes"));
|
|
|
|
check_mpv_error (this.mpv.observe_property (0, "time-pos", Mpv.Format.DOUBLE));
|
|
|
|
check_mpv_error (this.mpv.observe_property (1, "duration", Mpv.Format.DOUBLE));
|
|
|
|
check_mpv_error (this.mpv.observe_property (2, "playlist-pos", Mpv.Format.INT64));
|
|
|
|
} catch (Error e) {
|
|
|
|
error ("could not initialize mpv: %s", e.message);
|
2024-10-12 12:28:05 +00:00
|
|
|
}
|
2024-10-17 10:05:20 +00:00
|
|
|
|
|
|
|
this.mpv.wakeup_callback = () => {
|
|
|
|
Idle.add (() => {
|
|
|
|
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.COMMAND_REPLY:
|
|
|
|
this.mpv_command_error[event.reply_userdata] = event.error;
|
|
|
|
var cb = (owned) this.mpv_command_callbacks[event.reply_userdata].inner;
|
|
|
|
cb ();
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Mpv.EventId.SET_PROPERTY_REPLY:
|
|
|
|
this.mpv_set_property_error[event.reply_userdata] = event.error;
|
|
|
|
var cb = (owned) this.mpv_set_property_callbacks[event.reply_userdata].inner;
|
|
|
|
cb ();
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Mpv.EventId.PROPERTY_CHANGE:
|
|
|
|
switch (event.reply_userdata) {
|
|
|
|
case 0:
|
|
|
|
var data = (Mpv.EventProperty *) event.data;
|
|
|
|
assert (data.name == "time-pos");
|
|
|
|
if (data.format == Mpv.Format.NONE) {
|
|
|
|
this.position = 0.0;
|
|
|
|
} else {
|
|
|
|
assert (data.format == Mpv.Format.DOUBLE);
|
|
|
|
this.position = * (double *) data.data;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 1:
|
|
|
|
var data = (Mpv.EventProperty *) event.data;
|
|
|
|
assert (data.name == "duration");
|
|
|
|
if (data.format == Mpv.Format.NONE) {
|
|
|
|
this.duration = 0.0;
|
|
|
|
} else {
|
|
|
|
assert (data.format == Mpv.Format.DOUBLE);
|
|
|
|
this.duration = * (double *) data.data;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 2:
|
|
|
|
var data = (Mpv.EventProperty *) event.data;
|
|
|
|
assert (data.name == "playlist-pos");
|
|
|
|
if (data.format == Mpv.Format.NONE) {
|
|
|
|
this.play_queue_position = 0;
|
|
|
|
} else {
|
|
|
|
assert (data.format == Mpv.Format.INT64);
|
|
|
|
this.play_queue_position = (uint) * (int64 *) data.data;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
assert (false);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Mpv.EventId.START_FILE:
|
|
|
|
// ignore
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Mpv.EventId.FILE_LOADED:
|
|
|
|
this.song = (Subsonic.Song) this.play_queue.get_item (this.play_queue_position);
|
|
|
|
this.now_playing ();
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Mpv.EventId.PLAYBACK_RESTART:
|
|
|
|
// ignore
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Mpv.EventId.SEEK:
|
|
|
|
// ignore
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Mpv.EventId.END_FILE:
|
|
|
|
// ignore
|
|
|
|
break;
|
|
|
|
|
|
|
|
// deprecated, ignore
|
|
|
|
case Mpv.EventId.IDLE:
|
|
|
|
case Mpv.EventId.TICK:
|
|
|
|
// uninteresting, ignore
|
|
|
|
case Mpv.EventId.AUDIO_RECONFIG:
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
print ("got unimplemented %s\n", event.event_id.to_string ());
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
public void seek (double position) {
|
|
|
|
this.position = position;
|
|
|
|
this.mpv_command.begin ({"seek", position.to_string (), "absolute"});
|
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 10:05:20 +00:00
|
|
|
this.mpv_command.begin ({"playlist-play-index", position.to_string ()});
|
2024-10-15 11:27:47 +00:00
|
|
|
this.state = PlaybinState.PLAYING;
|
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-17 10:05:20 +00:00
|
|
|
this.mpv_command.begin ({"pause"});
|
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-17 10:05:20 +00:00
|
|
|
this.mpv_command.begin ({"play"});
|
2024-10-13 15:21:29 +00:00
|
|
|
}
|
2024-10-16 09:11:52 +00:00
|
|
|
|
2024-10-17 10:05:20 +00:00
|
|
|
public void next_track () {
|
|
|
|
assert (this.state != PlaybinState.STOPPED);
|
|
|
|
this.state = PlaybinState.PLAYING;
|
|
|
|
this.mpv_command.begin ({"playlist-next-playlist"});
|
|
|
|
}
|
|
|
|
|
|
|
|
public void prev_track () {
|
|
|
|
assert (this.state != PlaybinState.STOPPED);
|
|
|
|
this.state = PlaybinState.PLAYING;
|
|
|
|
this.mpv_command.begin ({"playlist-prev-playlist"});
|
2024-10-16 09:11:52 +00:00
|
|
|
}
|
2024-10-12 12:28:05 +00:00
|
|
|
}
|