Compare commits
2 commits
2b2ace0f5c
...
64dcceea22
Author | SHA1 | Date | |
---|---|---|---|
64dcceea22 | |||
72d4e63249 |
9 changed files with 175 additions and 105 deletions
|
@ -41,4 +41,8 @@ public class Audrey.Application : Adw.Application {
|
||||||
private void on_preferences_action () {
|
private void on_preferences_action () {
|
||||||
message ("app.preferences action activated");
|
message ("app.preferences action activated");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
~Application () {
|
||||||
|
debug ("destroying application");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,10 @@ class Mpris : Object {
|
||||||
public string desktop_entry { owned get { return "eu.callcc.audrey"; } }
|
public string desktop_entry { owned get { return "eu.callcc.audrey"; } }
|
||||||
public string[] supported_uri_schemes { owned get { return {}; } }
|
public string[] supported_uri_schemes { owned get { return {}; } }
|
||||||
public string[] supported_mime_types { owned get { return {}; } }
|
public string[] supported_mime_types { owned get { return {}; } }
|
||||||
|
|
||||||
|
~Mpris () {
|
||||||
|
debug ("destroying mpris");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[DBus (name = "org.mpris.MediaPlayer2.Player")]
|
[DBus (name = "org.mpris.MediaPlayer2.Player")]
|
||||||
|
@ -62,4 +66,8 @@ class MprisPlayer : Object {
|
||||||
public bool can_seek { get; default = false; }
|
public bool can_seek { get; default = false; }
|
||||||
[CCode (notify = false)]
|
[CCode (notify = false)]
|
||||||
public bool can_control { get { return false; } }
|
public bool can_control { get { return false; } }
|
||||||
|
|
||||||
|
~MprisPlayer () {
|
||||||
|
debug ("destroying mpris player");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
234
src/playbin.vala
234
src/playbin.vala
|
@ -52,14 +52,11 @@ public class Playbin : GLib.Object {
|
||||||
public double position { get; private set; default = 0.0; }
|
public double position { get; private set; default = 0.0; }
|
||||||
public double duration { get; private set; default = 0.0; }
|
public double duration { get; private set; default = 0.0; }
|
||||||
|
|
||||||
public Subsonic.Client api { get; set; default = null; }
|
public weak Subsonic.Client api { get; set; default = null; }
|
||||||
|
|
||||||
public ListStore _play_queue;
|
public ListStore _play_queue;
|
||||||
public ListModel play_queue { get { return this._play_queue; } }
|
public ListModel play_queue { get { return this._play_queue; } }
|
||||||
|
|
||||||
// try to prevent wait_event to be called twice
|
|
||||||
private bool is_handling_event = false;
|
|
||||||
|
|
||||||
private async Mpv.Error mpv_command_async (string[] args) {
|
private async Mpv.Error mpv_command_async (string[] args) {
|
||||||
CommandCallback cc = {};
|
CommandCallback cc = {};
|
||||||
|
|
||||||
|
@ -70,6 +67,9 @@ public class Playbin : GLib.Object {
|
||||||
return cc.error;
|
return cc.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// should be Mpv.WakeupCallback, but i think there's a vala bug here
|
||||||
|
private SourceOnceFunc wakeup_callback; // anchor reference here, mpv won't remind us
|
||||||
|
|
||||||
public Playbin () {
|
public Playbin () {
|
||||||
this._play_queue = new ListStore (typeof (Subsonic.Song));
|
this._play_queue = new ListStore (typeof (Subsonic.Song));
|
||||||
|
|
||||||
|
@ -83,123 +83,153 @@ public class Playbin : GLib.Object {
|
||||||
assert (this.mpv.observe_property (2, "playlist-pos", Mpv.Format.INT64) >= 0);
|
assert (this.mpv.observe_property (2, "playlist-pos", Mpv.Format.INT64) >= 0);
|
||||||
assert (this.mpv.observe_property (3, "pause", Mpv.Format.FLAG) >= 0);
|
assert (this.mpv.observe_property (3, "pause", Mpv.Format.FLAG) >= 0);
|
||||||
|
|
||||||
this.mpv.wakeup_callback = () => {
|
int wakeup_fds[2];
|
||||||
Idle.add (() => {
|
try {
|
||||||
if (this.is_handling_event) return false;
|
assert (Unix.open_pipe (wakeup_fds, 0));
|
||||||
this.is_handling_event = true;
|
} catch (Error e) {
|
||||||
|
error (@"could not open pipe for mpv wakeup: $(e.message)");
|
||||||
|
}
|
||||||
|
|
||||||
while (true) {
|
IOChannel wakeup_read = new IOChannel.unix_new (wakeup_fds[0]);
|
||||||
var event = this.mpv.wait_event (0.0);
|
IOChannel wakeup_write = new IOChannel.unix_new (wakeup_fds[1]);
|
||||||
if (event.event_id == Mpv.EventId.NONE) break;
|
|
||||||
|
|
||||||
switch (event.event_id) {
|
wakeup_read.set_close_on_unref (true);
|
||||||
case Mpv.EventId.PROPERTY_CHANGE:
|
wakeup_write.set_close_on_unref (true);
|
||||||
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:
|
try {
|
||||||
assert (data.name == "duration");
|
wakeup_read.set_encoding (null);
|
||||||
if (data.format == Mpv.Format.NONE) {
|
wakeup_write.set_encoding (null);
|
||||||
// this.duration = 0.0; i think this prevents the fallback below from working
|
wakeup_write.set_buffered (false);
|
||||||
} else {
|
} catch (Error e) {
|
||||||
this.duration = data.parse_double ();
|
error (@"could not set up pipes for mpv wakeup: $(e.message)");
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
|
|
||||||
case 2:
|
this.wakeup_callback = () => {
|
||||||
// here as a sanity check
|
try {
|
||||||
// should always match our own play_queu_position/state
|
wakeup_write.write_chars ({0}, null);
|
||||||
assert (data.name == "playlist-pos");
|
} catch (Error e) {
|
||||||
int64 playlist_pos = data.parse_int64 ();
|
error (@"could not write to mpv wakeup pipe: $(e.message)");
|
||||||
if (playlist_pos < 0) {
|
}
|
||||||
if (this.state != PlaybinState.STOPPED) {
|
};
|
||||||
error ("mpv has no current playlist entry, but we think it's index %u", this.play_queue_position);
|
this.mpv.wakeup_callback = this.wakeup_callback;
|
||||||
}
|
|
||||||
assert (this.play_queue_position == this.play_queue.get_n_items ());
|
|
||||||
} else {
|
|
||||||
if (this.state == PlaybinState.STOPPED) {
|
|
||||||
error ("mpv is at playlist entry %u, but we're stopped", (uint) playlist_pos);
|
|
||||||
}
|
|
||||||
if (this.play_queue_position != (uint) playlist_pos) {
|
|
||||||
error ("mpv is at playlist entry %u, but we think it's %u", (uint) playlist_pos, this.play_queue_position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 3:
|
assert (0 < wakeup_read.add_watch (IOCondition.IN, (source, condition) => {
|
||||||
// also here as a sanity check
|
try {
|
||||||
// should always match our own state
|
wakeup_read.read_chars ({0}, null);
|
||||||
assert (data.name == "pause");
|
} catch (Error e) {
|
||||||
bool pause = data.parse_flag ();
|
error (@"could not read from mpv wakeup pipe: $(e.message)");
|
||||||
if (pause && this.state != PlaybinState.PAUSED) {
|
}
|
||||||
error (@"mpv is paused, but we are @(this.state)");
|
|
||||||
}
|
|
||||||
if (!pause && this.state == PlaybinState.PAUSED) {
|
|
||||||
error ("mpv is not paused, but we are paused");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
while (true) {
|
||||||
assert (false);
|
var event = this.mpv.wait_event (0.0);
|
||||||
break;
|
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;
|
break;
|
||||||
|
|
||||||
case Mpv.EventId.START_FILE:
|
case 1:
|
||||||
debug ("START_FILE received");
|
assert (data.name == "duration");
|
||||||
|
if (data.format == Mpv.Format.NONE) {
|
||||||
// estimate duration from api data
|
// this.duration = 0.0; i think this prevents the fallback below from working
|
||||||
// while mpv doesn't know it
|
} else {
|
||||||
this.duration = ((Subsonic.Song) this._play_queue.get_item (this.play_queue_position)).duration;
|
this.duration = data.parse_double ();
|
||||||
|
}
|
||||||
this.new_track ();
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Mpv.EventId.END_FILE:
|
case 2:
|
||||||
var data = event.parse_end_file ();
|
// here as a sanity check
|
||||||
debug (@"END_FILE received (reason: $(data.reason))");
|
// should always match our own play_queu_position/state
|
||||||
|
assert (data.name == "playlist-pos");
|
||||||
if (data.error < 0) {
|
int64 playlist_pos = data.parse_int64 ();
|
||||||
warning ("playback of track aborted: %s", data.error.to_string ());
|
if (playlist_pos < 0) {
|
||||||
}
|
if (this.state != PlaybinState.STOPPED) {
|
||||||
|
error ("mpv has no current playlist entry, but we think it's index %u", this.play_queue_position);
|
||||||
if (data.reason == Mpv.EndFileReason.EOF) {
|
}
|
||||||
// assume this is a proper transition
|
assert (this.play_queue_position == this.play_queue.get_n_items ());
|
||||||
this.play_queue_position += 1;
|
} else {
|
||||||
|
if (this.state == PlaybinState.STOPPED) {
|
||||||
if (this.play_queue_position == this._play_queue.get_n_items ()) {
|
error ("mpv is at playlist entry %u, but we're stopped", (uint) playlist_pos);
|
||||||
// reached the end (?)
|
}
|
||||||
this.state = PlaybinState.STOPPED;
|
if (this.play_queue_position != (uint) playlist_pos) {
|
||||||
this.stopped ();
|
error ("mpv is at playlist entry %u, but we think it's %u", (uint) playlist_pos, this.play_queue_position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Mpv.EventId.COMMAND_REPLY:
|
case 3:
|
||||||
unowned CommandCallback *cc = (CommandCallback *) event.reply_userdata;
|
// also here as a sanity check
|
||||||
cc.error = event.error;
|
// should always match our own state
|
||||||
cc.callback ();
|
assert (data.name == "pause");
|
||||||
|
bool pause = data.parse_flag ();
|
||||||
|
if (pause && this.state != PlaybinState.PAUSED) {
|
||||||
|
error (@"mpv is paused, but we are @(this.state)");
|
||||||
|
}
|
||||||
|
if (!pause && this.state == PlaybinState.PAUSED) {
|
||||||
|
error ("mpv is not paused, but we are paused");
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// ignore by default
|
assert (false);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Mpv.EventId.START_FILE:
|
||||||
|
debug ("START_FILE received");
|
||||||
|
|
||||||
|
// estimate duration from api data
|
||||||
|
// while mpv doesn't know it
|
||||||
|
this.duration = ((Subsonic.Song) this._play_queue.get_item (this.play_queue_position)).duration;
|
||||||
|
|
||||||
|
this.new_track ();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Mpv.EventId.END_FILE:
|
||||||
|
var data = event.parse_end_file ();
|
||||||
|
debug (@"END_FILE received (reason: $(data.reason))");
|
||||||
|
|
||||||
|
if (data.error < 0) {
|
||||||
|
warning ("playback of track aborted: %s", data.error.to_string ());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.reason == Mpv.EndFileReason.EOF) {
|
||||||
|
// assume this is a proper transition
|
||||||
|
this.play_queue_position += 1;
|
||||||
|
|
||||||
|
if (this.play_queue_position == this._play_queue.get_n_items ()) {
|
||||||
|
// reached the end (?)
|
||||||
|
this.state = PlaybinState.STOPPED;
|
||||||
|
this.stopped ();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Mpv.EventId.COMMAND_REPLY:
|
||||||
|
unowned CommandCallback *cc = (CommandCallback *) event.reply_userdata;
|
||||||
|
cc.error = event.error;
|
||||||
|
cc.callback ();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// ignore by default
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.is_handling_event = false;
|
return true;
|
||||||
return false;
|
}));
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void seek (double position) {
|
public void seek (double position) {
|
||||||
|
@ -358,4 +388,8 @@ public class Playbin : GLib.Object {
|
||||||
else if (this.play_queue_position >= to && this.play_queue_position < from) this.play_queue_position += 1;
|
else if (this.play_queue_position >= to && this.play_queue_position < from) this.play_queue_position += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
~Playbin () {
|
||||||
|
debug ("destroying playbin");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -248,4 +248,8 @@ public class Subsonic.Client : Object {
|
||||||
assert (msg.get_status () == Soup.Status.OK);
|
assert (msg.get_status () == Soup.Status.OK);
|
||||||
return yield new Gdk.Pixbuf.from_stream_async (stream, cancellable);
|
return yield new Gdk.Pixbuf.from_stream_async (stream, cancellable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
~Client () {
|
||||||
|
debug ("destroying subsonic client");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ class Ui.PlayQueueSong : Gtk.Box {
|
||||||
|
|
||||||
public string play_icon_name { get; set; default = ""; }
|
public string play_icon_name { get; set; default = ""; }
|
||||||
|
|
||||||
private Playbin playbin;
|
private weak Playbin playbin;
|
||||||
public PlayQueueSong (Playbin playbin) {
|
public PlayQueueSong (Playbin playbin) {
|
||||||
this.playbin = playbin;
|
this.playbin = playbin;
|
||||||
|
|
||||||
|
@ -110,7 +110,7 @@ class Ui.PlayQueueSong : Gtk.Box {
|
||||||
|
|
||||||
[GtkTemplate (ui = "/eu/callcc/audrey/ui/play_queue.ui")]
|
[GtkTemplate (ui = "/eu/callcc/audrey/ui/play_queue.ui")]
|
||||||
public class Ui.PlayQueue : Gtk.Box {
|
public class Ui.PlayQueue : Gtk.Box {
|
||||||
private Playbin _playbin;
|
private weak Playbin _playbin;
|
||||||
public Playbin playbin {
|
public Playbin playbin {
|
||||||
get { return _playbin; }
|
get { return _playbin; }
|
||||||
set {
|
set {
|
||||||
|
@ -165,4 +165,8 @@ public class Ui.PlayQueue : Gtk.Box {
|
||||||
[GtkCallback] private string visible_child_name (uint n_items) {
|
[GtkCallback] private string visible_child_name (uint n_items) {
|
||||||
return n_items > 0 ? "not-empty" : "empty";
|
return n_items > 0 ? "not-empty" : "empty";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
~PlayQueue () {
|
||||||
|
debug ("destroying play queue widget");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
class Ui.Playbar : Gtk.Box {
|
class Ui.Playbar : Gtk.Box {
|
||||||
public Subsonic.Song? song { get; set; }
|
public Subsonic.Song? song { get; set; }
|
||||||
public Gdk.Paintable? playing_cover_art { get; set; }
|
public Gdk.Paintable? playing_cover_art { get; set; }
|
||||||
public Playbin playbin { get; set; }
|
public weak Playbin playbin { get; set; }
|
||||||
public bool show_cover_art { get; set; default = true; }
|
public bool show_cover_art { get; set; default = true; }
|
||||||
|
|
||||||
public int volume {
|
public int volume {
|
||||||
|
@ -82,4 +82,8 @@ class Ui.Playbar : Gtk.Box {
|
||||||
[GtkCallback] private string song_album (Subsonic.Song? song) {
|
[GtkCallback] private string song_album (Subsonic.Song? song) {
|
||||||
return song == null ? "" : song.album;
|
return song == null ? "" : song.album;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
~Playbar () {
|
||||||
|
debug ("destroying playbar widget");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -128,4 +128,8 @@ public class Ui.Setup : Adw.PreferencesDialog {
|
||||||
this.authn_can_edit = true;
|
this.authn_can_edit = true;
|
||||||
}, "server-url", this.server_url, "username", this.username);
|
}, "server-url", this.server_url, "username", this.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
~Setup () {
|
||||||
|
debug ("destroying setup dialog");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,4 +115,8 @@ class Ui.Window : Adw.ApplicationWindow {
|
||||||
[GtkCallback] private bool show_playbar_cover_art (string? stack_child) {
|
[GtkCallback] private bool show_playbar_cover_art (string? stack_child) {
|
||||||
return stack_child != "play-queue";
|
return stack_child != "play-queue";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
~Window () {
|
||||||
|
debug ("destroying main window");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ namespace Mpv {
|
||||||
|
|
||||||
public delegate void WakeupCallback ();
|
public delegate void WakeupCallback ();
|
||||||
|
|
||||||
[CCode (cname = "mpv_handle", free_function = "mpv_destroy")]
|
[CCode (cname = "mpv_handle", free_function = "mpv_terminate_destroy")]
|
||||||
[Compact]
|
[Compact]
|
||||||
public class Handle {
|
public class Handle {
|
||||||
[CCode (cname = "mpv_create")]
|
[CCode (cname = "mpv_create")]
|
||||||
|
@ -43,7 +43,7 @@ namespace Mpv {
|
||||||
[CCode (cname = "mpv_wait_event")]
|
[CCode (cname = "mpv_wait_event")]
|
||||||
public unowned Event? wait_event (double timeout);
|
public unowned Event? wait_event (double timeout);
|
||||||
|
|
||||||
public WakeupCallback wakeup_callback {
|
public unowned WakeupCallback wakeup_callback {
|
||||||
[CCode (cname = "mpv_set_wakeup_callback")] set;
|
[CCode (cname = "mpv_set_wakeup_callback")] set;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,6 +70,10 @@ namespace Mpv {
|
||||||
|
|
||||||
[CCode (cname = "mpv_observe_property")]
|
[CCode (cname = "mpv_observe_property")]
|
||||||
public Error observe_property (uint64 reply_userdata, string name, Format format);
|
public Error observe_property (uint64 reply_userdata, string name, Format format);
|
||||||
|
|
||||||
|
~Handle () {
|
||||||
|
GLib.debug ("destroying mpv handle");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[CCode (cname = "mpv_format", cprefix = "MPV_FORMAT_", has_type_id = false)]
|
[CCode (cname = "mpv_format", cprefix = "MPV_FORMAT_", has_type_id = false)]
|
||||||
|
|
Loading…
Reference in a new issue