From 72e0745d524d89b1f202393edaac21485df82c8d Mon Sep 17 00:00:00 2001 From: Erica Z Date: Tue, 15 Oct 2024 13:27:47 +0200 Subject: [PATCH] move a bunch of logic into playbin also yeah yeah fixme those buttons are gone for now --- src/playbin.vala | 143 ++++++++++++++++++++++++++++++--------- src/ui/play_queue.blp | 4 +- src/ui/play_queue.vala | 150 +++++++++++------------------------------ src/ui/window.blp | 20 +++--- src/ui/window.vala | 64 +++++++----------- 5 files changed, 193 insertions(+), 188 deletions(-) diff --git a/src/playbin.vala b/src/playbin.vala index 89b6df5..1f648b8 100644 --- a/src/playbin.vala +++ b/src/playbin.vala @@ -31,19 +31,24 @@ class Playbin : Object { public int64 position { get; private set; } public int64 duration { get; private set; } - private bool notify_next_transition; + public Subsonic api { get; set; } + private ListModel play_queue; - // 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 (); + 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"; } - construct { + 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 @@ -53,6 +58,53 @@ class Playbin : Object { 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 + this.next_uri.lock (); + switch (this.next_uri.length_unlocked ()) { + case 1: + // we're in luck, about-to-finish hasn't been triggered yet + // we can get away with replacing it + this.next_uri.try_pop_unlocked (); + 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_unlocked (this.api.stream_uri (song.id)); + } else { + this.next_uri.push_unlocked (""); + } + break; + + case 0: + // about-to-finish already triggered + // we'll need to stop the new track when it starts playing + assert (false); // TODO + break; + + default: + error ("invalid next_uri queue length %u\n", this.next_uri.length_unlocked ()); + } + this.next_uri.unlock (); + } + }); + // regularly update position/duration Timeout.add (500, () => { if (this.update_position) { @@ -95,10 +147,20 @@ class Playbin : Object { this.position = 0; - if (notify_next_transition) { - this.song_transition ((string) this.playbin.current_uri); + if (this.next_gapless) { + // advance position in play queue + this.playing_index += 1; } else { - notify_next_transition = true; + 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 (""); } }); @@ -112,8 +174,7 @@ class Playbin : Object { }); bus.message["eos"].connect ((message) => { - assert (notify_next_transition); - this.playback_finished (); + assert (false); // TODO }); } @@ -123,32 +184,54 @@ class Playbin : Object { } } - public void begin_playback (string uri) { - this.playbin.set_state (Gst.State.READY); - this.playbin.uri = uri; - this.playbin.set_state (Gst.State.PLAYING); - + public void begin_playback (uint position) { this.state = PlaybinState.PLAYING; - this.notify_next_transition = false; + + 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; + + this.next_uri.lock (); + switch (this.next_uri.length_unlocked ()) { + case 1: + // we're in luck, about-to-finish hasn't been triggered yet + this.next_uri.try_pop_unlocked (); + break; + + case 0: + // about-to-finish already triggered + // we'll need to stop the new track when it starts playing + assert (false); // TODO + break; + + case -1: + // about-to-finish is blocked + // extremely stupid edge case + assert (false); // TODO + break; + + default: + error ("invalid next_uri queue length %u\n", this.next_uri.length_unlocked ()); + } + this.next_uri.unlock (); } - 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 (); - } + // 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) { - this.next_uri_lock.lock (); - string? next_uri = this.next_uri; - this.next_uri_lock.unlock (); + print ("about to finish\n"); - if (next_uri != null) { + // 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; } } diff --git a/src/ui/play_queue.blp b/src/ui/play_queue.blp index adf41d8..949aece 100644 --- a/src/ui/play_queue.blp +++ b/src/ui/play_queue.blp @@ -9,7 +9,7 @@ template $UiPlayQueue: Adw.NavigationPage { Adw.HeaderBar { Button { icon-name: "edit-clear-all"; - clicked => $clear (); + clicked => $on_clear (); sensitive: bind template.can_clear_all; } } @@ -18,6 +18,8 @@ template $UiPlayQueue: Adw.NavigationPage { ColumnView view { styles [ "data-table" ] + model: bind template.model; + //ColumnViewColumn { // factory: SignalListItemFactory { // setup => $on_delete_cell_setup (); diff --git a/src/ui/play_queue.vala b/src/ui/play_queue.vala index b6c4049..1040549 100644 --- a/src/ui/play_queue.vala +++ b/src/ui/play_queue.vala @@ -1,64 +1,58 @@ -class Ui.PlayQueueStore : Object, ListModel, Gtk.SelectionModel { - public ListStore inner = new ListStore (typeof (Song)); - public uint playing_index { get; private set; default = 0; } +// this is a custom SelectionModel that lets us only signal the +// selection has changed on user interaction +class PlayQueueSelection : Object, ListModel, Gtk.SelectionModel { + public ListStore inner { get; private set; } + + private uint _selected_position; + public uint selected_position { + get { return _selected_position; } + set { + var previous = _selected_position; + _selected_position = value; - public signal void begin_playback (Song song); - public signal void prepare_next (Song? song); - public signal void playback_continues (Song song); - public signal void stop_playback (); + if (previous < inner.get_n_items ()) { + this.selection_changed (previous, 1); + } + if (value < inner.get_n_items ()) { + this.selection_changed (value, 1); + } + } + } + + public signal void user_selected (uint position); + + internal PlayQueueSelection () { + this.inner = new ListStore (typeof (Song)); + this._selected_position = inner.get_n_items (); - construct { this.inner.items_changed.connect ((position, removed, added) => { bool emit_signal = false; - if (this.playing_index >= position) { - if (this.playing_index < position+removed) { - this.playing_index = position; - if (this.playing_index < this.inner.get_n_items ()) { - emit_signal = true; - this.begin_playback ((Song) this.inner.get_item (this.playing_index)); - this.prepare_next ((Song?) this.inner.get_item (this.playing_index+1)); - } else { - this.stop_playback (); - } + if (this.selected_position >= position) { + if (this.selected_position < position+removed && added == 0) { + emit_signal = true; + this._selected_position = position; } else { - this.playing_index += added; - this.playing_index -= removed; + this._selected_position += added; + this._selected_position -= removed; } - } else { - this.prepare_next ((Song?) this.inner.get_item (this.playing_index+1)); } this.items_changed (position, removed, added); - if (emit_signal) { - this.selection_changed (this.playing_index, 1); - } + if (emit_signal) this.selection_changed (position, 1); }); } - public void song_transition () { - this.playing_index += 1; - this.selection_changed (this.playing_index-1, 2); - this.playback_continues ((Song) this.inner.get_item (this.playing_index)); - this.prepare_next ((Song?) this.inner.get_item (this.playing_index+1)); - } - - public void playback_finished () { - this.playing_index += 1; - assert (this.playing_index == this.inner.get_n_items ()); - this.selection_changed (this.playing_index-1, 1); - } - Gtk.Bitset get_selection_in_range (uint position, uint n_items) { var bitset = new Gtk.Bitset.empty (); - if (this.playing_index < this.inner.get_n_items ()) { - bitset.add (playing_index); + if (this.selected_position < this.inner.get_n_items ()) { + bitset.add (selected_position); } return bitset; } bool is_selected (uint position) { - return position == this.playing_index; + return position == this.selected_position; } bool select_all () { @@ -70,21 +64,8 @@ class Ui.PlayQueueStore : Object, ListModel, Gtk.SelectionModel { return false; } - var previous = this.playing_index; - this.playing_index = position; - - if (previous < this.inner.get_n_items ()) { - if (previous < position) { - this.selection_changed (previous, position-previous+1); - } else if (previous > position) { - this.selection_changed (position, previous-position+1); - } - } else { - this.selection_changed (position, 1); - } - - this.begin_playback ((Song) this.inner.get_item (this.playing_index)); - this.prepare_next ((Song) this.inner.get_item (this.playing_index+1)); + this.selected_position = position; + this.user_selected (position); return true; } @@ -125,62 +106,13 @@ class Ui.PlayQueueStore : Object, ListModel, Gtk.SelectionModel { [GtkTemplate (ui = "/eu/callcc/audrey/ui/play_queue.ui")] public class Ui.PlayQueue : Adw.NavigationPage { [GtkChild] private unowned Gtk.ColumnView view; - PlayQueueStore store = new PlayQueueStore (); - - public signal void begin_playback (Song song); - public signal void prepare_next (Song? next_song); - public signal void playback_continues (Song song); - public signal void stop_playback (); + public Gtk.SelectionModel model { get; set; } public bool can_clear_all { get; private set; default = false; } - construct { - this.view.model = this.store; + public signal void clear (); - this.store.begin_playback.connect ((song) => this.begin_playback (song)); - this.store.prepare_next.connect ((next_song) => this.prepare_next (next_song)); - this.store.playback_continues.connect ((song) => this.playback_continues (song)); - this.store.stop_playback.connect (() => this.stop_playback ()); + [GtkCallback] private void on_clear () { + this.clear (); } - - [GtkCallback] public void clear () { - this.store.inner.remove_all (); - this.can_clear_all = false; - } - - public void queue (Song song) { - this.store.inner.append (song); - } - - internal void song_transition () { - this.store.song_transition (); - } - - internal void playback_finished () { - this.store.playback_finished (); - } - - internal void restart () { - this.store.select_item (0, true); - } - - public void skip_forward () { - this.store.select_item (this.store.playing_index+1, true); - } - - public void skip_backward () { - if (this.store.playing_index >= 1) { - this.store.select_item (this.store.playing_index-1, true); - } - } -/* - [GtkCallback] private void on_delete_cell_setup (Object object) { - var cell = (Gtk.ColumnViewCell) object; - var button = new Gtk.Button.from_icon_name ("edit-delete"); - button.add_css_class ("flat"); - button.clicked.connect (() => { - //this.store.inner.remove (cell.position); - }); - cell.child = button; - }*/ } diff --git a/src/ui/window.blp b/src/ui/window.blp index 88cb89e..eb328ca 100644 --- a/src/ui/window.blp +++ b/src/ui/window.blp @@ -81,7 +81,9 @@ template $UiWindow: Adw.ApplicationWindow { name: "play_queue"; title: _("Play queue"); - child: $UiPlayQueue play_queue {}; + child: $UiPlayQueue play_queue { + model: bind template.play_queue_model; + }; } } } @@ -146,7 +148,7 @@ template $UiWindow: Adw.ApplicationWindow { adjustment: Adjustment { lower: 0; value: bind template.position; - upper: bind template.playbin as <$Playbin>.duration; + upper: bind template.duration; }; change-value => $on_play_position_seek (); @@ -159,7 +161,7 @@ template $UiWindow: Adw.ApplicationWindow { "numeric", ] - label: bind $format_timestamp (template.playbin as <$Playbin>.duration) as ; + label: bind $format_timestamp (template.duration) as ; } } @@ -170,7 +172,7 @@ template $UiWindow: Adw.ApplicationWindow { Button { icon-name: "media-skip-backward"; valign: center; - sensitive: bind $playbin_active (template.playbin as <$Playbin>.state as <$PlaybinState>) as ; + //sensitive: bind $playbin_active (template.playbin as <$Playbin>.state as <$PlaybinState>) as ; clicked => $on_skip_backward_clicked (); } @@ -178,15 +180,15 @@ template $UiWindow: Adw.ApplicationWindow { Button { icon-name: "media-seek-backward"; valign: center; - sensitive: bind $playbin_active (template.playbin as <$Playbin>.state as <$PlaybinState>) as ; + //sensitive: bind $playbin_active (template.playbin as <$Playbin>.state as <$PlaybinState>) as ; clicked => $seek_backward (); } Button { - icon-name: bind $play_pause_icon_name (template.playbin as <$Playbin>.state as <$PlaybinState>) as ; + //icon-name: bind $play_pause_icon_name (template.playbin as <$Playbin>.state as <$PlaybinState>) as ; valign: center; - sensitive: bind $playbin_active (template.playbin as <$Playbin>.state as <$PlaybinState>) as ; + //sensitive: bind $playbin_active (template.playbin as <$Playbin>.state as <$PlaybinState>) as ; clicked => $on_play_pause_clicked (); } @@ -194,7 +196,7 @@ template $UiWindow: Adw.ApplicationWindow { Button { icon-name: "media-seek-forward"; valign: center; - sensitive: bind $playbin_active (template.playbin as <$Playbin>.state as <$PlaybinState>) as ; + //sensitive: bind $playbin_active (template.playbin as <$Playbin>.state as <$PlaybinState>) as ; clicked => $seek_forward (); } @@ -202,7 +204,7 @@ template $UiWindow: Adw.ApplicationWindow { Button { icon-name: "media-skip-forward"; valign: center; - sensitive: bind $playbin_active (template.playbin as <$Playbin>.state as <$PlaybinState>) as ; + //sensitive: bind $playbin_active (template.playbin as <$Playbin>.state as <$PlaybinState>) as ; clicked => $on_skip_forward_clicked (); } diff --git a/src/ui/window.vala b/src/ui/window.vala index 5ecd20e..54cd178 100644 --- a/src/ui/window.vala +++ b/src/ui/window.vala @@ -10,6 +10,7 @@ class Ui.Window : Adw.ApplicationWindow { private Setup setup; public int64 position { get; private set; } + public int64 duration { get; private set; } public double volume { get { return this.playbin.volume; } @@ -28,7 +29,8 @@ class Ui.Window : Adw.ApplicationWindow { private Gdk.Paintable next_cover_art = null; - internal Playbin playbin { get; default = new Playbin (); } + internal Playbin playbin; + public PlayQueueSelection play_queue_model { get; private set; default = new PlayQueueSelection (); } public Window (Gtk.Application app) { Object (application: app); @@ -61,15 +63,30 @@ class Ui.Window : Adw.ApplicationWindow { this.setup = new Setup (); + this.playbin = new Playbin (this.play_queue_model); + this.setup.connected.connect ((api) => { + this.playbin.api = api; + + this.playbin.now_playing.connect ((position, song, duration) => { + this.song = song; + this.duration = duration; + api.scrobble.begin (song.id); + this.play_queue_model.selected_position = position; + }); + + this.play_queue_model.user_selected.connect ((position) => { + this.playbin.begin_playback (position); + }); + public_api = api; this.shuffle_all_tracks.sensitive = true; this.shuffle_all_tracks.activated.connect (() => { this.shuffle_all_tracks.sensitive = false; - this.play_queue.clear (); + this.play_queue_model.inner.remove_all (); api.get_random_songs.begin (null, (song) => { - this.play_queue.queue (song); + this.play_queue_model.inner.append (song); }, (obj, res) => { try { api.get_random_songs.end (res); @@ -78,41 +95,9 @@ class Ui.Window : Adw.ApplicationWindow { } this.shuffle_all_tracks.sensitive = true; - this.play_queue.restart (); + this.playbin.begin_playback (0); }); }); - - this.play_queue.begin_playback.connect ((song) => { - var uri = api.stream_uri (song.id); - this.playbin.begin_playback (uri); - - this.song = song; - api.scrobble.begin (song.id); - }); - - this.play_queue.prepare_next.connect ((next_song) => { - var next_uri = next_song == null ? null : api.stream_uri (next_song.id); - this.playbin.prepare_next (next_uri); - }); - - this.playbin.song_transition.connect ((uri) => { - this.play_queue.song_transition (); - }); - - this.play_queue.playback_continues.connect ((song) => { - this.song = song; - api.scrobble.begin (song.id); - }); - - this.playbin.playback_finished.connect (() => { - this.play_queue.playback_finished (); - this.song = null; - }); - - this.play_queue.stop_playback.connect (() => { - this.playbin.stop_playback (); - this.song = null; - }); }); this.setup.load (); @@ -190,6 +175,7 @@ class Ui.Window : Adw.ApplicationWindow { } } +/* [GtkCallback] private string play_pause_icon_name (PlaybinState state) { if (state == PlaybinState.PLAYING) { return "media-playback-pause"; @@ -200,7 +186,7 @@ class Ui.Window : Adw.ApplicationWindow { [GtkCallback] private bool playbin_active (PlaybinState state) { return state != PlaybinState.STOPPED; - } + }*/ [GtkCallback] private string mute_button_icon_name (bool mute) { return mute ? "audio-volume-muted" : "audio-volume-high"; @@ -211,11 +197,11 @@ class Ui.Window : Adw.ApplicationWindow { } [GtkCallback] private void on_skip_forward_clicked () { - this.play_queue.skip_forward (); + //this.play_queue.skip_forward (); } [GtkCallback] private void on_skip_backward_clicked () { - this.play_queue.skip_backward (); + //this.play_queue.skip_backward (); } [GtkCallback] private void show_setup_dialog () {