audrey/src/playbin.vala

256 lines
8.7 KiB
Vala
Raw Normal View History

enum PlaybinState {
STOPPED,
PAUSED,
PLAYING,
}
2024-10-12 12:28:05 +00:00
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);
2024-10-12 12:28:05 +00:00
// 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 {
2024-10-13 09:56:28 +00:00
get { return this.playbin.mute; }
set { this.playbin.mute = value; }
2024-10-12 12:28:05 +00:00
}
public PlaybinState state { get; private set; default = PlaybinState.STOPPED; }
2024-10-13 17:00:47 +00:00
private bool update_position = false;
2024-10-13 09:56:28 +00:00
public int64 position { get; private set; }
2024-10-12 12:28:05 +00:00
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;
2024-10-12 12:28:05 +00:00
2024-10-13 09:56:28 +00:00
private void source_setup (Gst.Element playbin, dynamic Gst.Element source) {
source.user_agent = "audrey/linux";
2024-10-12 21:11:55 +00:00
}
public Playbin (ListModel play_queue) {
this.play_queue = play_queue;
this.next_uri = new AsyncQueue<string?> ();
this.next_uri.push ("");
2024-10-15 09:11:14 +00:00
// 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);
2024-10-13 09:56:28 +00:00
this.playbin.source_setup.connect (this.source_setup);
this.playbin.about_to_finish.connect (this.about_to_finish);
2024-10-12 12:28:05 +00:00
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 ();
}
});
2024-10-13 17:00:47 +00:00
// regularly update position/duration
2024-10-12 18:17:31 +00:00
Timeout.add (500, () => {
2024-10-13 17:00:47 +00:00
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;
}
2024-10-12 12:28:05 +00:00
}
2024-10-12 18:17:31 +00:00
// keep rerunning
2024-10-12 12:28:05 +00:00
return true;
});
2024-10-13 09:56:28 +00:00
var bus = this.playbin.get_bus ();
bus.add_signal_watch ();
2024-10-12 12:28:05 +00:00
2024-10-13 09:56:28 +00:00
bus.message["error"].connect ((message) => {
Error err;
string? debug;
message.parse_error (out err, out debug);
error ("gst playbin bus error: %s", err.message);
});
2024-10-12 12:28:05 +00:00
2024-10-13 09:56:28 +00:00
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) => {
2024-10-13 17:00:47 +00:00
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 ("");
}
2024-10-12 12:28:05 +00:00
});
2024-10-13 17:00:47 +00:00
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;
});
2024-10-13 09:56:28 +00:00
bus.message["eos"].connect ((message) => {
assert (false); // TODO
2024-10-13 09:56:28 +00:00
});
2024-10-12 12:28:05 +00:00
}
2024-10-13 10:40:29 +00:00
public void seek (int64 position) {
2024-10-13 16:22:44 +00:00
if (!this.playbin.seek_simple (Gst.Format.TIME, Gst.SeekFlags.FLUSH, position)) {
warning ("could not seek");
2024-10-12 12:28:05 +00:00
}
}
public void begin_playback (uint position) {
this.state = PlaybinState.PLAYING;
this.playing_index = position;
2024-10-13 10:40:29 +00:00
this.playbin.set_state (Gst.State.READY);
this.playbin.uri = this.api.stream_uri (((Song) this.play_queue.get_item (position)).id);
2024-10-13 10:40:29 +00:00
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 ();
2024-10-12 12:28:05 +00:00
}
// ASSUMPTION: about-to-finish will be signalled exactly once per track
// even if seeking backwards after
AsyncQueue<string?> next_uri;
2024-10-12 12:28:05 +00:00
// called when uri can be switched for gapless playback
// need async queue because this might be called from a gstreamer thread
2024-10-13 09:56:28 +00:00
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 ();
2024-10-12 12:28:05 +00:00
if (next_uri != "") {
2024-10-12 12:28:05 +00:00
playbin.uri = next_uri;
}
}
2024-10-12 13:36:47 +00:00
public void pause () {
assert (this.state != PlaybinState.STOPPED);
2024-10-12 13:36:47 +00:00
this.playbin.set_state (Gst.State.PAUSED);
this.state = PlaybinState.PAUSED;
2024-10-12 13:36:47 +00:00
}
public void play () {
assert (this.state != PlaybinState.STOPPED);
2024-10-12 13:36:47 +00:00
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;
2024-10-12 13:36:47 +00:00
}
2024-10-12 12:28:05 +00:00
}