Stop cue sections at exactly the right place, and move seamlessly between consecutive sections of the same file. Fixes issue #1233. Fixes issue #1419.

This commit is contained in:
David Sansome 2011-03-06 16:35:47 +00:00
parent b8a008aaeb
commit 2cb8b8dba7
6 changed files with 117 additions and 48 deletions

View File

@ -437,6 +437,7 @@ void Player::TrackAboutToEnd() {
break; break;
} }
} }
engine_->StartPreloading(url); engine_->StartPreloading(url, next_item_->Metadata().beginning_nanosec(),
next_item_->Metadata().end_nanosec());
} }
} }

View File

@ -45,7 +45,7 @@ class Base : public QObject, boost::noncopyable {
virtual bool Init() = 0; virtual bool Init() = 0;
virtual bool CanDecode(const QUrl &url) = 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 bool Play(quint64 offset_nanosec) = 0;
virtual void Stop() = 0; virtual void Stop() = 0;
virtual void Pause() = 0; virtual void Pause() = 0;

View File

@ -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(); EnsureInitialised();
QUrl gst_url = FixupUrl(url); QUrl gst_url = FixupUrl(url);
@ -409,7 +410,7 @@ void GstEngine::StartPreloading(const QUrl& url) {
if (autocrossfade_enabled_) { if (autocrossfade_enabled_) {
// Have to create a new pipeline so we can crossfade between the two // 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_) if (!preload_pipeline_)
return; return;
@ -423,7 +424,7 @@ void GstEngine::StartPreloading(const QUrl& url) {
// No crossfading, so we can just queue the new URL in the existing // No crossfading, so we can just queue the new URL in the existing
// pipeline and get gapless playback (hopefully) // pipeline and get gapless playback (hopefully)
if (current_pipeline_) 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)), SIGNAL(MetadataFound(Engine::SimpleMetaBundle)),
SLOT(NewMetaData(Engine::SimpleMetaBundle))); SLOT(NewMetaData(Engine::SimpleMetaBundle)));
} else { } else {
pipeline = CreatePipeline(gst_url); pipeline = CreatePipeline(gst_url, end_nanosec);
if (!pipeline) if (!pipeline)
return false; return false;
} }
@ -537,7 +538,7 @@ void GstEngine::PlayDone() {
QUrl redirect_url = current_pipeline_->redirect_url(); QUrl redirect_url = current_pipeline_->redirect_url();
if (!redirect_url.isEmpty() && redirect_url != current_pipeline_->url()) { if (!redirect_url.isEmpty() && redirect_url != current_pipeline_->url()) {
qDebug() << "Redirecting to" << redirect_url; qDebug() << "Redirecting to" << redirect_url;
current_pipeline_ = CreatePipeline(redirect_url); current_pipeline_ = CreatePipeline(redirect_url, end_nanosec_);
Play(offset_nanosec); Play(offset_nanosec);
return; return;
} }
@ -687,19 +688,6 @@ void GstEngine::timerEvent(QTimerEvent* e) {
if (remaining < gap + fudge) { if (remaining < gap + fudge) {
EmitAboutToEnd(); 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) { void GstEngine::EndOfStreamReached(bool has_next_track) {
GstEnginePipeline* pipeline_sender = qobject_cast<GstEnginePipeline*>(sender());
if (!pipeline_sender || pipeline_sender != current_pipeline_.get())
return;
if (!has_next_track) if (!has_next_track)
current_pipeline_.reset(); current_pipeline_.reset();
ClearScopeBuffers(); ClearScopeBuffers();
@ -794,7 +786,8 @@ shared_ptr<GstEnginePipeline> GstEngine::CreatePipeline() {
return ret; return ret;
} }
shared_ptr<GstEnginePipeline> GstEngine::CreatePipeline(const QUrl& url) { shared_ptr<GstEnginePipeline> GstEngine::CreatePipeline(const QUrl& url,
qint64 end_nanosec) {
shared_ptr<GstEnginePipeline> ret = CreatePipeline(); shared_ptr<GstEnginePipeline> ret = CreatePipeline();
if (url.scheme() == "hypnotoad") { if (url.scheme() == "hypnotoad") {
@ -802,7 +795,7 @@ shared_ptr<GstEnginePipeline> GstEngine::CreatePipeline(const QUrl& url) {
return ret; return ret;
} }
if (!ret->InitFromUrl(url)) if (!ret->InitFromUrl(url, end_nanosec))
ret.reset(); ret.reset();
return ret; return ret;
@ -899,12 +892,12 @@ void GstEngine::BackgroundStreamPlayDone() {
} }
int GstEngine::AddBackgroundStream(const QUrl& url) { int GstEngine::AddBackgroundStream(const QUrl& url) {
shared_ptr<GstEnginePipeline> pipeline = CreatePipeline(url); shared_ptr<GstEnginePipeline> pipeline = CreatePipeline(url, 0);
if (!pipeline) { if (!pipeline) {
return -1; return -1;
} }
pipeline->SetVolume(30); pipeline->SetVolume(30);
pipeline->SetNextUrl(url); pipeline->SetNextUrl(url, 0, 0);
return AddBackgroundStream(pipeline); return AddBackgroundStream(pipeline);
} }
@ -914,7 +907,7 @@ void GstEngine::StopBackgroundStream(int id) {
void GstEngine::BackgroundStreamFinished() { void GstEngine::BackgroundStreamFinished() {
GstEnginePipeline* pipeline = qobject_cast<GstEnginePipeline*>(sender()); GstEnginePipeline* pipeline = qobject_cast<GstEnginePipeline*>(sender());
pipeline->SetNextUrl(pipeline->url()); pipeline->SetNextUrl(pipeline->url(), 0, 0);
} }
void GstEngine::SetBackgroundStreamVolume(int id, int volume) { void GstEngine::SetBackgroundStreamVolume(int id, int volume) {

View File

@ -87,7 +87,8 @@ class GstEngine : public Engine::Base, public BufferConsumer {
void ConsumeBuffer(GstBuffer *buffer, GstEnginePipeline* pipeline); void ConsumeBuffer(GstBuffer *buffer, GstEnginePipeline* pipeline);
public slots: public slots:
void StartPreloading(const QUrl &); void StartPreloading(const QUrl& url, qint64 beginning_nanosec,
qint64 end_nanosec);
bool Load(const QUrl&, Engine::TrackChangeType change, bool Load(const QUrl&, Engine::TrackChangeType change,
quint64 beginning_nanosec, qint64 end_nanosec); quint64 beginning_nanosec, qint64 end_nanosec);
bool Play(quint64 offset_nanosec); bool Play(quint64 offset_nanosec);
@ -141,7 +142,7 @@ class GstEngine : public Engine::Base, public BufferConsumer {
void StopTimers(); void StopTimers();
boost::shared_ptr<GstEnginePipeline> CreatePipeline(); boost::shared_ptr<GstEnginePipeline> CreatePipeline();
boost::shared_ptr<GstEnginePipeline> CreatePipeline(const QUrl& url); boost::shared_ptr<GstEnginePipeline> CreatePipeline(const QUrl& url, qint64 end_nanosec);
void UpdateScope(); void UpdateScope();
qint64 PruneScope(); qint64 PruneScope();

View File

@ -49,6 +49,9 @@ GstEnginePipeline::GstEnginePipeline(GstEngine* engine)
rg_preamp_(0.0), rg_preamp_(0.0),
rg_compression_(true), rg_compression_(true),
buffer_duration_nanosec_(1 * kNsecPerSec), buffer_duration_nanosec_(1 * kNsecPerSec),
end_offset_nanosec_(-1),
next_beginning_offset_nanosec_(-1),
next_end_offset_nanosec_(-1),
ignore_tags_(false), ignore_tags_(false),
volume_percent_(100), volume_percent_(100),
volume_modifier_(1.0), volume_modifier_(1.0),
@ -237,10 +240,11 @@ bool GstEnginePipeline::InitFromString(const QString& pipeline) {
return gst_element_link(new_bin, audiobin_); 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"); pipeline_ = gst_pipeline_new("pipeline");
url_ = url; url_ = url;
end_offset_nanosec_ = end_nanosec;
// Decode bin // Decode bin
if (!ReplaceDecodeBin(url)) return false; if (!ReplaceDecodeBin(url)) return false;
@ -401,6 +405,40 @@ bool GstEnginePipeline::HandoffCallback(GstPad*, GstBuffer* buf, gpointer self)
consumer->ConsumeBuffer(buf, instance); 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; return true;
} }
@ -423,27 +461,34 @@ void GstEnginePipeline::SourceDrainedCallback(GstURIDecodeBin* bin, gpointer sel
GstEnginePipeline* instance = reinterpret_cast<GstEnginePipeline*>(self); GstEnginePipeline* instance = reinterpret_cast<GstEnginePipeline*>(self);
if (instance->has_next_valid_url()) { if (instance->has_next_valid_url()) {
GstElement* old_decode_bin = instance->uridecodebin_; instance->TransitionToNext();
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;
} }
} }
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 { qint64 GstEnginePipeline::position() const {
GstFormat fmt = GST_FORMAT_TIME; GstFormat fmt = GST_FORMAT_TIME;
gint64 value = 0; gint64 value = 0;
@ -476,6 +521,11 @@ QFuture<GstStateChangeReturn> GstEnginePipeline::SetState(GstState state) {
} }
bool GstEnginePipeline::Seek(qint64 nanosec) { bool GstEnginePipeline::Seek(qint64 nanosec) {
if (ignore_next_seek_) {
ignore_next_seek_ = false;
return true;
}
return gst_element_seek_simple(pipeline_, GST_FORMAT_TIME, return gst_element_seek_simple(pipeline_, GST_FORMAT_TIME,
GST_SEEK_FLAG_FLUSH, nanosec); GST_SEEK_FLAG_FLUSH, nanosec);
} }
@ -586,3 +636,11 @@ void GstEnginePipeline::RemoveAllBufferConsumers() {
QMutexLocker l(&buffer_consumers_mutex_); QMutexLocker l(&buffer_consumers_mutex_);
buffer_consumers_.clear(); 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;
}

View File

@ -49,7 +49,7 @@ class GstEnginePipeline : public QObject {
void set_buffer_duration_nanosec(qint64 duration_nanosec); void set_buffer_duration_nanosec(qint64 duration_nanosec);
// Creates the pipeline, returns false on error // 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); bool InitFromString(const QString& pipeline);
// BufferConsumers get fed audio data. Thread-safe. // 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 // If this is set then it will be loaded automatically when playback finishes
// for gapless playback // 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(); } bool has_next_valid_url() const { return next_url_.isValid(); }
// Get information about the music playback // Get information about the music playback
@ -120,6 +120,8 @@ class GstEnginePipeline : public QObject {
bool ReplaceDecodeBin(GstElement* new_bin); bool ReplaceDecodeBin(GstElement* new_bin);
bool ReplaceDecodeBin(const QUrl& url); bool ReplaceDecodeBin(const QUrl& url);
void TransitionToNext();
private slots: private slots:
void FaderTimelineFinished(); void FaderTimelineFinished();
@ -161,6 +163,20 @@ class GstEnginePipeline : public QObject {
QUrl url_; QUrl url_;
QUrl next_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 // 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 // get sent while the Player still thinks it's playing the last song
bool ignore_tags_; bool ignore_tags_;