replace gstreamer with mpv
lol
This commit is contained in:
parent
c4dbba35c0
commit
925367b180
6 changed files with 351 additions and 252 deletions
|
@ -3,8 +3,6 @@ int main (string[] args) {
|
||||||
Intl.bind_textdomain_codeset (Audrey.Config.GETTEXT_PACKAGE, "UTF-8");
|
Intl.bind_textdomain_codeset (Audrey.Config.GETTEXT_PACKAGE, "UTF-8");
|
||||||
Intl.textdomain (Audrey.Config.GETTEXT_PACKAGE);
|
Intl.textdomain (Audrey.Config.GETTEXT_PACKAGE);
|
||||||
|
|
||||||
Gst.init (ref args);
|
|
||||||
|
|
||||||
var app = new Audrey.Application ();
|
var app = new Audrey.Application ();
|
||||||
return app.run (args);
|
return app.run (args);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,14 +12,13 @@ audrey_sources = [
|
||||||
|
|
||||||
audrey_deps = [
|
audrey_deps = [
|
||||||
config_dep,
|
config_dep,
|
||||||
dependency('gstreamer-1.0', version: '>= 1.24'),
|
|
||||||
dependency('gstreamer-audio-1.0', version: '>= 1.24'),
|
|
||||||
dependency('gtk4', version: '>= 4.16'),
|
dependency('gtk4', version: '>= 4.16'),
|
||||||
dependency('json-glib-1.0', version: '>= 1.10'),
|
dependency('json-glib-1.0', version: '>= 1.10'),
|
||||||
dependency('libadwaita-1', version: '>= 1.6'),
|
dependency('libadwaita-1', version: '>= 1.6'),
|
||||||
dependency('libgcrypt', version: '>= 1.11'),
|
dependency('libgcrypt', version: '>= 1.11'),
|
||||||
dependency('libsecret-1', version: '>= 0.21'),
|
dependency('libsecret-1', version: '>= 0.21'),
|
||||||
dependency('libsoup-3.0', version: '>= 3.6'),
|
dependency('libsoup-3.0', version: '>= 3.6'),
|
||||||
|
dependency('mpv', version: '>= 2.3'),
|
||||||
]
|
]
|
||||||
|
|
||||||
subdir('ui')
|
subdir('ui')
|
||||||
|
|
421
src/playbin.vala
421
src/playbin.vala
|
@ -4,269 +4,282 @@ enum PlaybinState {
|
||||||
PLAYING,
|
PLAYING,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
errordomain PlaybinError {
|
||||||
|
MPV,
|
||||||
|
}
|
||||||
|
|
||||||
|
private void check_mpv_error (int ec) throws PlaybinError {
|
||||||
|
if (ec < 0) {
|
||||||
|
throw new PlaybinError.MPV ("%s", Mpv.error_string (ec));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SourceFuncWrapper {
|
||||||
|
public SourceFunc inner;
|
||||||
|
|
||||||
|
public SourceFuncWrapper () {
|
||||||
|
this.inner = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class Playbin : GLib.Object {
|
class Playbin : GLib.Object {
|
||||||
// dynamic: undocumented vala feature
|
private Mpv.Handle mpv = new Mpv.Handle ();
|
||||||
// lets us access the about-to-finish signal
|
|
||||||
private dynamic Gst.Element playbin = Gst.ElementFactory.make ("playbin3", null);
|
|
||||||
|
|
||||||
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; }
|
public PlaybinState state { get; private set; default = PlaybinState.STOPPED; }
|
||||||
|
|
||||||
public Subsonic.Song? current_song { get; private set; default = null; }
|
private int _volume = 100;
|
||||||
|
public int volume {
|
||||||
|
get { return _volume; }
|
||||||
|
set {
|
||||||
|
_volume = value;
|
||||||
|
mpv_set_property_int64.begin ("volume", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public int64 position { get; private set; default = 0; }
|
public bool _mute = false;
|
||||||
public int64 duration { get; private set; default = 1; } // if 0, the seekbar vanishes
|
public bool mute {
|
||||||
|
get { return _mute; }
|
||||||
|
set {
|
||||||
|
_mute = value;
|
||||||
|
mpv_set_property_flag.begin ("mute", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint play_queue_position { get; private set; }
|
||||||
|
public Subsonic.Song? song { get; private set; }
|
||||||
|
public signal void now_playing ();
|
||||||
|
|
||||||
|
public double position { get; private set; default = 0.0; }
|
||||||
|
public double duration { get; private set; default = 0.0; }
|
||||||
|
|
||||||
public Subsonic.Client api { get; set; default = null; }
|
public Subsonic.Client api { get; set; default = null; }
|
||||||
|
|
||||||
// sent when a new song starts playing
|
|
||||||
// continues: whether the track is a gapless continuation
|
|
||||||
public signal void now_playing (bool continues);
|
|
||||||
public signal void stopped ();
|
|
||||||
|
|
||||||
// the index of the track in the play queue that is currently playing
|
|
||||||
// must equal play queue len iff state is STOPPED
|
|
||||||
public uint current_position { get; private set; }
|
|
||||||
|
|
||||||
// whether we are expecting a gapless continuation next
|
|
||||||
private bool next_gapless;
|
|
||||||
|
|
||||||
private void source_setup (Gst.Element playbin, dynamic Gst.Element source) {
|
|
||||||
source.user_agent = Audrey.Const.user_agent;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ASSUMPTION: about-to-finish will be signalled exactly once per track
|
|
||||||
// even if seeking backwards after
|
|
||||||
private GLib.AsyncQueue<string> next_uri = new GLib.AsyncQueue<string> ();
|
|
||||||
|
|
||||||
private ListModel _play_queue = null;
|
private ListModel _play_queue = null;
|
||||||
private ulong _play_queue_items_changed;
|
|
||||||
public ListModel play_queue {
|
public ListModel play_queue {
|
||||||
get { return _play_queue; }
|
get { return _play_queue; }
|
||||||
set {
|
set {
|
||||||
if (_play_queue != null) {
|
assert (_play_queue == null); // only set this once
|
||||||
SignalHandler.disconnect (_play_queue, _play_queue_items_changed);
|
|
||||||
}
|
|
||||||
_play_queue = value;
|
_play_queue = value;
|
||||||
_play_queue_items_changed = value.items_changed.connect (on_play_queue_items_changed);
|
value.items_changed.connect (on_play_queue_items_changed);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void on_play_queue_items_changed (ListModel play_queue, uint position, uint removed, uint added) {
|
private void on_play_queue_items_changed (ListModel play_queue, uint position, uint removed, uint added) {
|
||||||
if (this.state == PlaybinState.STOPPED) {
|
// FIXME: these should prolly be chained
|
||||||
return;
|
|
||||||
|
for (uint i = 0; i < removed; i += 1) {
|
||||||
|
this.mpv_command.begin ({"playlist-remove", position.to_string ()});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.current_position >= position) {
|
for (uint i = 0; i < added; i += 1) {
|
||||||
if (this.current_position < position+removed) {
|
this.mpv_command.begin ({"loadfile", this.api.stream_uri (((Subsonic.Song) play_queue.get_item (position+i)).id), "insert-at-play", (position+i).to_string ()});
|
||||||
// current track was removed, start playing something else
|
|
||||||
// TODO check if it was actually reordered
|
|
||||||
|
|
||||||
if (position == play_queue.get_n_items ()) {
|
|
||||||
this.stop ();
|
|
||||||
} else {
|
|
||||||
this.select_track (position);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// unaffected
|
|
||||||
// fix up playing index though
|
|
||||||
this.current_position = this.current_position + added - removed;
|
|
||||||
}
|
|
||||||
} else if (this.current_position+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.current_position+1 < play_queue.get_n_items ()) {
|
|
||||||
var song = (Subsonic.Song) play_queue.get_item (this.current_position+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
|
|
||||||
// but stream-start should already be able to take care of that
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool queued_seek = false;
|
private SourceFuncWrapper[] mpv_command_callbacks = {};
|
||||||
|
private int[] mpv_command_error = {};
|
||||||
|
|
||||||
|
private async void mpv_command (string[] args) throws Error {
|
||||||
|
int userdata = -1;
|
||||||
|
for (int i = 0; i < this.mpv_command_callbacks.length; i += 1) {
|
||||||
|
if (this.mpv_command_callbacks[i].inner == null) {
|
||||||
|
userdata = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (userdata == -1) {
|
||||||
|
userdata = this.mpv_command_callbacks.length;
|
||||||
|
this.mpv_command_callbacks += new SourceFuncWrapper ();
|
||||||
|
this.mpv_command_error += 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
check_mpv_error (this.mpv.command_async ((uint64) userdata, args));
|
||||||
|
this.mpv_command_callbacks[userdata].inner = this.mpv_command.callback;
|
||||||
|
yield;
|
||||||
|
|
||||||
|
check_mpv_error (this.mpv_command_error[userdata]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SourceFuncWrapper[] mpv_set_property_callbacks = {};
|
||||||
|
private int[] mpv_set_property_error = {};
|
||||||
|
|
||||||
|
private async void mpv_set_property (string name, Mpv.Format format, void *data) throws Error {
|
||||||
|
int userdata = -1;
|
||||||
|
for (int i = 0; i < this.mpv_set_property_callbacks.length; i += 1) {
|
||||||
|
if (this.mpv_set_property_callbacks[i].inner == null) {
|
||||||
|
userdata = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (userdata == -1) {
|
||||||
|
userdata = this.mpv_set_property_callbacks.length;
|
||||||
|
this.mpv_set_property_callbacks += new SourceFuncWrapper ();
|
||||||
|
this.mpv_set_property_error += 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
check_mpv_error (this.mpv.set_property_async ((uint64) userdata, name, format, data));
|
||||||
|
this.mpv_set_property_callbacks[userdata].inner = this.mpv_set_property.callback;
|
||||||
|
yield;
|
||||||
|
|
||||||
|
check_mpv_error (this.mpv_set_property_error[userdata]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void mpv_set_property_int64 (string name, int64 value) throws Error {
|
||||||
|
yield this.mpv_set_property (name, Mpv.Format.INT64, &value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void mpv_set_property_flag (string name, bool value) throws Error {
|
||||||
|
int flag = value ? 1 : 0;
|
||||||
|
yield this.mpv_set_property (name, Mpv.Format.FLAG, &flag);
|
||||||
|
}
|
||||||
|
|
||||||
public Playbin () {
|
public Playbin () {
|
||||||
this.next_uri.push ("");
|
try {
|
||||||
|
check_mpv_error (this.mpv.initialize ());
|
||||||
this.playbin.source_setup.connect (this.source_setup);
|
check_mpv_error (this.mpv.set_property_string ("user-agent", Audrey.Const.user_agent));
|
||||||
this.playbin.about_to_finish.connect (this.about_to_finish);
|
check_mpv_error (this.mpv.set_property_string ("video", "no"));
|
||||||
|
check_mpv_error (this.mpv.set_property_string ("prefetch-playlist", "yes"));
|
||||||
// regularly update position
|
check_mpv_error (this.mpv.set_property_string ("gapless-audio", "yes"));
|
||||||
Timeout.add (100, () => {
|
check_mpv_error (this.mpv.observe_property (0, "time-pos", Mpv.Format.DOUBLE));
|
||||||
if (this.queued_seek) {
|
check_mpv_error (this.mpv.observe_property (1, "duration", Mpv.Format.DOUBLE));
|
||||||
if (this.playbin.seek_simple (Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE, position)) {
|
check_mpv_error (this.mpv.observe_property (2, "playlist-pos", Mpv.Format.INT64));
|
||||||
this.queued_seek = false;
|
} catch (Error e) {
|
||||||
|
error ("could not initialize mpv: %s", e.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.mpv.wakeup_callback = () => {
|
||||||
|
Idle.add (() => {
|
||||||
|
while (true) {
|
||||||
|
var event = this.mpv.wait_event (0.0);
|
||||||
|
if (event.event_id == Mpv.EventId.NONE) break;
|
||||||
|
|
||||||
|
switch (event.event_id) {
|
||||||
|
case Mpv.EventId.COMMAND_REPLY:
|
||||||
|
this.mpv_command_error[event.reply_userdata] = event.error;
|
||||||
|
var cb = (owned) this.mpv_command_callbacks[event.reply_userdata].inner;
|
||||||
|
cb ();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Mpv.EventId.SET_PROPERTY_REPLY:
|
||||||
|
this.mpv_set_property_error[event.reply_userdata] = event.error;
|
||||||
|
var cb = (owned) this.mpv_set_property_callbacks[event.reply_userdata].inner;
|
||||||
|
cb ();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Mpv.EventId.PROPERTY_CHANGE:
|
||||||
|
switch (event.reply_userdata) {
|
||||||
|
case 0:
|
||||||
|
var data = (Mpv.EventProperty *) event.data;
|
||||||
|
assert (data.name == "time-pos");
|
||||||
|
if (data.format == Mpv.Format.NONE) {
|
||||||
|
this.position = 0.0;
|
||||||
} else {
|
} else {
|
||||||
int64 new_position;
|
assert (data.format == Mpv.Format.DOUBLE);
|
||||||
if (this.playbin.query_position (Gst.Format.TIME, out new_position)) {
|
this.position = * (double *) data.data;
|
||||||
if (new_position > this.duration) this.position = this.duration;
|
|
||||||
else this.position = new_position;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
// keep rerunning
|
case 1:
|
||||||
return true;
|
var data = (Mpv.EventProperty *) event.data;
|
||||||
});
|
assert (data.name == "duration");
|
||||||
|
if (data.format == Mpv.Format.NONE) {
|
||||||
var bus = this.playbin.get_bus ();
|
this.duration = 0.0;
|
||||||
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 duration;
|
|
||||||
assert (this.playbin.query_duration (Gst.Format.TIME, out duration));
|
|
||||||
this.duration = duration;
|
|
||||||
|
|
||||||
// cancel any queued seeks
|
|
||||||
this.queued_seek = false;
|
|
||||||
this.position = 0;
|
|
||||||
|
|
||||||
bool continues = this.next_gapless;
|
|
||||||
if (this.next_gapless) {
|
|
||||||
// advance position in play queue
|
|
||||||
this.current_position += 1;
|
|
||||||
} else {
|
} else {
|
||||||
this.next_gapless = true;
|
assert (data.format == Mpv.Format.DOUBLE);
|
||||||
|
this.duration = * (double *) data.data;
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
var now_playing = (Subsonic.Song) play_queue.get_item (this.current_position);
|
case 2:
|
||||||
if (this.api.stream_uri (now_playing.id) == (string) this.playbin.current_uri) {
|
var data = (Mpv.EventProperty *) event.data;
|
||||||
if (continues) {
|
assert (data.name == "playlist-pos");
|
||||||
this.current_song = now_playing;
|
if (data.format == Mpv.Format.NONE) {
|
||||||
this.now_playing (true);
|
this.play_queue_position = 0;
|
||||||
}
|
|
||||||
|
|
||||||
if (this.current_position+1 < play_queue.get_n_items ()) {
|
|
||||||
var song = (Subsonic.Song) play_queue.get_item (this.current_position+1);
|
|
||||||
this.next_uri.push (this.api.stream_uri (song.id));
|
|
||||||
} else {
|
} else {
|
||||||
this.next_uri.push ("");
|
assert (data.format == Mpv.Format.INT64);
|
||||||
|
this.play_queue_position = (uint) * (int64 *) data.data;
|
||||||
}
|
}
|
||||||
} else {
|
break;
|
||||||
// edge case
|
|
||||||
// just flush everything and pray next stream-start is fine
|
default:
|
||||||
this.select_track (this.current_position);
|
assert (false);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
break;
|
||||||
|
|
||||||
bus.message["state-changed"].connect ((message) => {
|
case Mpv.EventId.START_FILE:
|
||||||
if (message.src != this.playbin) return;
|
// ignore
|
||||||
|
break;
|
||||||
|
|
||||||
Gst.State new_state;
|
case Mpv.EventId.FILE_LOADED:
|
||||||
message.parse_state_changed (null, out new_state, null);
|
this.song = (Subsonic.Song) this.play_queue.get_item (this.play_queue_position);
|
||||||
});
|
this.now_playing ();
|
||||||
|
break;
|
||||||
|
|
||||||
bus.message["eos"].connect ((message) => {
|
case Mpv.EventId.PLAYBACK_RESTART:
|
||||||
this.stop ();
|
// ignore
|
||||||
});
|
break;
|
||||||
|
|
||||||
|
case Mpv.EventId.SEEK:
|
||||||
|
// ignore
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Mpv.EventId.END_FILE:
|
||||||
|
// ignore
|
||||||
|
break;
|
||||||
|
|
||||||
|
// deprecated, ignore
|
||||||
|
case Mpv.EventId.IDLE:
|
||||||
|
case Mpv.EventId.TICK:
|
||||||
|
// uninteresting, ignore
|
||||||
|
case Mpv.EventId.AUDIO_RECONFIG:
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
print ("got unimplemented %s\n", event.event_id.to_string ());
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void seek (int64 position) {
|
return false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void seek (double position) {
|
||||||
this.position = position;
|
this.position = position;
|
||||||
|
this.mpv_command.begin ({"seek", position.to_string (), "absolute"});
|
||||||
if (!this.queued_seek) {
|
|
||||||
if (!this.playbin.seek_simple (Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE, position)) {
|
|
||||||
this.queued_seek = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// manually changes which track in the play queue to play
|
// manually changes which track in the play queue to play
|
||||||
public void select_track (uint position)
|
public void select_track (uint position)
|
||||||
requires (position < this.play_queue.get_n_items ())
|
requires (position < this.play_queue.get_n_items ())
|
||||||
{
|
{
|
||||||
|
this.mpv_command.begin ({"playlist-play-index", position.to_string ()});
|
||||||
this.state = PlaybinState.PLAYING;
|
this.state = PlaybinState.PLAYING;
|
||||||
|
|
||||||
this.current_position = position;
|
|
||||||
this.playbin.set_state (Gst.State.READY);
|
|
||||||
var song = (Subsonic.Song) this.play_queue.get_item (position);
|
|
||||||
this.playbin.uri = this.api.stream_uri (song.id);
|
|
||||||
this.playbin.set_state (Gst.State.PLAYING);
|
|
||||||
this.next_gapless = false;
|
|
||||||
|
|
||||||
// make sure the queue is empty, so next stream-changed can fix it up
|
|
||||||
this.next_uri.try_pop ();
|
|
||||||
// if it was already empty then uhhhh if theres any problems then
|
|
||||||
// playbin.uri wont match up with the current track's stream uri and we can
|
|
||||||
// fix it there
|
|
||||||
|
|
||||||
this.current_song = song;
|
|
||||||
this.position = 0;
|
|
||||||
this.duration = song.duration * Gst.SECOND - 1;
|
|
||||||
this.now_playing (false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void pause () {
|
public void pause () {
|
||||||
assert (this.state != PlaybinState.STOPPED);
|
assert (this.state != PlaybinState.STOPPED);
|
||||||
this.playbin.set_state (Gst.State.PAUSED);
|
|
||||||
this.state = PlaybinState.PAUSED;
|
this.state = PlaybinState.PAUSED;
|
||||||
|
this.mpv_command.begin ({"pause"});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void play () {
|
public void play () {
|
||||||
assert (this.state != PlaybinState.STOPPED);
|
assert (this.state != PlaybinState.STOPPED);
|
||||||
this.playbin.set_state (Gst.State.PLAYING);
|
|
||||||
this.state = PlaybinState.PLAYING;
|
this.state = PlaybinState.PLAYING;
|
||||||
|
this.mpv_command.begin ({"play"});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stop () {
|
public void next_track () {
|
||||||
this.playbin.set_state (Gst.State.READY);
|
assert (this.state != PlaybinState.STOPPED);
|
||||||
this.state = PlaybinState.STOPPED;
|
this.state = PlaybinState.PLAYING;
|
||||||
this.current_position = this.play_queue.get_n_items ();
|
this.mpv_command.begin ({"playlist-next-playlist"});
|
||||||
this.stopped ();
|
}
|
||||||
|
|
||||||
this.position = 0;
|
public void prev_track () {
|
||||||
this.duration = 1;
|
assert (this.state != PlaybinState.STOPPED);
|
||||||
this.current_song = null;
|
this.state = PlaybinState.PLAYING;
|
||||||
|
this.mpv_command.begin ({"playlist-prev-playlist"});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,21 +103,21 @@ template $UiWindow: Adw.ApplicationWindow {
|
||||||
styles [ "heading" ]
|
styles [ "heading" ]
|
||||||
xalign: 0;
|
xalign: 0;
|
||||||
halign: start;
|
halign: start;
|
||||||
label: bind $song_title (template.playbin as <$Playbin>.current_song) as <string>;
|
label: bind $song_title (template.playbin as <$Playbin>.song) as <string>;
|
||||||
ellipsize: end;
|
ellipsize: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
styles [ "caption" ]
|
styles [ "caption" ]
|
||||||
xalign: 0;
|
xalign: 0;
|
||||||
label: bind $song_artist (template.playbin as <$Playbin>.current_song) as <string>;
|
label: bind $song_artist (template.playbin as <$Playbin>.song) as <string>;
|
||||||
ellipsize: end;
|
ellipsize: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
styles [ "caption" ]
|
styles [ "caption" ]
|
||||||
xalign: 0;
|
xalign: 0;
|
||||||
label: bind $song_album (template.playbin as <$Playbin>.current_song) as <string>;
|
label: bind $song_album (template.playbin as <$Playbin>.song) as <string>;
|
||||||
ellipsize: end;
|
ellipsize: end;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -232,9 +232,9 @@ template $UiWindow: Adw.ApplicationWindow {
|
||||||
width-request: 130;
|
width-request: 130;
|
||||||
|
|
||||||
adjustment: Adjustment {
|
adjustment: Adjustment {
|
||||||
lower: 0.0;
|
lower: 0;
|
||||||
value: bind template.volume bidirectional;
|
value: bind template.volume bidirectional;
|
||||||
upper: 1.0;
|
upper: 100;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ class Ui.Window : Adw.ApplicationWindow {
|
||||||
|
|
||||||
private Subsonic.Client api;
|
private Subsonic.Client api;
|
||||||
|
|
||||||
public double volume {
|
public int volume {
|
||||||
get { return this.playbin.volume; }
|
get { return this.playbin.volume; }
|
||||||
set { this.playbin.volume = value; }
|
set { this.playbin.volume = value; }
|
||||||
}
|
}
|
||||||
|
@ -54,12 +54,9 @@ class Ui.Window : Adw.ApplicationWindow {
|
||||||
this.api = api;
|
this.api = api;
|
||||||
this.playbin.api = api;
|
this.playbin.api = api;
|
||||||
|
|
||||||
this.playbin.now_playing.connect ((continues) => {
|
this.playbin.now_playing.connect (() => {
|
||||||
api.scrobble.begin (playbin.current_song.id);
|
api.scrobble.begin (playbin.song.id);
|
||||||
this.play_queue.selection.playbin_select (playbin.current_position);
|
this.play_queue.selection.playbin_select (playbin.play_queue_position);
|
||||||
});
|
|
||||||
this.playbin.stopped.connect (() => {
|
|
||||||
this.play_queue.selection.playbin_select (this.play_queue_store.get_n_items ());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.play_queue.selection.user_selected.connect ((position) => {
|
this.play_queue.selection.user_selected.connect ((position) => {
|
||||||
|
@ -95,10 +92,10 @@ class Ui.Window : Adw.ApplicationWindow {
|
||||||
this.cancel_loading_art = new GLib.Cancellable ();
|
this.cancel_loading_art = new GLib.Cancellable ();
|
||||||
|
|
||||||
this.playing_cover_art = Gdk.Paintable.empty (1, 1);
|
this.playing_cover_art = Gdk.Paintable.empty (1, 1);
|
||||||
if (playbin.current_song != null) {
|
if (playbin.song != null) {
|
||||||
this.cover_art_loading = true;
|
this.cover_art_loading = true;
|
||||||
|
|
||||||
string song_id = playbin.current_song.id;
|
string song_id = playbin.song.id;
|
||||||
this.api.cover_art.begin (song_id, this.cancel_loading_art, (obj, res) => {
|
this.api.cover_art.begin (song_id, this.cancel_loading_art, (obj, res) => {
|
||||||
try {
|
try {
|
||||||
this.playing_cover_art = Gdk.Texture.for_pixbuf (this.api.cover_art.end (res));
|
this.playing_cover_art = Gdk.Texture.for_pixbuf (this.api.cover_art.end (res));
|
||||||
|
@ -120,13 +117,8 @@ class Ui.Window : Adw.ApplicationWindow {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[GtkCallback] private string format_timestamp (int64 ns) {
|
[GtkCallback] private string format_timestamp (double s) {
|
||||||
if (ns == 1) {
|
return "%02d:%02d".printf (((int) s)/60, ((int) s)%60);
|
||||||
// treat 1 nanosecond as a sentinel value
|
|
||||||
return "-";
|
|
||||||
}
|
|
||||||
int s = (int) (ns / Gst.SECOND);
|
|
||||||
return "%02d:%02d".printf (s/60, s%60);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[GtkCallback] private bool on_play_position_seek (Gtk.Range range, Gtk.ScrollType scroll_type, double value) {
|
[GtkCallback] private bool on_play_position_seek (Gtk.Range range, Gtk.ScrollType scroll_type, double value) {
|
||||||
|
@ -163,15 +155,11 @@ class Ui.Window : Adw.ApplicationWindow {
|
||||||
}
|
}
|
||||||
|
|
||||||
[GtkCallback] private void on_skip_forward_clicked () {
|
[GtkCallback] private void on_skip_forward_clicked () {
|
||||||
if (this.playbin.current_position+1 < this.playbin.play_queue.get_n_items ()) {
|
this.playbin.next_track ();
|
||||||
this.playbin.select_track (this.playbin.current_position+1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[GtkCallback] private void on_skip_backward_clicked () {
|
[GtkCallback] private void on_skip_backward_clicked () {
|
||||||
if (this.playbin.current_position > 0) {
|
this.playbin.prev_track ();
|
||||||
this.playbin.select_track (this.playbin.current_position-1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[GtkCallback] private void show_setup_dialog () {
|
[GtkCallback] private void show_setup_dialog () {
|
||||||
|
@ -180,14 +168,14 @@ class Ui.Window : Adw.ApplicationWindow {
|
||||||
|
|
||||||
[GtkCallback] private void seek_backward () {
|
[GtkCallback] private void seek_backward () {
|
||||||
// 10 seconds
|
// 10 seconds
|
||||||
int64 new_position = playbin.position - 10 * Gst.SECOND;
|
double new_position = playbin.position - 10.0;
|
||||||
if (new_position < 0) new_position = 0;
|
if (new_position < 0.0) new_position = 0.0;
|
||||||
this.playbin.seek (new_position);
|
this.playbin.seek (new_position);
|
||||||
}
|
}
|
||||||
|
|
||||||
[GtkCallback] private void seek_forward () {
|
[GtkCallback] private void seek_forward () {
|
||||||
// 10 seconds
|
// 10 seconds
|
||||||
int64 new_position = playbin.position + 10 * Gst.SECOND;
|
double new_position = playbin.position + 10.0;
|
||||||
if (new_position > this.playbin.duration) new_position = this.playbin.duration;
|
if (new_position > this.playbin.duration) new_position = this.playbin.duration;
|
||||||
this.playbin.seek (new_position);
|
this.playbin.seek (new_position);
|
||||||
}
|
}
|
||||||
|
|
101
src/vapi/mpv.vapi
Normal file
101
src/vapi/mpv.vapi
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
[CCode (cheader_filename = "mpv/client.h")]
|
||||||
|
namespace Mpv {
|
||||||
|
|
||||||
|
[CCode (cname = "mpv_error_string")]
|
||||||
|
public unowned string error_string (int error);
|
||||||
|
|
||||||
|
public delegate void WakeupCallback ();
|
||||||
|
|
||||||
|
[CCode (cname = "mpv_handle", free_function = "mpv_destroy")]
|
||||||
|
[Compact]
|
||||||
|
public class Handle {
|
||||||
|
[CCode (cname = "mpv_create")]
|
||||||
|
public Handle ();
|
||||||
|
|
||||||
|
[CCode (cname = "mpv_initialize")]
|
||||||
|
public int initialize ();
|
||||||
|
|
||||||
|
[CCode (cname = "mpv_wait_event")]
|
||||||
|
public unowned Event *wait_event (double timeout);
|
||||||
|
|
||||||
|
public WakeupCallback wakeup_callback {
|
||||||
|
[CCode (cname = "mpv_set_wakeup_callback")] set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "mpv_set_property_string")]
|
||||||
|
public int set_property_string (string name, string data);
|
||||||
|
|
||||||
|
[CCode (cname = "mpv_set_property_async")]
|
||||||
|
public int set_property_async (uint64 reply_userdata, string name, Format format, void *data);
|
||||||
|
|
||||||
|
[CCode (cname = "mpv_command_async")]
|
||||||
|
public int command_async (
|
||||||
|
uint64 reply_userdata,
|
||||||
|
[CCode (array_length = false)]
|
||||||
|
string[] args);
|
||||||
|
|
||||||
|
[CCode (cname = "mpv_observe_property")]
|
||||||
|
public int observe_property (uint64 reply_userdata, string name, Format format);
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "mpv_format", cprefix = "MPV_FORMAT_", has_type_id = false)]
|
||||||
|
public enum Format {
|
||||||
|
NONE,
|
||||||
|
STRING,
|
||||||
|
OSD_STRING,
|
||||||
|
FLAG,
|
||||||
|
INT64,
|
||||||
|
DOUBLE,
|
||||||
|
NODE,
|
||||||
|
NODE_ARRAY,
|
||||||
|
NODE_MAP,
|
||||||
|
BYTE_ARRAY,
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "mpv_event_id", cprefix = "MPV_EVENT_", has_type_id = false)]
|
||||||
|
public enum EventId {
|
||||||
|
NONE,
|
||||||
|
SHUTDOWN,
|
||||||
|
LOG_MESSAGE,
|
||||||
|
GET_PROPERTY_REPLY,
|
||||||
|
SET_PROPERTY_REPLY,
|
||||||
|
COMMAND_REPLY,
|
||||||
|
START_FILE,
|
||||||
|
END_FILE,
|
||||||
|
FILE_LOADED,
|
||||||
|
CLIENT_MESSAGE,
|
||||||
|
VIDEO_RECONFIG,
|
||||||
|
AUDIO_RECONFIG,
|
||||||
|
SEEK,
|
||||||
|
PLAYBACK_RESTART,
|
||||||
|
PROPERTY_CHANGE,
|
||||||
|
QUEUE_OVERFLOW,
|
||||||
|
HOOK,
|
||||||
|
|
||||||
|
// deprecated
|
||||||
|
IDLE,
|
||||||
|
TICK,
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "mpv_event")]
|
||||||
|
public struct Event {
|
||||||
|
EventId event_id;
|
||||||
|
int error;
|
||||||
|
uint64 reply_userdata;
|
||||||
|
void *data;
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "mpv_event_start_file")]
|
||||||
|
public struct EventStartFile {
|
||||||
|
int64 playlist_entry_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "mpv_event_property")]
|
||||||
|
public struct EventProperty {
|
||||||
|
string name;
|
||||||
|
Format format;
|
||||||
|
void *data;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue