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;
|
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 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;
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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_;
|
||||||
|
|
Loading…
Reference in New Issue