Fix and improve gapless playback

If "about-to-finish" was emitted before the preload time was reached, we never set the next uri, so gapless playback was broken.
Make sure to always set the next uri, and increase preload gap from 5 to 8 seconds.
This commit is contained in:
Jonas Kvinge 2023-04-22 03:54:09 +02:00
parent f4600bd8eb
commit c96498758f
6 changed files with 56 additions and 28 deletions

View File

@ -190,7 +190,7 @@ void Engine::Base::ReloadSettings() {
}
void Engine::Base::EmitAboutToEnd() {
void Engine::Base::EmitAboutToFinish() {
if (about_to_end_emitted_) {
return;

View File

@ -102,9 +102,7 @@ class Base : public QObject {
public slots:
virtual void ReloadSettings();
void UpdateVolume(const uint volume);
protected:
void EmitAboutToEnd();
void EmitAboutToFinish();
public:
@ -217,7 +215,6 @@ class Base : public QObject {
bool http2_enabled_;
bool strict_ssl_enabled_;
private:
bool about_to_end_emitted_;
Q_DISABLE_COPY(Base)

View File

@ -70,6 +70,9 @@ const char *GstEngine::InterAudiosink = "interaudiosink";
const char *GstEngine::kDirectSoundSink = "directsoundsink";
const char *GstEngine::kOSXAudioSink = "osxaudiosink";
const int GstEngine::kDiscoveryTimeoutS = 10;
const qint64 GstEngine::kTimerIntervalNanosec = 1000 * kNsecPerMsec; // 1s
const qint64 GstEngine::kPreloadGapNanosec = 8000 * kNsecPerMsec; // 8s
const qint64 GstEngine::kSeekDelayNanosec = 100 * kNsecPerMsec; // 100msec
GstEngine::GstEngine(TaskManager *task_manager, QObject *parent)
: Engine::Base(Engine::EngineType::GStreamer, parent),
@ -162,7 +165,7 @@ void GstEngine::StartPreloading(const QUrl &media_url, const QUrl &stream_url, c
// No crossfading, so we can just queue the new URL in the existing pipeline and get gapless playback (hopefully)
if (current_pipeline_) {
current_pipeline_->SetNextUrl(media_url, stream_url, gst_url, beginning_nanosec, force_stop_at_end ? end_nanosec : 0);
current_pipeline_->PrepareNextUrl(media_url, stream_url, gst_url, beginning_nanosec, force_stop_at_end ? end_nanosec : 0);
// Add request to discover the stream
if (discoverer_) {
if (!gst_discoverer_discover_uri_async(discoverer_, gst_url.constData())) {
@ -502,20 +505,18 @@ void GstEngine::timerEvent(QTimerEvent *e) {
if (e->timerId() != timer_id_) return;
if (current_pipeline_) {
const qint64 current_position = position_nanosec();
if (current_pipeline_ && !about_to_end_emitted_) {
const qint64 current_length = length_nanosec();
const qint64 remaining = current_length - current_position;
const qint64 fudge = kTimerIntervalNanosec + 100 * kNsecPerMsec; // Mmm fudge
const qint64 gap = static_cast<qint64>(buffer_duration_nanosec_) + (autocrossfade_enabled_ ? fadeout_duration_nanosec_ : kPreloadGapNanosec);
// Only if we know the length of the current stream...
if (current_length > 0) {
const qint64 current_position = position_nanosec();
const qint64 remaining = current_length - current_position;
const qint64 fudge = kTimerIntervalNanosec + 100 * kNsecPerMsec; // Mmm fudge
const qint64 gap = static_cast<qint64>(buffer_duration_nanosec_) + (autocrossfade_enabled_ ? fadeout_duration_nanosec_ : kPreloadGapNanosec);
// Emit TrackAboutToEnd when we're a few seconds away from finishing
if (remaining < gap + fudge) {
EmitAboutToEnd();
qLog(Debug) << "Stream from URL" << media_url_.toString() << "about to end in" << remaining / kNsecPerSec << "seconds. Fuge:" << fudge / kNsecPerMsec << "+" << "Gap:" << gap / kNsecPerMsec;
EmitAboutToFinish();
}
}
}
@ -782,15 +783,19 @@ void GstEngine::StartFadeoutPause() {
}
void GstEngine::StartTimers() {
StopTimers();
timer_id_ = startTimer(kTimerIntervalNanosec / kNsecPerMsec);
}
void GstEngine::StopTimers() {
if (timer_id_ != -1) {
killTimer(timer_id_);
timer_id_ = -1;
}
}
std::shared_ptr<GstEnginePipeline> GstEngine::CreatePipeline() {
@ -824,6 +829,7 @@ std::shared_ptr<GstEnginePipeline> GstEngine::CreatePipeline() {
QObject::connect(ret.get(), &GstEnginePipeline::BufferingProgress, this, &GstEngine::BufferingProgress);
QObject::connect(ret.get(), &GstEnginePipeline::BufferingFinished, this, &GstEngine::BufferingFinished);
QObject::connect(ret.get(), &GstEnginePipeline::VolumeChanged, this, &EngineBase::UpdateVolume);
QObject::connect(ret.get(), &GstEnginePipeline::AboutToFinish, this, &EngineBase::EmitAboutToFinish);
return ret;

View File

@ -159,9 +159,9 @@ class GstEngine : public Engine::Base, public GstBufferConsumer {
static const char *kDirectSoundSink;
static const char *kOSXAudioSink;
static const int kDiscoveryTimeoutS;
static const qint64 kTimerIntervalNanosec = 1000 * kNsecPerMsec; // 1s
static const qint64 kPreloadGapNanosec = 5000 * kNsecPerMsec; // 5s
static const qint64 kSeekDelayNanosec = 100 * kNsecPerMsec; // 100msec
static const qint64 kTimerIntervalNanosec;
static const qint64 kPreloadGapNanosec;
static const qint64 kSeekDelayNanosec;
TaskManager *task_manager_;
GstStartup *gst_startup_;
@ -172,7 +172,6 @@ class GstEngine : public Engine::Base, public GstBufferConsumer {
std::shared_ptr<GstEnginePipeline> current_pipeline_;
std::shared_ptr<GstEnginePipeline> fadeout_pipeline_;
std::shared_ptr<GstEnginePipeline> fadeout_pause_pipeline_;
QUrl preloaded_url_;
QList<GstBufferConsumer*> buffer_consumers_;
@ -202,7 +201,6 @@ class GstEngine : public Engine::Base, public GstBufferConsumer {
int discovery_finished_cb_id_;
int discovery_discovered_cb_id_;
};
#endif // GSTENGINE_H

View File

@ -123,7 +123,8 @@ GstEnginePipeline::GstEnginePipeline(QObject *parent)
notify_source_cb_id_(-1),
about_to_finish_cb_id_(-1),
notify_volume_cb_id_(-1),
logged_unsupported_analyzer_format_(false) {
logged_unsupported_analyzer_format_(false),
about_to_finish_(false) {
eq_band_gains_.reserve(kEqBandCount);
for (int i = 0; i < kEqBandCount; ++i) eq_band_gains_ << 0;
@ -1102,13 +1103,16 @@ void GstEnginePipeline::AboutToFinishCallback(GstPlayBin *playbin, gpointer self
GstEnginePipeline *instance = reinterpret_cast<GstEnginePipeline*>(self);
qLog(Debug) << "Stream from URL" << instance->gst_url_ << "about to finish.";
instance->about_to_finish_ = true;
if (instance->has_next_valid_url() && !instance->next_uri_set_) {
// Set the next uri. When the current song ends it will be played automatically and a STREAM_START message is send to the bus.
// When the next uri is not playable an error message is send when the pipeline goes to PLAY (or PAUSE) state or immediately if it is currently in PLAY state.
instance->next_uri_set_ = true;
g_object_set(G_OBJECT(instance->pipeline_), "uri", instance->next_gst_url_.constData(), nullptr);
instance->SetNextUrl();
}
emit instance->AboutToFinish();
}
GstBusSyncReply GstEnginePipeline::BusSyncCallback(GstBus *bus, GstMessage *msg, gpointer self) {
@ -1204,8 +1208,9 @@ void GstEnginePipeline::StreamStatusMessageReceived(GstMessage *msg) {
void GstEnginePipeline::StreamStartMessageReceived() {
if (next_uri_set_) {
qLog(Debug) << "Stream changed from URL" << gst_url_ << "to" << next_gst_url_;
next_uri_set_ = false;
about_to_finish_ = false;
media_url_ = next_media_url_;
stream_url_ = next_stream_url_;
gst_url_ = next_gst_url_;
@ -1654,7 +1659,7 @@ void GstEnginePipeline::RemoveAllBufferConsumers() {
buffer_consumers_.clear();
}
void GstEnginePipeline::SetNextUrl(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 beginning_nanosec, const qint64 end_nanosec) {
void GstEnginePipeline::PrepareNextUrl(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 beginning_nanosec, const qint64 end_nanosec) {
next_media_url_ = media_url;
next_stream_url_ = stream_url;
@ -1662,4 +1667,21 @@ void GstEnginePipeline::SetNextUrl(const QUrl &media_url, const QUrl &stream_url
next_beginning_offset_nanosec_ = beginning_nanosec;
next_end_offset_nanosec_ = end_nanosec;
if (about_to_finish_) {
SetNextUrl();
}
}
void GstEnginePipeline::SetNextUrl() {
if (about_to_finish_ && has_next_valid_url() && !next_uri_set_) {
// Set the next uri. When the current song ends it will be played automatically and a STREAM_START message is send to the bus.
// When the next uri is not playable an error message is send when the pipeline goes to PLAY (or PAUSE) state or immediately if it is currently in PLAY state.
next_uri_set_ = true;
qLog(Debug) << "Setting next URL to" << next_gst_url_;
g_object_set(G_OBJECT(pipeline_), "uri", next_gst_url_.constData(), nullptr);
about_to_finish_ = false;
}
}

View File

@ -95,7 +95,8 @@ class GstEnginePipeline : public QObject {
void StartFader(const qint64 duration_nanosec, const QTimeLine::Direction direction = QTimeLine::Forward, const QEasingCurve::Type shape = QEasingCurve::Linear, const bool use_fudge_timer = true);
// If this is set then it will be loaded automatically when playback finishes for gapless playback
void SetNextUrl(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 beginning_nanosec, const qint64 end_nanosec);
void PrepareNextUrl(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 beginning_nanosec, const qint64 end_nanosec);
void SetNextUrl();
bool has_next_valid_url() const { return next_stream_url_.isValid(); }
void SetSourceDevice(const QString &device) { source_device_ = device; }
@ -140,6 +141,8 @@ class GstEnginePipeline : public QObject {
void BufferingProgress(const int percent);
void BufferingFinished();
void AboutToFinish();
protected:
void timerEvent(QTimerEvent*) override;
@ -317,6 +320,8 @@ class GstEnginePipeline : public QObject {
bool logged_unsupported_analyzer_format_;
bool about_to_finish_;
};
#endif // GSTENGINEPIPELINE_H