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:
parent
b8a008aaeb
commit
2cb8b8dba7
|
@ -437,6 +437,7 @@ void Player::TrackAboutToEnd() {
|
|||
break;
|
||||
}
|
||||
}
|
||||
engine_->StartPreloading(url);
|
||||
engine_->StartPreloading(url, next_item_->Metadata().beginning_nanosec(),
|
||||
next_item_->Metadata().end_nanosec());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<GstEnginePipeline*>(sender());
|
||||
if (!pipeline_sender || pipeline_sender != current_pipeline_.get())
|
||||
return;
|
||||
|
||||
if (!has_next_track)
|
||||
current_pipeline_.reset();
|
||||
ClearScopeBuffers();
|
||||
|
@ -794,7 +786,8 @@ shared_ptr<GstEnginePipeline> GstEngine::CreatePipeline() {
|
|||
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();
|
||||
|
||||
if (url.scheme() == "hypnotoad") {
|
||||
|
@ -802,7 +795,7 @@ shared_ptr<GstEnginePipeline> 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<GstEnginePipeline> pipeline = CreatePipeline(url);
|
||||
shared_ptr<GstEnginePipeline> 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<GstEnginePipeline*>(sender());
|
||||
pipeline->SetNextUrl(pipeline->url());
|
||||
pipeline->SetNextUrl(pipeline->url(), 0, 0);
|
||||
}
|
||||
|
||||
void GstEngine::SetBackgroundStreamVolume(int id, int volume) {
|
||||
|
|
|
@ -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<GstEnginePipeline> CreatePipeline();
|
||||
boost::shared_ptr<GstEnginePipeline> CreatePipeline(const QUrl& url);
|
||||
boost::shared_ptr<GstEnginePipeline> CreatePipeline(const QUrl& url, qint64 end_nanosec);
|
||||
|
||||
void UpdateScope();
|
||||
qint64 PruneScope();
|
||||
|
|
|
@ -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<GstEnginePipeline*>(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<GstStateChangeReturn> 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;
|
||||
}
|
||||
|
|
|
@ -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_;
|
||||
|
|
Loading…
Reference in New Issue