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); // 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 update_position = false; public int64 position { get; private set; } public int64 duration { get; private set; } public Subsonic api { get; set; } private ListModel play_queue; public signal void now_playing (uint index, Song song, int64 duration); private uint playing_index; private bool next_gapless; private void source_setup (Gst.Element playbin, dynamic Gst.Element source) { source.user_agent = "audrey/linux"; } public Playbin (ListModel play_queue) { this.play_queue = play_queue; this.next_uri = new AsyncQueue (); this.next_uri.push (""); // gstreamer docs: GNOME-based applications, for example, will usually // want to create gconfaudiosink and gconfvideosink elements and make // playbin3 use those, so that output happens to whatever the user has // configured in the GNOME Multimedia System Selector configuration dialog. this.playbin.audio_sink = Gst.ElementFactory.make ("gconfaudiosink", null); this.playbin.source_setup.connect (this.source_setup); this.playbin.about_to_finish.connect (this.about_to_finish); play_queue.items_changed.connect ((play_queue, position, removed, added) => { if (this.state == PlaybinState.STOPPED) { return; } if (this.playing_index >= position) { if (this.playing_index < position+removed) { // current track was removed, start playing something else // TODO check if it was actually reordered this.begin_playback (position); } else { // unaffected // fix up playing index though this.playing_index += added; this.playing_index -= removed; } } else if (this.playing_index+1 == position) { // next track was changed // try to fix up gapless transition string? next_uri = this.next_uri.try_pop (); if (next_uri != null) { // we're in luck, about-to-finish hasn't been triggered yet // we can get away with replacing it if (this.playing_index+1 < play_queue.get_n_items ()) { Song song = (Song) play_queue.get_item (this.playing_index+1); this.next_uri.push (this.api.stream_uri (song.id)); } else { this.next_uri.push (""); } } else { // about-to-finish already triggered // we'll need to stop the new track when it starts playing assert (false); // TODO } } }); // regularly update position/duration Timeout.add (500, () => { if (this.update_position) { 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["stream-start"].connect ((message) => { int64 new_duration; if (this.playbin.query_duration (Gst.Format.TIME, out new_duration)) { this.duration = new_duration; } else { warning ("could not obtain new stream duration"); } this.position = 0; if (this.next_gapless) { // advance position in play queue this.playing_index += 1; } else { this.next_gapless = true; } this.now_playing (this.playing_index, (Song) play_queue.get_item (this.playing_index), this.duration); if (this.playing_index+1 < play_queue.get_n_items ()) { Song song = (Song) play_queue.get_item (this.playing_index+1); this.next_uri.push (this.api.stream_uri (song.id)); } else { this.next_uri.push (""); } }); bus.message["state-changed"].connect ((message) => { if (message.src != this.playbin) return; Gst.State new_state; message.parse_state_changed (null, out new_state, null); this.update_position = new_state == Gst.State.PLAYING; }); bus.message["eos"].connect ((message) => { assert (false); // TODO }); } public void seek (int64 position) { if (!this.playbin.seek_simple (Gst.Format.TIME, Gst.SeekFlags.FLUSH, position)) { warning ("could not seek"); } } public void begin_playback (uint position) { this.state = PlaybinState.PLAYING; this.playing_index = position; this.playbin.set_state (Gst.State.READY); this.playbin.uri = this.api.stream_uri (((Song) this.play_queue.get_item (position)).id); this.playbin.set_state (Gst.State.PLAYING); this.next_gapless = false; string? next_uri = this.next_uri.try_pop (); if (next_uri != null) { // we're in luck, about-to-finish hasn't been triggered yet } else { // about-to-finish already triggered // we'll need to stop the new track when it starts playing assert (false); // TODO } } // ASSUMPTION: about-to-finish will be signalled exactly once per track // even if seeking backwards after AsyncQueue next_uri; // 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) { print ("about to finish\n"); // will block if the next uri isn't ready yet // leaves the queue empty as per the ASSUMPTION above string? next_uri = this.next_uri.pop (); if (next_uri != "") { 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; } }