2024-10-13 15:21:29 +00:00
|
|
|
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
|
2024-10-13 15:21:29 +00:00
|
|
|
private dynamic Gst.Element playbin = Gst.ElementFactory.make ("playbin3", null);
|
2024-10-12 12:28:05 +00:00
|
|
|
|
2024-10-13 10:40:29 +00:00
|
|
|
// used to prevent stale seeks
|
|
|
|
private uint64 stream_counter = 0;
|
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
|
|
|
}
|
|
|
|
|
2024-10-13 15:21:29 +00:00
|
|
|
public PlaybinState state { get; private set; default = PlaybinState.STOPPED; }
|
|
|
|
|
2024-10-13 10:40:29 +00:00
|
|
|
private bool seeking = 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; }
|
|
|
|
|
2024-10-13 15:21:29 +00:00
|
|
|
private bool notify_next_transition;
|
|
|
|
|
|
|
|
// 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 ();
|
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) {
|
2024-10-12 21:24:23 +00:00
|
|
|
source.user_agent = "audrey/linux";
|
2024-10-12 21:11:55 +00:00
|
|
|
}
|
|
|
|
|
2024-10-12 12:28:05 +00:00
|
|
|
construct {
|
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
|
|
|
|
|
|
|
// regularly update position
|
2024-10-12 18:17:31 +00:00
|
|
|
Timeout.add (500, () => {
|
2024-10-13 10:40:29 +00:00
|
|
|
if (!this.seeking) {
|
|
|
|
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);
|
|
|
|
});
|
|
|
|
|
2024-10-13 10:40:29 +00:00
|
|
|
bus.message["state-changed"].connect ((message) => {
|
|
|
|
if (message.src != this.playbin) return;
|
|
|
|
|
|
|
|
Gst.State old_state;
|
|
|
|
Gst.State new_state;
|
|
|
|
message.parse_state_changed (out old_state, out new_state, null);
|
|
|
|
|
2024-10-13 15:21:29 +00:00
|
|
|
if (new_state == Gst.State.PLAYING) {
|
2024-10-13 10:40:29 +00:00
|
|
|
if (this.queued_seek_position < 0) {
|
|
|
|
this.seeking = false;
|
|
|
|
} else if (this.queued_seek_counter == this.stream_counter) {
|
|
|
|
assert (this.playbin.seek_simple (Gst.Format.TIME, Gst.SeekFlags.FLUSH, this.queued_seek_position));
|
|
|
|
}
|
|
|
|
this.queued_seek_position = -1;
|
|
|
|
}
|
2024-10-13 09:56:28 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
bus.message["stream-start"].connect ((message) => {
|
2024-10-13 10:40:29 +00:00
|
|
|
this.stream_counter += 1;
|
|
|
|
|
2024-10-13 15:21:29 +00:00
|
|
|
if (notify_next_transition) {
|
|
|
|
this.song_transition ((string) this.playbin.current_uri);
|
|
|
|
} else {
|
|
|
|
notify_next_transition = true;
|
|
|
|
}
|
2024-10-12 12:28:05 +00:00
|
|
|
});
|
2024-10-13 16:04:59 +00:00
|
|
|
|
|
|
|
bus.message["async-done"].connect ((message) => {
|
|
|
|
int64 new_duration;
|
|
|
|
assert (this.playbin.query_duration (Gst.Format.TIME, out new_duration));
|
|
|
|
this.duration = new_duration;
|
|
|
|
});
|
2024-10-13 09:56:28 +00:00
|
|
|
|
|
|
|
bus.message["eos"].connect ((message) => {
|
2024-10-13 15:21:29 +00:00
|
|
|
assert (notify_next_transition);
|
|
|
|
this.playback_finished ();
|
2024-10-13 09:56:28 +00:00
|
|
|
});
|
2024-10-12 12:28:05 +00:00
|
|
|
}
|
|
|
|
|
2024-10-13 10:40:29 +00:00
|
|
|
private uint64 queued_seek_counter;
|
|
|
|
private int64 queued_seek_position = -1;
|
|
|
|
|
|
|
|
public void seek (int64 position) {
|
2024-10-13 15:21:29 +00:00
|
|
|
if (this.state == PlaybinState.STOPPED) return;
|
|
|
|
|
|
|
|
if (this.playbin.seek_simple (Gst.Format.TIME, Gst.SeekFlags.FLUSH, position)) {
|
2024-10-13 10:40:29 +00:00
|
|
|
this.seeking = true;
|
|
|
|
this.position = position;
|
|
|
|
} else {
|
|
|
|
// queue this seek
|
|
|
|
this.queued_seek_counter = this.stream_counter;
|
|
|
|
this.queued_seek_position = position;
|
2024-10-12 12:28:05 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-13 15:21:29 +00:00
|
|
|
public void begin_playback (string uri) {
|
2024-10-13 10:40:29 +00:00
|
|
|
this.playbin.set_state (Gst.State.READY);
|
2024-10-12 12:28:05 +00:00
|
|
|
this.playbin.uri = uri;
|
2024-10-13 10:40:29 +00:00
|
|
|
this.playbin.set_state (Gst.State.PLAYING);
|
2024-10-12 12:28:05 +00:00
|
|
|
|
2024-10-13 15:21:29 +00:00
|
|
|
this.state = PlaybinState.PLAYING;
|
|
|
|
this.notify_next_transition = false;
|
2024-10-12 12:28:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Mutex next_uri_lock;
|
|
|
|
string? next_uri;
|
|
|
|
|
2024-10-13 15:21:29 +00:00
|
|
|
public void prepare_next (string? next_uri) {
|
2024-10-12 12:28:05 +00:00
|
|
|
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
|
2024-10-13 09:56:28 +00:00
|
|
|
private void about_to_finish (dynamic Gst.Element playbin) {
|
2024-10-12 12:28:05 +00:00
|
|
|
this.next_uri_lock.lock ();
|
|
|
|
string? next_uri = this.next_uri;
|
|
|
|
this.next_uri_lock.unlock ();
|
|
|
|
|
|
|
|
if (next_uri != null) {
|
|
|
|
playbin.uri = next_uri;
|
|
|
|
}
|
|
|
|
}
|
2024-10-12 13:36:47 +00:00
|
|
|
|
|
|
|
public void pause () {
|
2024-10-13 15:21:29 +00:00
|
|
|
assert (this.state != PlaybinState.STOPPED);
|
2024-10-12 13:36:47 +00:00
|
|
|
this.playbin.set_state (Gst.State.PAUSED);
|
2024-10-13 15:21:29 +00:00
|
|
|
this.state = PlaybinState.PAUSED;
|
2024-10-12 13:36:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public void play () {
|
2024-10-13 15:21:29 +00:00
|
|
|
assert (this.state != PlaybinState.STOPPED);
|
2024-10-12 13:36:47 +00:00
|
|
|
this.playbin.set_state (Gst.State.PLAYING);
|
2024-10-13 15:21:29 +00:00
|
|
|
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
|
|
|
}
|