diff --git a/src/core/player.cpp b/src/core/player.cpp index ae7c7bfd2..cee7ff38e 100644 --- a/src/core/player.cpp +++ b/src/core/player.cpp @@ -437,6 +437,7 @@ void Player::TrackAboutToEnd() { break; } } - engine_->StartPreloading(url); + engine_->StartPreloading(url, next_item_->Metadata().beginning_nanosec(), + next_item_->Metadata().end_nanosec()); } } diff --git a/src/engines/enginebase.h b/src/engines/enginebase.h index 9ce5421d7..d264e632f 100644 --- a/src/engines/enginebase.h +++ b/src/engines/enginebase.h @@ -45,7 +45,7 @@ class Base : public QObject, boost::noncopyable { virtual bool Init() = 0; virtual bool CanDecode(const QUrl &url) = 0; - virtual void StartPreloading(const QUrl&) {} + virtual void StartPreloading(const QUrl&, qint64, qint64) {} virtual bool Play(quint64 offset_nanosec) = 0; virtual void Stop() = 0; virtual void Pause() = 0; diff --git a/src/engines/gstengine.cpp b/src/engines/gstengine.cpp index 37f4ecf1c..7b0359a3e 100644 --- a/src/engines/gstengine.cpp +++ b/src/engines/gstengine.cpp @@ -401,7 +401,8 @@ void GstEngine::UpdateScope() { } } -void GstEngine::StartPreloading(const QUrl& url) { +void GstEngine::StartPreloading(const QUrl& url, qint64 beginning_nanosec, + qint64 end_nanosec) { EnsureInitialised(); QUrl gst_url = FixupUrl(url); @@ -409,7 +410,7 @@ void GstEngine::StartPreloading(const QUrl& url) { if (autocrossfade_enabled_) { // Have to create a new pipeline so we can crossfade between the two - preload_pipeline_ = CreatePipeline(gst_url); + preload_pipeline_ = CreatePipeline(gst_url, end_nanosec); if (!preload_pipeline_) return; @@ -423,7 +424,7 @@ void GstEngine::StartPreloading(const QUrl& url) { // 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(gst_url); + current_pipeline_->SetNextUrl(gst_url, beginning_nanosec, end_nanosec); } } @@ -473,7 +474,7 @@ bool GstEngine::Load(const QUrl& url, Engine::TrackChangeType change, SIGNAL(MetadataFound(Engine::SimpleMetaBundle)), SLOT(NewMetaData(Engine::SimpleMetaBundle))); } else { - pipeline = CreatePipeline(gst_url); + pipeline = CreatePipeline(gst_url, end_nanosec); if (!pipeline) return false; } @@ -537,7 +538,7 @@ void GstEngine::PlayDone() { QUrl redirect_url = current_pipeline_->redirect_url(); if (!redirect_url.isEmpty() && redirect_url != current_pipeline_->url()) { qDebug() << "Redirecting to" << redirect_url; - current_pipeline_ = CreatePipeline(redirect_url); + current_pipeline_ = CreatePipeline(redirect_url, end_nanosec_); Play(offset_nanosec); return; } @@ -687,19 +688,6 @@ void GstEngine::timerEvent(QTimerEvent* e) { if (remaining < gap + fudge) { EmitAboutToEnd(); } - - // TODO: the code below stops my test CUE songs about two seconds too late now that - // we have nanoseconds; we should find a more clever way to implement this - // see issue #1233 - - // when at the end, kill the track if it didn't stop yet (probably a - // multisection media file). We add 1 second onto the length during this - // check to allow for the fact that the length has been rounded down to - // the nearest second, and to stop us from occasionally stopping the - // stream just before it ends normally. - if(current_position >= current_length + 1000 * kNsecPerMsec) { - EndOfStreamReached(current_pipeline_->has_next_valid_url()); - } } } } @@ -714,6 +702,10 @@ void GstEngine::HandlePipelineError(const QString& message) { void GstEngine::EndOfStreamReached(bool has_next_track) { + GstEnginePipeline* pipeline_sender = qobject_cast(sender()); + if (!pipeline_sender || pipeline_sender != current_pipeline_.get()) + return; + if (!has_next_track) current_pipeline_.reset(); ClearScopeBuffers(); @@ -794,7 +786,8 @@ shared_ptr GstEngine::CreatePipeline() { return ret; } -shared_ptr GstEngine::CreatePipeline(const QUrl& url) { +shared_ptr GstEngine::CreatePipeline(const QUrl& url, + qint64 end_nanosec) { shared_ptr ret = CreatePipeline(); if (url.scheme() == "hypnotoad") { @@ -802,7 +795,7 @@ shared_ptr GstEngine::CreatePipeline(const QUrl& url) { return ret; } - if (!ret->InitFromUrl(url)) + if (!ret->InitFromUrl(url, end_nanosec)) ret.reset(); return ret; @@ -899,12 +892,12 @@ void GstEngine::BackgroundStreamPlayDone() { } int GstEngine::AddBackgroundStream(const QUrl& url) { - shared_ptr pipeline = CreatePipeline(url); + shared_ptr pipeline = CreatePipeline(url, 0); if (!pipeline) { return -1; } pipeline->SetVolume(30); - pipeline->SetNextUrl(url); + pipeline->SetNextUrl(url, 0, 0); return AddBackgroundStream(pipeline); } @@ -914,7 +907,7 @@ void GstEngine::StopBackgroundStream(int id) { void GstEngine::BackgroundStreamFinished() { GstEnginePipeline* pipeline = qobject_cast(sender()); - pipeline->SetNextUrl(pipeline->url()); + pipeline->SetNextUrl(pipeline->url(), 0, 0); } void GstEngine::SetBackgroundStreamVolume(int id, int volume) { diff --git a/src/engines/gstengine.h b/src/engines/gstengine.h index 4a9ee1d0a..79a8ae91b 100644 --- a/src/engines/gstengine.h +++ b/src/engines/gstengine.h @@ -87,7 +87,8 @@ class GstEngine : public Engine::Base, public BufferConsumer { void ConsumeBuffer(GstBuffer *buffer, GstEnginePipeline* pipeline); public slots: - void StartPreloading(const QUrl &); + void StartPreloading(const QUrl& url, qint64 beginning_nanosec, + qint64 end_nanosec); bool Load(const QUrl&, Engine::TrackChangeType change, quint64 beginning_nanosec, qint64 end_nanosec); bool Play(quint64 offset_nanosec); @@ -141,7 +142,7 @@ class GstEngine : public Engine::Base, public BufferConsumer { void StopTimers(); boost::shared_ptr CreatePipeline(); - boost::shared_ptr CreatePipeline(const QUrl& url); + boost::shared_ptr CreatePipeline(const QUrl& url, qint64 end_nanosec); void UpdateScope(); qint64 PruneScope(); diff --git a/src/engines/gstenginepipeline.cpp b/src/engines/gstenginepipeline.cpp index 537a72df9..6aced2804 100644 --- a/src/engines/gstenginepipeline.cpp +++ b/src/engines/gstenginepipeline.cpp @@ -49,6 +49,9 @@ GstEnginePipeline::GstEnginePipeline(GstEngine* engine) rg_preamp_(0.0), rg_compression_(true), buffer_duration_nanosec_(1 * kNsecPerSec), + end_offset_nanosec_(-1), + next_beginning_offset_nanosec_(-1), + next_end_offset_nanosec_(-1), ignore_tags_(false), volume_percent_(100), volume_modifier_(1.0), @@ -237,10 +240,11 @@ bool GstEnginePipeline::InitFromString(const QString& pipeline) { return gst_element_link(new_bin, audiobin_); } -bool GstEnginePipeline::InitFromUrl(const QUrl &url) { +bool GstEnginePipeline::InitFromUrl(const QUrl &url, qint64 end_nanosec) { pipeline_ = gst_pipeline_new("pipeline"); url_ = url; + end_offset_nanosec_ = end_nanosec; // Decode bin if (!ReplaceDecodeBin(url)) return false; @@ -401,6 +405,40 @@ bool GstEnginePipeline::HandoffCallback(GstPad*, GstBuffer* buf, gpointer self) consumer->ConsumeBuffer(buf, instance); } + // Calculate the end time of this buffer so we can stop playback if it's + // after the end time of this song. + if (instance->end_offset_nanosec_ > 0) { + quint64 start_time = GST_BUFFER_TIMESTAMP(buf) - instance->segment_start_; + quint64 duration = GST_BUFFER_DURATION(buf); + quint64 end_time = start_time + duration; + + if (end_time > instance->end_offset_nanosec_) { + if (instance->has_next_valid_url()) { + if (instance->next_url_ == instance->url_ && + instance->next_beginning_offset_nanosec_ == instance->end_offset_nanosec_) { + // The "next" song is actually the next segment of this file - so + // cheat and keep on playing, but just tell the Engine we've moved on. + instance->end_offset_nanosec_ = instance->next_end_offset_nanosec_; + instance->next_url_ = QUrl(); + instance->next_beginning_offset_nanosec_ = 0; + instance->next_end_offset_nanosec_ = 0; + + // GstEngine will try to seek to the start of the new section, but + // we're already there so ignore it. + instance->ignore_next_seek_ = true; + + emit instance->EndOfStreamReached(true); + } else { + // We have a next song but we can't cheat, so move to it normally. + instance->TransitionToNext(); + } + } else { + // There's no next song + emit instance->EndOfStreamReached(false); + } + } + } + return true; } @@ -423,27 +461,34 @@ void GstEnginePipeline::SourceDrainedCallback(GstURIDecodeBin* bin, gpointer sel GstEnginePipeline* instance = reinterpret_cast(self); if (instance->has_next_valid_url()) { - GstElement* old_decode_bin = instance->uridecodebin_; - - instance->ignore_tags_ = true; - - instance->ReplaceDecodeBin(instance->next_url_); - gst_element_set_state(instance->uridecodebin_, GST_STATE_PLAYING); - - instance->url_ = instance->next_url_; - instance->next_url_ = QUrl(); - - // This just tells the UI that we've moved on to the next song - emit instance->EndOfStreamReached(true); - - // This has to happen *after* the gst_element_set_state on the new bin to - // fix an occasional race condition deadlock. - sElementDeleter->DeleteElementLater(old_decode_bin); - - instance->ignore_tags_ = false; + instance->TransitionToNext(); } } +void GstEnginePipeline::TransitionToNext() { + GstElement* old_decode_bin = uridecodebin_; + + ignore_tags_ = true; + + ReplaceDecodeBin(next_url_); + gst_element_set_state(uridecodebin_, GST_STATE_PLAYING); + + url_ = next_url_; + end_offset_nanosec_ = next_end_offset_nanosec_; + next_url_ = QUrl(); + next_beginning_offset_nanosec_ = 0; + next_end_offset_nanosec_ = 0; + + // This just tells the UI that we've moved on to the next song + emit EndOfStreamReached(true); + + // This has to happen *after* the gst_element_set_state on the new bin to + // fix an occasional race condition deadlock. + sElementDeleter->DeleteElementLater(old_decode_bin); + + ignore_tags_ = false; +} + qint64 GstEnginePipeline::position() const { GstFormat fmt = GST_FORMAT_TIME; gint64 value = 0; @@ -476,6 +521,11 @@ QFuture GstEnginePipeline::SetState(GstState state) { } bool GstEnginePipeline::Seek(qint64 nanosec) { + if (ignore_next_seek_) { + ignore_next_seek_ = false; + return true; + } + return gst_element_seek_simple(pipeline_, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH, nanosec); } @@ -586,3 +636,11 @@ void GstEnginePipeline::RemoveAllBufferConsumers() { QMutexLocker l(&buffer_consumers_mutex_); buffer_consumers_.clear(); } + +void GstEnginePipeline::SetNextUrl(const QUrl& url, + qint64 beginning_nanosec, + qint64 end_nanosec) { + next_url_ = url; + next_beginning_offset_nanosec_ = beginning_nanosec; + next_end_offset_nanosec_ = end_nanosec; +} diff --git a/src/engines/gstenginepipeline.h b/src/engines/gstenginepipeline.h index 0d00981ce..f6109f41c 100644 --- a/src/engines/gstenginepipeline.h +++ b/src/engines/gstenginepipeline.h @@ -49,7 +49,7 @@ class GstEnginePipeline : public QObject { void set_buffer_duration_nanosec(qint64 duration_nanosec); // Creates the pipeline, returns false on error - bool InitFromUrl(const QUrl& url); + bool InitFromUrl(const QUrl& url, qint64 end_nanosec); bool InitFromString(const QString& pipeline); // BufferConsumers get fed audio data. Thread-safe. @@ -69,7 +69,7 @@ class GstEnginePipeline : public QObject { // If this is set then it will be loaded automatically when playback finishes // for gapless playback - void SetNextUrl(const QUrl& url) { next_url_ = url; } + void SetNextUrl(const QUrl& url, qint64 beginning_nanosec, qint64 end_nanosec); bool has_next_valid_url() const { return next_url_.isValid(); } // Get information about the music playback @@ -120,6 +120,8 @@ class GstEnginePipeline : public QObject { bool ReplaceDecodeBin(GstElement* new_bin); bool ReplaceDecodeBin(const QUrl& url); + void TransitionToNext(); + private slots: void FaderTimelineFinished(); @@ -161,6 +163,20 @@ class GstEnginePipeline : public QObject { QUrl url_; QUrl next_url_; + // If this is > 0 then the pipeline will be forced to stop when playback goes + // past this position. + qint64 end_offset_nanosec_; + + // We store the beginning and end for the preloading song too, so we can just + // carry on without reloading the file if the sections carry on from each + // other. + qint64 next_beginning_offset_nanosec_; + qint64 next_end_offset_nanosec_; + + // Set temporarily when moving to the next contiguous section in a multi-part + // file. + bool ignore_next_seek_; + // Set temporarily when switching out the decode bin, so metadata doesn't // get sent while the Player still thinks it's playing the last song bool ignore_tags_;