enum PlaybinState { STOPPED, PAUSED, PLAYING, } class Playbin : Object { // dynamic: undocumented vala feature // lets us access the about-to-finish signal private dynamic Gst.Element playbin = Gst.ElementFactory.make ("playbin3", null); // used to prevent stale seeks private uint64 stream_counter = 0; // cubic: recommended for media player volume sliders? 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 { get { return this.playbin.mute; } set { this.playbin.mute = value; } } public PlaybinState state { get; private set; default = PlaybinState.STOPPED; } private bool seeking = false; public int64 position { get; private set; } public int64 duration { get; private set; } private bool notify_next_transition; // public void begin_playback (string uri); // public void prepare_next (string? next_uri); public signal void song_transition (string uri); public signal void playback_finished (); // public void stop_playback (); private void source_setup (Gst.Element playbin, dynamic Gst.Element source) { source.user_agent = "audrey/linux"; } construct { this.playbin.source_setup.connect (this.source_setup); this.playbin.about_to_finish.connect (this.about_to_finish); // regularly update position Timeout.add (500, () => { if (!this.seeking) { int64 new_position; if (this.playbin.query_position (Gst.Format.TIME, out new_position)) { this.position = new_position < this.duration ? new_position : this.duration; } else { this.position = 0; } } // keep rerunning return true; }); var bus = this.playbin.get_bus (); bus.add_signal_watch (); bus.message["error"].connect ((message) => { Error err; string? debug; message.parse_error (out err, out debug); error ("gst playbin bus error: %s", err.message); }); 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["state-changed"].connect ((message) => { if (message.src != this.playbin) return; Gst.State old_state; Gst.State new_state; message.parse_state_changed (out old_state, out new_state, null); if (new_state == Gst.State.PLAYING) { if (this.queued_seek_position < 0) { this.seeking = false; } else if (this.queued_seek_counter == this.stream_counter) { assert (this.playbin.seek_simple (Gst.Format.TIME, Gst.SeekFlags.FLUSH, this.queued_seek_position)); } this.queued_seek_position = -1; } }); bus.message["stream-start"].connect ((message) => { this.stream_counter += 1; int64 new_duration; assert (this.playbin.query_duration (Gst.Format.TIME, out new_duration)); this.duration = new_duration; if (notify_next_transition) { this.song_transition ((string) this.playbin.current_uri); } else { notify_next_transition = true; } }); bus.message["eos"].connect ((message) => { assert (notify_next_transition); this.playback_finished (); }); } private uint64 queued_seek_counter; private int64 queued_seek_position = -1; public void seek (int64 position) { if (this.state == PlaybinState.STOPPED) return; if (this.playbin.seek_simple (Gst.Format.TIME, Gst.SeekFlags.FLUSH, position)) { this.seeking = true; this.position = position; } else { // queue this seek this.queued_seek_counter = this.stream_counter; this.queued_seek_position = position; } } public void begin_playback (string uri) { this.playbin.set_state (Gst.State.READY); this.playbin.uri = uri; this.playbin.set_state (Gst.State.PLAYING); this.state = PlaybinState.PLAYING; this.notify_next_transition = false; } Mutex next_uri_lock; string? next_uri; public void prepare_next (string? next_uri) { this.next_uri_lock.lock (); this.next_uri = next_uri; this.next_uri_lock.unlock (); } // 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) { this.next_uri_lock.lock (); string? next_uri = this.next_uri; this.next_uri_lock.unlock (); if (next_uri != null) { playbin.uri = next_uri; } } public void pause () { assert (this.state != PlaybinState.STOPPED); this.playbin.set_state (Gst.State.PAUSED); this.state = PlaybinState.PAUSED; } public void play () { assert (this.state != PlaybinState.STOPPED); this.playbin.set_state (Gst.State.PLAYING); this.state = PlaybinState.PLAYING; } public void stop_playback() { this.playbin.set_state (Gst.State.READY); this.state = PlaybinState.STOPPED; } }