enum PlaybinState { STOPPED, PAUSED, PLAYING, } 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; } } class Playbin : GLib.Object { private Mpv.Handle mpv = new Mpv.Handle (); public PlaybinState state { get; private set; default = PlaybinState.STOPPED; } private int _volume = 100; public int volume { get { return _volume; } set { _volume = value; mpv_set_property_int64.begin ("volume", value); } } public bool _mute = false; public bool mute { get { return _mute; } set { _mute = value; mpv_set_property_flag.begin ("mute", value); } } public uint play_queue_position { get; private set; } public Subsonic.Song? song { get; private set; } public signal void now_playing (); public double position { get; private set; default = 0.0; } public double duration { get; private set; default = 0.0; } public Subsonic.Client api { get; set; default = null; } private ListModel _play_queue = null; public ListModel play_queue { get { return _play_queue; } set { assert (_play_queue == null); // only set this once _play_queue = value; value.items_changed.connect (on_play_queue_items_changed); } } private void on_play_queue_items_changed (ListModel play_queue, uint position, uint removed, uint added) { // FIXME: these should prolly be chained for (uint i = 0; i < removed; i += 1) { this.mpv_command.begin ({"playlist-remove", position.to_string ()}); } 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 ()}); } } private SourceFuncWrapper[] mpv_command_callbacks = {}; private int[] mpv_command_error = {}; 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; } } if (userdata == -1) { userdata = this.mpv_command_callbacks.length; this.mpv_command_callbacks += new SourceFuncWrapper (); this.mpv_command_error += 0; } check_mpv_error (this.mpv.command_async ((uint64) userdata, args)); this.mpv_command_callbacks[userdata].inner = this.mpv_command.callback; yield; check_mpv_error (this.mpv_command_error[userdata]); } private SourceFuncWrapper[] mpv_set_property_callbacks = {}; private int[] mpv_set_property_error = {}; 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; } } if (userdata == -1) { userdata = this.mpv_set_property_callbacks.length; this.mpv_set_property_callbacks += new SourceFuncWrapper (); this.mpv_set_property_error += 0; } 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; check_mpv_error (this.mpv_set_property_error[userdata]); } private async void mpv_set_property_int64 (string name, int64 value) throws Error { yield this.mpv_set_property (name, Mpv.Format.INT64, &value); } 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); } 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); } 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"}); } // manually changes which track in the play queue to play public void select_track (uint position) requires (position < this.play_queue.get_n_items ()) { this.mpv_command.begin ({"playlist-play-index", position.to_string ()}); this.state = PlaybinState.PLAYING; } public void pause () { assert (this.state != PlaybinState.STOPPED); this.state = PlaybinState.PAUSED; this.mpv_command.begin ({"pause"}); } public void play () { assert (this.state != PlaybinState.STOPPED); this.state = PlaybinState.PLAYING; this.mpv_command.begin ({"play"}); } 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"}); } }