enum PlaybinState { STOPPED, PAUSED, PLAYING, } 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 { var ret = mpv.set_property_int64 ("volume", value); if (ret >= 0) { _volume = value; } else { warning ("failed to set volume: %s", ret.to_string ()); } } } public bool _mute = false; public bool mute { get { return _mute; } set { var ret = mpv.set_property_flag ("mute", value); if (ret >= 0) { _mute = value; } else { warning ("failed to set mute status: %s", ret.to_string ()); } } } public uint play_queue_position { get; private set; } public Subsonic.Song? song { get; private set; } private bool notify_next_playing; public signal void now_playing (); public signal void stopped (); 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) { for (uint i = 0; i < removed; i += 1) { assert (this.mpv.command ({ "playlist-remove", position.to_string (), }) >= 0); } for (uint i = 0; i < added; i += 1) { assert (this.mpv.command ({ "loadfile", this.api.stream_uri (((Subsonic.Song) play_queue.get_item (position+i)).id), "insert-at-play", (position+i).to_string (), }) >= 0); } } public Playbin () { 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); 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.PROPERTY_CHANGE: var data = event.parse_property (); switch (event.reply_userdata) { case 0: assert (data.name == "time-pos"); if (data.format == Mpv.Format.NONE) { this.position = 0.0; } else { this.position = data.parse_double (); } break; case 1: assert (data.name == "duration"); if (data.format == Mpv.Format.NONE) { this.duration = 0.0; } else { this.duration = data.parse_double (); } break; case 2: assert (data.name == "playlist-pos"); if (data.parse_int64 () < 0) { debug ("playlist-pos is null, sending stopped event"); this.play_queue_position = this.play_queue.get_n_items (); this.song = null; this.state = PlaybinState.STOPPED; this.stopped (); } else { this.play_queue_position = (uint) data.parse_int64 (); debug (@"playlist-pos has been updated to $(this.play_queue_position)"); this.song = (Subsonic.Song) this.play_queue.get_item (this.play_queue_position); this.now_playing (); } break; default: assert (false); break; } break; case Mpv.EventId.START_FILE: debug ("START_FILE received"); break; case Mpv.EventId.END_FILE: debug ("END_FILE received"); var data = event.parse_end_file (); if (data.error < 0) { warning ("playback of track aborted: %s", data.error.to_string ()); } break; default: // ignore by default break; } } return false; }); }; } public void seek (double position) { this.position = position; assert (this.mpv.command ({"seek", position.to_string (), "absolute"}) >= 0); } // manually changes which track in the play queue to play public void select_track (uint position) requires (position < this.play_queue.get_n_items ()) { assert (this.mpv.command ({"playlist-play-index", position.to_string ()}) >= 0); this.state = PlaybinState.PLAYING; this.play_queue_position = position; this.song = (Subsonic.Song) this.play_queue.get_item (position); this.now_playing (); this.notify_next_playing = false; } public void pause () { assert (this.state != PlaybinState.STOPPED); this.state = PlaybinState.PAUSED; GLib.debug ("setting state to paused"); // 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) { GLib.debug ("failed to set state to paused (%d): %s", ret, ret.to_string()); } } public void play () { assert (this.state != PlaybinState.STOPPED); this.state = PlaybinState.PLAYING; GLib.debug ("setting state to playing"); var ret = this.mpv.set_property_flag("pause", false); if (ret != 0) { GLib.debug ("failed to set state to playing (%d): %s", ret, ret.to_string()); } } public void next_track () { assert (this.state != PlaybinState.STOPPED); this.state = PlaybinState.PLAYING; assert (this.mpv.command ({"playlist-next-playlist"}) >= 0); } public void prev_track () { assert (this.state != PlaybinState.STOPPED); this.state = PlaybinState.PLAYING; assert (this.mpv.command ({"playlist-prev-playlist"}) >= 0); } }