now gapless
This commit is contained in:
parent
4f2ce1c340
commit
c2ad6c425e
8 changed files with 298 additions and 123 deletions
|
@ -1,7 +1,7 @@
|
||||||
project('wavelet', ['c', 'vala'],
|
project('wavelet', ['c', 'vala'],
|
||||||
version: '0.1.0',
|
version: '0.1.0', # WAVELET_VERSION
|
||||||
meson_version: '>= 1.0.0',
|
meson_version: '>= 1.0.0',
|
||||||
default_options: [ 'warning_level=none', 'werror=false', ],
|
default_options: [ 'warning_level=0', 'werror=false', ],
|
||||||
)
|
)
|
||||||
|
|
||||||
i18n = import('i18n')
|
i18n = import('i18n')
|
||||||
|
|
|
@ -167,6 +167,7 @@ public class Wavelet.Subsonic : Object {
|
||||||
this.parameters = @"u=$(Uri.escape_string(username))&p=$(Uri.escape_string(password))&v=1.16.1&c=eu.callcc.Wavelet";
|
this.parameters = @"u=$(Uri.escape_string(username))&p=$(Uri.escape_string(password))&v=1.16.1&c=eu.callcc.Wavelet";
|
||||||
|
|
||||||
this.session = new Soup.Session ();
|
this.session = new Soup.Session ();
|
||||||
|
this.session.user_agent = "Wavelet/0.1.0 (Linux)"; // WAVELET_VERSION
|
||||||
|
|
||||||
this.artist_list = new ListStore (typeof (Artist));
|
this.artist_list = new ListStore (typeof (Artist));
|
||||||
this.album_list = new ListStore (typeof (Album));
|
this.album_list = new ListStore (typeof (Album));
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Gst.Element playbin;
|
|
||||||
Wavelet.Subsonic public_api;
|
Wavelet.Subsonic public_api;
|
||||||
|
|
||||||
public class Wavelet.Application : Adw.Application {
|
public class Wavelet.Application : Adw.Application {
|
||||||
|
@ -41,88 +40,9 @@ public class Wavelet.Application : Adw.Application {
|
||||||
|
|
||||||
public override void activate () {
|
public override void activate () {
|
||||||
base.activate ();
|
base.activate ();
|
||||||
// FIXME edit back if issues var win = this.active_window ?? new Wavelet.Window (this);
|
// if this.active_window not null, this isnt the primary instance
|
||||||
var win = new Wavelet.Window (this);
|
var win = this.active_window ?? new Wavelet.Window (this);
|
||||||
win.present ();
|
win.present ();
|
||||||
|
|
||||||
playbin = Gst.ElementFactory.make ("playbin3", null);
|
|
||||||
assert (playbin != null);
|
|
||||||
|
|
||||||
win.notify["volume"].connect ((s, p) => {
|
|
||||||
// gst docs: Volume sliders should usually use a cubic volume.
|
|
||||||
((Gst.Audio.StreamVolume) playbin).set_volume (Gst.Audio.StreamVolumeFormat.CUBIC, win.volume);
|
|
||||||
});
|
|
||||||
|
|
||||||
win.mute.clicked.connect (() => {
|
|
||||||
var vol = (Gst.Audio.StreamVolume) playbin;
|
|
||||||
if (vol.get_mute ()) {
|
|
||||||
win.show_unmute ();
|
|
||||||
vol.set_mute (false);
|
|
||||||
} else {
|
|
||||||
win.show_mute ();
|
|
||||||
vol.set_mute (true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Timeout.add (100, () => {
|
|
||||||
int64 position_ns;
|
|
||||||
if (playbin.query_position (Gst.Format.TIME, out position_ns)) {
|
|
||||||
win.play_position_ms = (int) (position_ns / 1000000);
|
|
||||||
}
|
|
||||||
return Source.CONTINUE;
|
|
||||||
});
|
|
||||||
|
|
||||||
playbin.get_bus ().add_watch (Priority.DEFAULT, (bus, message) => {
|
|
||||||
switch (message.type) {
|
|
||||||
case Gst.MessageType.ASYNC_DONE:
|
|
||||||
win.play_position.sensitive = true;
|
|
||||||
|
|
||||||
int64 duration_ns;
|
|
||||||
if (playbin.query_duration (Gst.Format.TIME, out duration_ns)) {
|
|
||||||
win.play_position_ms = 0;
|
|
||||||
win.play_duration_ms = (int) (duration_ns / 1000000);
|
|
||||||
} else {
|
|
||||||
warning ("could not query playbin duration after ASYNC_DONE");
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
win.setup.connected.connect ((api) => {
|
|
||||||
public_api = api;
|
|
||||||
|
|
||||||
win.shuffle_all_tracks.sensitive = true;
|
|
||||||
win.shuffle_all_tracks.activated.connect (() => {
|
|
||||||
win.shuffle_all_tracks.sensitive = false;
|
|
||||||
win.play_queue.clear ();
|
|
||||||
api.get_random_songs.begin (null, (song) => {
|
|
||||||
win.play_queue.queue (song);
|
|
||||||
}, (obj, res) => {
|
|
||||||
try {
|
|
||||||
api.get_random_songs.end (res);
|
|
||||||
} catch (Error e) {
|
|
||||||
error ("could not get random songs: %s", e.message);
|
|
||||||
}
|
|
||||||
win.shuffle_all_tracks.sensitive = true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
win.play_queue.play_now.connect ((song) => {
|
|
||||||
win.play_position.sensitive = false;
|
|
||||||
|
|
||||||
playbin.set_state (Gst.State.READY);
|
|
||||||
playbin.set ("uri", api.stream_uri (song.id));
|
|
||||||
playbin.set_state (Gst.State.PLAYING);
|
|
||||||
|
|
||||||
win.song = song;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
win.setup.load ();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void on_about_action () {
|
private void on_about_action () {
|
||||||
|
@ -132,7 +52,7 @@ public class Wavelet.Application : Adw.Application {
|
||||||
application_icon = "eu.callcc.Wavelet",
|
application_icon = "eu.callcc.Wavelet",
|
||||||
developer_name = "Erica Z",
|
developer_name = "Erica Z",
|
||||||
translator_credits = _("translator-credits"),
|
translator_credits = _("translator-credits"),
|
||||||
version = "0.1.0",
|
version = "0.1.0", // WAVELET_VERSION
|
||||||
developers = developers,
|
developers = developers,
|
||||||
copyright = "© 2024 Erica Z",
|
copyright = "© 2024 Erica Z",
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,6 +3,7 @@ wavelet_sources = [
|
||||||
'application.vala',
|
'application.vala',
|
||||||
'main.vala',
|
'main.vala',
|
||||||
'play_queue.vala',
|
'play_queue.vala',
|
||||||
|
'playbin.vala',
|
||||||
'setup.vala',
|
'setup.vala',
|
||||||
'window.vala',
|
'window.vala',
|
||||||
]
|
]
|
||||||
|
|
|
@ -23,39 +23,40 @@ public class Wavelet.PlayQueue : Adw.NavigationPage {
|
||||||
[GtkChild] private unowned Gtk.ListView list_view;
|
[GtkChild] private unowned Gtk.ListView list_view;
|
||||||
|
|
||||||
private ListStore songs;
|
private ListStore songs;
|
||||||
private uint current;
|
private uint next_song;
|
||||||
|
|
||||||
public signal void play_now (Song song);
|
public signal void play_now (Song song);
|
||||||
|
public signal void now_playing (Song song);
|
||||||
|
public signal void play_next (Song? song);
|
||||||
|
|
||||||
construct {
|
construct {
|
||||||
this.songs = new ListStore (typeof (Song));
|
this.songs = new ListStore (typeof (Song));
|
||||||
this.current = 0;
|
this.next_song = 0;
|
||||||
|
|
||||||
this.list_view.model = new Gtk.NoSelection (this.songs);
|
this.list_view.model = new Gtk.NoSelection (this.songs);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clear () {
|
public void clear () {
|
||||||
this.songs.remove_all ();
|
this.songs.remove_all ();
|
||||||
this.current = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void queue (Song song) {
|
public void queue (Song song) {
|
||||||
this.songs.append (song);
|
this.songs.append (song);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void next () {
|
|
||||||
if (this.current < this.songs.get_n_items ()) {
|
|
||||||
this.current += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Song? peek () {
|
|
||||||
return (Song?) this.songs.get_item (this.current+1);
|
|
||||||
}
|
|
||||||
|
|
||||||
[GtkCallback] private void on_song_activate (uint position) {
|
[GtkCallback] private void on_song_activate (uint position) {
|
||||||
this.current = position;
|
this.next_song = position;
|
||||||
Song song = (Song) this.songs.get_item (position);
|
Song song = (Song) this.songs.get_item (position);
|
||||||
this.play_now (song);
|
this.play_now (song);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void on_stream_start (Playbin playbin) {
|
||||||
|
Song song = (Song) this.songs.get_item (this.next_song);
|
||||||
|
this.now_playing (song);
|
||||||
|
|
||||||
|
// prepare for next song gapless
|
||||||
|
this.next_song += 1;
|
||||||
|
Song? next_song = (Song?) this.songs.get_item (this.next_song);
|
||||||
|
this.play_next (next_song);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
194
src/playbin.vala
Normal file
194
src/playbin.vala
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
/* playbin.vala
|
||||||
|
*
|
||||||
|
* Copyright 2024 Erica Z
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Playbin : Object {
|
||||||
|
// dynamic: undocumented vala feature
|
||||||
|
// lets us access the about-to-finish signal
|
||||||
|
private dynamic Gst.Element playbin;
|
||||||
|
|
||||||
|
private SourceFunc? async_done_callback;
|
||||||
|
|
||||||
|
// 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 playbin.mute; }
|
||||||
|
set { playbin.mute = value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public signal void set_position (int64 position);
|
||||||
|
public int64 duration { get; private set; }
|
||||||
|
|
||||||
|
public signal void stream_started ();
|
||||||
|
|
||||||
|
construct {
|
||||||
|
this.playbin = Gst.ElementFactory.make ("playbin3", null);
|
||||||
|
assert (this.playbin != null);
|
||||||
|
|
||||||
|
//dynamic Gst.Element souphttpsrc = ((Gst.Bin) this.playbin).get_by_name ("souphttpsrc");
|
||||||
|
//assert (souphttpsrc != null);
|
||||||
|
//souphttpsrc.user_agent = "Wavelet/0.1.0 (Linux)"; // WAVELET_VERSION
|
||||||
|
|
||||||
|
// regularly update position
|
||||||
|
Idle.add (() => {
|
||||||
|
int64 new_position;
|
||||||
|
if (this.playbin.query_position (Gst.Format.TIME, out new_position)) {
|
||||||
|
this.set_position (new_position < this.duration ? new_position : this.duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// rerun when idle
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.playbin.get_bus ().add_watch (Priority.DEFAULT, (bus, message) => {
|
||||||
|
// message.type actually seems to be flags
|
||||||
|
if (Gst.MessageType.ERROR in message.type) {
|
||||||
|
Error err;
|
||||||
|
string? debug;
|
||||||
|
message.parse_error (out err, out debug);
|
||||||
|
warning ("gst playbin bus error: %s", err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Gst.MessageType.ASYNC_DONE in message.type) {
|
||||||
|
assert (this.async_done_callback != null);
|
||||||
|
var cb = (owned) this.async_done_callback;
|
||||||
|
assert (this.async_done_callback == null); // sanity check
|
||||||
|
cb ();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Gst.MessageType.STREAM_START in message.type) {
|
||||||
|
print ("stream start\n");
|
||||||
|
|
||||||
|
int64 new_duration;
|
||||||
|
assert (this.playbin.query_duration (Gst.Format.TIME, out new_duration));
|
||||||
|
this.duration = new_duration;
|
||||||
|
|
||||||
|
this.next_uri_lock.lock ();
|
||||||
|
this.next_uri = null;
|
||||||
|
this.next_uri_lock.unlock ();
|
||||||
|
|
||||||
|
this.stream_started ();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Gst.MessageType.EOS in message.type) {
|
||||||
|
print ("eos\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.playbin.about_to_finish.connect (this.on_about_to_finish);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void set_state (Gst.State state) {
|
||||||
|
assert (this.async_done_callback == null);
|
||||||
|
|
||||||
|
switch (this.playbin.set_state (state)) {
|
||||||
|
case Gst.StateChangeReturn.SUCCESS:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Gst.StateChangeReturn.ASYNC:
|
||||||
|
this.async_done_callback = this.set_state.callback;
|
||||||
|
yield;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
assert (false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async bool seek (int64 position) {
|
||||||
|
// don't actually seek if an operation is pending
|
||||||
|
if (this.async_done_callback != null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ASSUMPTION: this can only work asynchronously
|
||||||
|
// (will wait for an ASYNC_DONE in the bus)
|
||||||
|
assert (this.playbin.seek_simple (Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, position));
|
||||||
|
this.async_done_callback = this.seek.callback;
|
||||||
|
yield;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? play_now_queued;
|
||||||
|
// returns true if the "play now" request wasn't overriden by a later call
|
||||||
|
public async bool play_now (string uri) {
|
||||||
|
if (this.async_done_callback != null) {
|
||||||
|
// an operation was already pending
|
||||||
|
// last writer wins here
|
||||||
|
this.play_now_queued = uri;
|
||||||
|
// idle spinning is probably fine
|
||||||
|
while (this.async_done_callback != null) {
|
||||||
|
Idle.add (this.play_now.callback);
|
||||||
|
yield;
|
||||||
|
// only keep spinning if we can still win
|
||||||
|
if (this.play_now_queued != uri) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.play_now_queued = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield this.set_state (Gst.State.READY);
|
||||||
|
this.playbin.uri = uri;
|
||||||
|
yield this.set_state (Gst.State.PLAYING);
|
||||||
|
|
||||||
|
if (this.play_now_queued != null) {
|
||||||
|
// another "play now" was queued while we were busy
|
||||||
|
// defer to it
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Mutex next_uri_lock;
|
||||||
|
string? next_uri;
|
||||||
|
|
||||||
|
public void set_next_uri (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 on_about_to_finish (dynamic Gst.Element playbin) {
|
||||||
|
print("about to finish\n");
|
||||||
|
|
||||||
|
this.next_uri_lock.lock ();
|
||||||
|
string? next_uri = this.next_uri;
|
||||||
|
this.next_uri_lock.unlock ();
|
||||||
|
|
||||||
|
if (next_uri != null) {
|
||||||
|
playbin.uri = next_uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -91,19 +91,9 @@ template $WaveletWindow: Adw.ApplicationWindow {
|
||||||
"toolbar",
|
"toolbar",
|
||||||
]
|
]
|
||||||
|
|
||||||
Overlay {
|
|
||||||
[overlay] Adw.Spinner {
|
|
||||||
halign: center;
|
|
||||||
valign: center;
|
|
||||||
width-request: 20;
|
|
||||||
height-request: 20;
|
|
||||||
visible: bind template.cover_art_loading;
|
|
||||||
}
|
|
||||||
|
|
||||||
Picture {
|
Picture {
|
||||||
paintable: bind template.playing_cover_art;
|
paintable: bind template.playing_cover_art;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Box {
|
Box {
|
||||||
orientation: vertical;
|
orientation: vertical;
|
||||||
|
@ -149,19 +139,20 @@ template $WaveletWindow: Adw.ApplicationWindow {
|
||||||
"numeric",
|
"numeric",
|
||||||
]
|
]
|
||||||
|
|
||||||
label: bind $format_timestamp (template.play_position_ms) as <string>;
|
label: bind $format_timestamp (template.position) as <string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
Scale play_position {
|
Scale play_position {
|
||||||
orientation: horizontal;
|
orientation: horizontal;
|
||||||
width-request: 200;
|
width-request: 200;
|
||||||
sensitive: false;
|
|
||||||
|
|
||||||
adjustment: Adjustment {
|
adjustment: Adjustment {
|
||||||
lower: 0;
|
lower: 0;
|
||||||
value: bind template.play_position_ms bidirectional;
|
value: bind template.position;
|
||||||
upper: bind template.play_duration_ms;
|
upper: bind template.playbin as <$Playbin>.duration;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
change-value => $on_play_position_seek ();
|
||||||
}
|
}
|
||||||
|
|
||||||
Label play_duration {
|
Label play_duration {
|
||||||
|
@ -170,7 +161,7 @@ template $WaveletWindow: Adw.ApplicationWindow {
|
||||||
"numeric",
|
"numeric",
|
||||||
]
|
]
|
||||||
|
|
||||||
label: bind $format_timestamp (template.play_duration_ms) as <string>;
|
label: bind $format_timestamp (template.playbin as <$Playbin>.duration) as <string>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -191,6 +182,8 @@ template $WaveletWindow: Adw.ApplicationWindow {
|
||||||
Button {
|
Button {
|
||||||
icon-name: "media-playback-start";
|
icon-name: "media-playback-start";
|
||||||
valign: center;
|
valign: center;
|
||||||
|
|
||||||
|
clicked => $on_play_pause_clicked ();
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
|
|
|
@ -30,10 +30,9 @@ public class Wavelet.Window : Adw.ApplicationWindow {
|
||||||
[GtkChild] public unowned Adw.ButtonRow shuffle_all_tracks;
|
[GtkChild] public unowned Adw.ButtonRow shuffle_all_tracks;
|
||||||
|
|
||||||
[GtkChild] public unowned Gtk.Button mute;
|
[GtkChild] public unowned Gtk.Button mute;
|
||||||
[GtkChild] public unowned Gtk.Scale play_position;
|
|
||||||
|
|
||||||
public int play_position_ms { get; set; default = 0; }
|
[GtkChild] private unowned Gtk.Scale play_position;
|
||||||
public int play_duration_ms { get; set; default = 1; }
|
public int64 position { get; private set; }
|
||||||
|
|
||||||
public double volume { get; set; default = 1.0; }
|
public double volume { get; set; default = 1.0; }
|
||||||
|
|
||||||
|
@ -43,11 +42,52 @@ public class Wavelet.Window : Adw.ApplicationWindow {
|
||||||
public bool cover_art_loading { get; set; default = false; }
|
public bool cover_art_loading { get; set; default = false; }
|
||||||
public Gdk.Paintable playing_cover_art { get; set; }
|
public Gdk.Paintable playing_cover_art { get; set; }
|
||||||
|
|
||||||
|
internal Playbin playbin { get; default = new Playbin (); }
|
||||||
|
|
||||||
public Window (Gtk.Application app) {
|
public Window (Gtk.Application app) {
|
||||||
Object (application: app);
|
Object (application: app);
|
||||||
}
|
}
|
||||||
|
|
||||||
construct {
|
construct {
|
||||||
|
this.setup.connected.connect ((api) => {
|
||||||
|
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 ();
|
||||||
|
api.get_random_songs.begin (null, (song) => {
|
||||||
|
this.play_queue.queue (song);
|
||||||
|
}, (obj, res) => {
|
||||||
|
try {
|
||||||
|
api.get_random_songs.end (res);
|
||||||
|
} catch (Error e) {
|
||||||
|
error ("could not get random songs: %s", e.message);
|
||||||
|
}
|
||||||
|
this.shuffle_all_tracks.sensitive = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
playbin.stream_started.connect (this.play_queue.on_stream_start);
|
||||||
|
|
||||||
|
this.play_queue.now_playing.connect ((song) => {
|
||||||
|
print ("now playing %s\n", song.title);
|
||||||
|
this.song = song;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.play_queue.play_now.connect ((song) => {
|
||||||
|
playbin.play_now.begin (api.stream_uri (song.id));
|
||||||
|
});
|
||||||
|
this.play_queue.play_next.connect ((song) => {
|
||||||
|
if (song == null) {
|
||||||
|
playbin.set_next_uri (null);
|
||||||
|
} else {
|
||||||
|
playbin.set_next_uri (api.stream_uri (song.id));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.setup.load ();
|
||||||
|
|
||||||
this.sidebar.select_row (this.sidebar.get_row_at_index (0));
|
this.sidebar.select_row (this.sidebar.get_row_at_index (0));
|
||||||
|
|
||||||
this.notify["song"].connect (() => {
|
this.notify["song"].connect (() => {
|
||||||
|
@ -74,7 +114,14 @@ public class Wavelet.Window : Adw.ApplicationWindow {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.set("song", null);
|
this.song = null;
|
||||||
|
|
||||||
|
this.playbin.set_position.connect ((sender, new_position) => {
|
||||||
|
// only set if we aren't seeking
|
||||||
|
if (this.seek_timeout_id == 0) {
|
||||||
|
this.position = new_position;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void show_mute () {
|
public void show_mute () {
|
||||||
|
@ -93,8 +140,26 @@ public class Wavelet.Window : Adw.ApplicationWindow {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[GtkCallback] private string format_timestamp (int ms) {
|
[GtkCallback] private string format_timestamp (int64 ns) {
|
||||||
int s = ms / 1000;
|
int64 ms = ns / 1000000;
|
||||||
|
int s = (int) (ms / 1000);
|
||||||
return "%02d:%02d".printf (s/60, s%60);
|
return "%02d:%02d".printf (s/60, s%60);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[GtkCallback] private void on_play_pause_clicked () {
|
||||||
|
}
|
||||||
|
|
||||||
|
// same timeout logic as https://code.videolan.org/videolan/npapi-vlc/blob/6eae0ffb9cbaf8f6e04423de2ff38daabdf7cae3/npapi/vlcplugin_gtk.cpp#L312
|
||||||
|
private uint seek_timeout_id = 0;
|
||||||
|
[GtkCallback] private bool on_play_position_seek (Gtk.Range range, Gtk.ScrollType scroll_type, double value) {
|
||||||
|
if (this.seek_timeout_id == 0) {
|
||||||
|
this.seek_timeout_id = Timeout.add (500, () => {
|
||||||
|
playbin.seek.begin ((int64) range.adjustment.value, (obj, res) => {
|
||||||
|
this.seek_timeout_id = 0;
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue