Buffer entire songs

This commit is contained in:
Jonas Kvinge 2024-06-23 21:46:33 +02:00
parent 8ddd309d5d
commit c87b56adcb
3 changed files with 210 additions and 34 deletions

View File

@ -66,6 +66,7 @@ const char *GstEngine::kAutoSink = "autoaudiosink";
const char *GstEngine::kALSASink = "alsasink";
namespace {
constexpr char kOpenALSASink[] = "openalsink";
constexpr char kOSSSink[] = "osssink";
constexpr char kOSS4Sink[] = "oss4sink";
@ -81,6 +82,7 @@ constexpr int kDiscoveryTimeoutS = 10;
constexpr qint64 kTimerIntervalNanosec = 1000 * kNsecPerMsec; // 1s
constexpr qint64 kPreloadGapNanosec = 8000 * kNsecPerMsec; // 8s
constexpr qint64 kSeekDelayNanosec = 100 * kNsecPerMsec; // 100msec
} // namespace
GstEngine::GstEngine(SharedPtr<TaskManager> task_manager, QObject *parent)
@ -256,11 +258,11 @@ bool GstEngine::Play(const quint64 offset_nanosec) {
watcher->deleteLater();
PlayDone(ret, offset_nanosec, pipeline_id);
});
QFuture<GstStateChangeReturn> future = current_pipeline_->SetState(GST_STATE_PLAYING);
QFuture<GstStateChangeReturn> future = current_pipeline_->SetTargetState(GST_STATE_PLAYING);
watcher->setFuture(future);
if (is_fading_out_to_pause_) {
current_pipeline_->SetState(GST_STATE_PAUSED);
current_pipeline_->SetTargetState(GST_STATE_PAUSED);
}
return true;
@ -312,8 +314,9 @@ void GstEngine::Pause() {
StartFadeoutPause();
}
else {
current_pipeline_->SetState(GST_STATE_PAUSED);
current_pipeline_->SetTargetState(GST_STATE_PAUSED);
emit StateChanged(State::Paused);
emit StateChanged(EngineBase::State::Paused);
StopTimers();
}
}
@ -325,7 +328,7 @@ void GstEngine::Unpause() {
if (!current_pipeline_ || current_pipeline_->is_buffering()) return;
if (current_pipeline_->state() == GST_STATE_PAUSED) {
current_pipeline_->SetState(GST_STATE_PLAYING);
current_pipeline_->SetTargetState(GST_STATE_PLAYING);
// Check if we faded out last time. If yes, fade in no matter what the settings say.
// If we pause with fadeout, deactivate fadeout and resume playback, the player would be muted if not faded in.
@ -630,8 +633,8 @@ void GstEngine::FadeoutFinished() {
void GstEngine::FadeoutPauseFinished() {
fadeout_pause_pipeline_->SetState(GST_STATE_PAUSED);
current_pipeline_->SetState(GST_STATE_PAUSED);
fadeout_pause_pipeline_->SetTargetState(GST_STATE_PAUSED);
current_pipeline_->SetTargetState(GST_STATE_PAUSED);
emit StateChanged(State::Paused);
StopTimers();

View File

@ -64,6 +64,12 @@
namespace {
constexpr int GST_PLAY_FLAG_VIDEO = 0x00000001;
constexpr int GST_PLAY_FLAG_AUDIO = 0x00000002;
constexpr int GST_PLAY_FLAG_DOWNLOAD = 0x00000080;
constexpr int GST_PLAY_FLAG_BUFFERING = 0x00000100;
constexpr int GST_PLAY_FLAG_SOFT_VOLUME = 0x00000010;
constexpr int kGstStateTimeoutNanosecs = 10000000;
constexpr int kFaderFudgeMsec = 2000;
@ -94,7 +100,6 @@ GstEnginePipeline::GstEnginePipeline(QObject *parent)
buffer_duration_nanosec_(BackendSettingsPage::kDefaultBufferDuration * kNsecPerMsec),
buffer_low_watermark_(BackendSettingsPage::kDefaultBufferLowWatermark),
buffer_high_watermark_(BackendSettingsPage::kDefaultBufferHighWatermark),
buffering_(false),
proxy_authentication_(false),
channels_enabled_(false),
channels_(0),
@ -111,6 +116,12 @@ GstEnginePipeline::GstEnginePipeline(QObject *parent)
pipeline_active_(false),
pending_seek_nanosec_(-1),
last_known_position_ns_(0),
target_state_(GST_STATE_NULL),
current_state_(GST_STATE_NULL),
active_state_(GST_STATE_NULL),
buffering_(false),
buffering_finished_(false),
live_stream_(false),
next_uri_set_(false),
next_uri_reset_(false),
ebur128_loudness_normalizing_gain_db_(0.0),
@ -396,6 +407,8 @@ bool GstEnginePipeline::InitFromUrl(const QUrl &media_url, const QUrl &stream_ur
gint flags = 0;
g_object_get(G_OBJECT(pipeline_), "flags", &flags, nullptr);
flags |= 0x00000002;
flags |= 0x00000080;
flags |= 0x00000100;
flags &= ~0x00000001;
flags &= ~0x00000010;
g_object_set(G_OBJECT(pipeline_), "flags", flags, nullptr);
@ -1025,10 +1038,10 @@ void GstEnginePipeline::SourceSetupCallback(GstElement *playbin, GstElement *sou
#endif
// If the pipeline was buffering we stop that now.
if (instance->buffering_) {
if (!instance->buffer_timeout_running_ && instance->buffering_ && instance->target_state_ == GST_STATE_PLAYING) {
instance->buffering_ = false;
emit instance->BufferingFinished();
instance->SetState(GST_STATE_PLAYING);
instance->SetCurrentState(GST_STATE_PLAYING);
}
}
@ -1337,6 +1350,10 @@ GstBusSyncReply GstEnginePipeline::BusSyncCallback(GstBus *bus, GstMessage *msg,
instance->BufferingMessageReceived(msg);
break;
case GST_MESSAGE_ASYNC_DONE:
instance->AsyncDoneMessageReceived(msg);
break;
case GST_MESSAGE_STREAM_STATUS:
instance->StreamStatusMessageReceived(msg);
break;
@ -1582,6 +1599,7 @@ void GstEnginePipeline::StateChangedMessageReceived(GstMessage *msg) {
gst_message_parse_state_changed(msg, &old_state, &new_state, &pending);
qLog(Debug) << "Pipeline state changed from" << GstStateText(old_state) << "to" << GstStateText(new_state);
active_state_ = new_state;
if (!pipeline_active_ && (new_state == GST_STATE_PAUSED || new_state == GST_STATE_PLAYING)) {
qLog(Debug) << "Pipeline is active";
@ -1610,14 +1628,14 @@ void GstEnginePipeline::StateChangedMessageReceived(GstMessage *msg) {
if (next_uri_set_ && new_state == GST_STATE_READY) {
next_uri_set_ = false;
g_object_set(G_OBJECT(pipeline_), "uri", gst_url_.constData(), nullptr);
if (pending_seek_nanosec_ == -1) {
qLog(Debug) << "Reverting next uri and going to playing state.";
SetState(GST_STATE_PLAYING);
if (pending_seek_nanosec_ == -1 && active_state_ != target_state_) {
qLog(Debug) << "Reverting next uri and going to target state.";
SetCurrentState(target_state_);
}
else {
qLog(Debug) << "Reverting next uri and going to paused state.";
next_uri_reset_ = true;
SetState(GST_STATE_PAUSED);
SetCurrentState(GST_STATE_PAUSED);
}
}
}
@ -1631,26 +1649,120 @@ void GstEnginePipeline::BufferingMessageReceived(GstMessage *msg) {
return;
}
int percent = 0;
gst_message_parse_buffering(msg, &percent);
int buffering_percent = 0;
gst_message_parse_buffering(msg, &buffering_percent);
const GstState current_state = state();
if (percent == 0 && current_state == GST_STATE_PLAYING && !buffering_) {
if (buffering_ && buffering_percent == 100) {
if (!buffer_timeout_running_) {
buffering_finished_ = true;
}
//if (current_state_ != GST_STATE_PLAYING && target_state_ == GST_STATE_PLAYING) {
//SetCurrentState(GST_STATE_PLAYING);
//}
//emit BufferingFinished();
}
else if (!buffering_ && buffering_percent < 100) {
buffering_finished_ = false;
buffering_ = true;
if (current_state_ != GST_STATE_PAUSED && target_state_ == GST_STATE_PLAYING) {
SetCurrentState(GST_STATE_PAUSED);
}
emit BufferingStarted();
}
else {
qLog(Debug) << "Buffering" << buffering_percent;
emit BufferingProgress(buffering_percent);
}
SetState(GST_STATE_PAUSED);
}
else if (percent == 100 && buffering_) {
buffering_ = false;
emit BufferingFinished();
}
SetState(GST_STATE_PLAYING);
void GstEnginePipeline::AsyncDoneMessageReceived(GstMessage *msg) {
qLog(Debug) << __PRETTY_FUNCTION__ << GST_ELEMENT(GST_MESSAGE_SRC(msg));
// Only handle buffering messages from the queue2 element in audiobin - not the one that's created automatically by playbin.
if (GST_ELEMENT(GST_MESSAGE_SRC(msg)) != audioqueue_) {
//return;
}
else if (buffering_) {
emit BufferingProgress(percent);
qLog(Debug) << __PRETTY_FUNCTION__;
if (buffering_) {
if (!buffer_timeout_running_) {
buffering_finished_ = false;
buffer_timeout_running_ = true;
g_timeout_add(500, GstEnginePipeline::BufferTimeoutCallback, this);
}
}
else {
if (current_state_ != target_state_) {
SetCurrentState(target_state_);
}
}
}
gboolean GstEnginePipeline::BufferTimeoutCallback(gpointer self) {
GstEnginePipeline *instance = reinterpret_cast<GstEnginePipeline*>(self);
GstQuery *query = gst_query_new_buffering(GST_FORMAT_TIME);
if (!query) {
return FALSE;
}
if (!gst_element_query(instance->audioqueue_, query)) {
return TRUE;
}
gboolean busy = FALSE;
gint percent = 0;
gst_query_parse_buffering_percent(query, &busy, &percent);
gint64 estimated_total = 0;
gst_query_parse_buffering_range(query, nullptr, nullptr, nullptr, &estimated_total);
if (estimated_total == -1) estimated_total = 0;
// Calculate the remaining playback time
gint64 position = 0;
if (!gst_element_query_position(instance->audioqueue_, GST_FORMAT_TIME, &position)) {
position = -1;
}
gint64 duration = 0;
if (!gst_element_query_duration(instance->audioqueue_, GST_FORMAT_TIME, &duration)) {
duration = -1;
}
guint64 duration_left = 0;
if (duration != -1 && position != -1) {
duration_left = GST_TIME_AS_MSECONDS(duration - position);
}
else {
duration_left = 0;
}
qLog(Debug) << "Play duration left" << duration_left << "estimated total:" << estimated_total << "precent:" << percent;
// We are buffering or the estimated download time is bigger than the remaining playback time. We keep buffering.
const bool buffering_needed = (busy || estimated_total * 1.1 > duration_left);
instance->buffer_timeout_running_ = buffering_needed;
instance->buffering_finished_ = !buffering_needed;
instance->buffering_ = buffering_needed;
if (buffering_needed) {
qLog(Debug) << "Buffering is active.";
}
else {
qLog(Debug) << "Buffering is finished." << instance->current_state_ << instance->current_state_;
if (instance->current_state_ != instance->target_state_) {
instance->SetCurrentState(instance->target_state_);
}
emit instance->BufferingFinished();
}
return instance->buffering_;
}
@ -1680,24 +1792,72 @@ GstState GstEnginePipeline::state() const {
GstState s = GST_STATE_NULL, sp = GST_STATE_NULL;
if (!pipeline_ || gst_element_get_state(pipeline_, &s, &sp, kGstStateTimeoutNanosecs) == GST_STATE_CHANGE_FAILURE) {
return GST_STATE_NULL;
return active_state_;
}
return s;
}
QFuture<GstStateChangeReturn> GstEnginePipeline::SetState(const GstState state) {
QFuture<GstStateChangeReturn> GstEnginePipeline::SetTargetState(const GstState state) {
qLog(Debug) << "Setting pipeline state to" << GstStateText(state);
return QtConcurrent::run(&set_state_threadpool_, &gst_element_set_state, pipeline_, state);
qLog(Debug) << "Setting pipeline target state to" << GstStateText(state) << buffering_finished_;
target_state_ = state;
return SetCurrentState(state == GST_STATE_PLAYING && !buffering_finished_ ? GST_STATE_PAUSED : state);
}
QFuture<GstStateChangeReturn> GstEnginePipeline::SetCurrentState(const GstState state) {
qLog(Debug) << "Setting pipeline current state to" << GstStateText(state);
current_state_ = state;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QFuture<GstStateChangeReturn> future = QtConcurrent::run(&set_state_threadpool_, &gst_element_set_state, pipeline_, state);
#else
QFuture<GstStateChangeReturn> future = QtConcurrent::run(this, &gst_element_set_state, pipeline_, state);
#endif
QFutureWatcher<GstStateChangeReturn> *watcher = new QFutureWatcher<GstStateChangeReturn>(sender());
QObject::connect(watcher, &QFutureWatcher<GstStateChangeReturn>::finished, this, [this, watcher, state](){
const GstStateChangeReturn state_change = watcher->result();
watcher->deleteLater();
SetStateFinished(state, state_change);
});
watcher->setFuture(future);
return future;
}
void GstEnginePipeline::SetStateFinished(const GstState state, const GstStateChangeReturn state_change) {
switch (state_change) {
case GST_STATE_CHANGE_SUCCESS:
case GST_STATE_CHANGE_ASYNC:
case GST_STATE_CHANGE_NO_PREROLL:
qLog(Debug) << "Pipeline state successfully set to" << GstStateText(state);
active_state_ = state;
if (state == GST_STATE_PAUSED) {
live_stream_ = state_change == GST_STATE_CHANGE_NO_PREROLL;
qLog(Debug) << "Live stream:" << live_stream_;
}
break;
case GST_STATE_CHANGE_FAILURE:
qLog(Error) << "Failed to set pipeline to state" << GstStateText(state);
break;
}
}
void GstEnginePipeline::SetStateDelayed(const GstState state) {
if (state == target_state_) return;
QMetaObject::invokeMethod(this, [this, state]() {
QTimer::singleShot(300, this, [this, state]() { SetState(state); });
QTimer::singleShot(300, this, [this, state]() { SetTargetState(state); });
}, Qt::QueuedConnection);
}
@ -1716,7 +1876,9 @@ bool GstEnginePipeline::Seek(const qint64 nanosec) {
if (next_uri_set_) {
pending_seek_nanosec_ = nanosec;
SetState(GST_STATE_READY);
if (target_state_ != GST_STATE_READY) {
SetTargetState(GST_STATE_READY);
}
return true;
}

View File

@ -89,7 +89,8 @@ class GstEnginePipeline : public QObject {
void RemoveAllBufferConsumers();
// Control the music playback
Q_INVOKABLE QFuture<GstStateChangeReturn> SetState(const GstState state);
Q_INVOKABLE QFuture<GstStateChangeReturn> SetTargetState(const GstState state);
Q_INVOKABLE QFuture<GstStateChangeReturn> SetCurrentState(const GstState state);
void SetStateDelayed(const GstState state);
Q_INVOKABLE bool Seek(const qint64 nanosec);
void SeekQueued(const qint64 nanosec);
@ -173,12 +174,14 @@ class GstEnginePipeline : public QObject {
static GstBusSyncReply BusSyncCallback(GstBus *bus, GstMessage *msg, gpointer self);
static gboolean BusWatchCallback(GstBus *bus, GstMessage *msg, gpointer self);
static void TaskEnterCallback(GstTask *task, GThread *thread, gpointer self);
static gboolean BufferTimeoutCallback(gpointer self);
void TagMessageReceived(GstMessage *msg);
void ErrorMessageReceived(GstMessage *msg);
void ElementMessageReceived(GstMessage *msg);
void StateChangedMessageReceived(GstMessage *msg);
void BufferingMessageReceived(GstMessage *msg);
void AsyncDoneMessageReceived(GstMessage *msg);
void StreamStatusMessageReceived(GstMessage *msg);
void StreamStartMessageReceived();
@ -190,6 +193,7 @@ class GstEnginePipeline : public QObject {
void UpdateEqualizer();
private slots:
void SetStateFinished(const GstState state, const GstStateChangeReturn state_change);
void FaderTimelineFinished();
private:
@ -231,7 +235,6 @@ class GstEnginePipeline : public QObject {
quint64 buffer_duration_nanosec_;
double buffer_low_watermark_;
double buffer_high_watermark_;
bool buffering_;
// Proxy
QString proxy_address_;
@ -297,6 +300,14 @@ class GstEnginePipeline : public QObject {
// it here so that we can use it when using gst_element_query_position() is not possible.
mutable gint64 last_known_position_ns_;
GstState target_state_;
GstState current_state_;
GstState active_state_;
bool buffering_;
bool buffering_finished_;
bool buffer_timeout_running_;
bool live_stream_;
// Complete the transition to the next song when it starts playing
bool next_uri_set_;
bool next_uri_reset_;