mirror of
https://github.com/clementine-player/Clementine
synced 2025-01-19 04:50:16 +01:00
If we're not crossfading, keep the same pipeline when changing tracks and just swap out the gstreamer source - this should allow for completely gapless playback.
This commit is contained in:
parent
8ea3213f09
commit
980d61a583
@ -350,16 +350,25 @@ void GstEngine::UpdateScope() {
|
||||
}
|
||||
|
||||
void GstEngine::StartPreloading(const QUrl& url) {
|
||||
preload_pipeline_ = CreatePipeline(url);
|
||||
if (!preload_pipeline_)
|
||||
return;
|
||||
if (autocrossfade_enabled_) {
|
||||
// Have to create a new pipeline so we can crossfade between the two
|
||||
|
||||
// We don't want to get metadata messages before the track starts playing -
|
||||
// we reconnect this in GstEngine::Load
|
||||
disconnect(preload_pipeline_.get(), SIGNAL(MetadataFound(Engine::SimpleMetaBundle)), this, 0);
|
||||
preload_pipeline_ = CreatePipeline(url);
|
||||
if (!preload_pipeline_)
|
||||
return;
|
||||
|
||||
preloaded_url_ = url;
|
||||
preload_pipeline_->SetState(GST_STATE_PAUSED);
|
||||
// We don't want to get metadata messages before the track starts playing -
|
||||
// we reconnect this in GstEngine::Load
|
||||
disconnect(preload_pipeline_.get(), SIGNAL(MetadataFound(Engine::SimpleMetaBundle)), this, 0);
|
||||
|
||||
preloaded_url_ = url;
|
||||
preload_pipeline_->SetState(GST_STATE_PAUSED);
|
||||
} else {
|
||||
// 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(url);
|
||||
}
|
||||
}
|
||||
|
||||
bool GstEngine::Load(const QUrl& url, Engine::TrackChangeType change) {
|
||||
@ -375,6 +384,12 @@ bool GstEngine::Load(const QUrl& url, Engine::TrackChangeType change) {
|
||||
((crossfade_enabled_ && change == Engine::Manual) ||
|
||||
(autocrossfade_enabled_ && change == Engine::Auto));
|
||||
|
||||
if (!crossfade && current_pipeline_ && current_pipeline_->url() == url) {
|
||||
// We're not crossfading, and the pipeline is already playing the URI we
|
||||
// want, so just do nothing.
|
||||
return true;
|
||||
}
|
||||
|
||||
shared_ptr<GstEnginePipeline> pipeline;
|
||||
if (preload_pipeline_ && preloaded_url_ == url) {
|
||||
pipeline = preload_pipeline_;
|
||||
@ -545,8 +560,10 @@ void GstEngine::HandlePipelineError(const QString& message) {
|
||||
}
|
||||
|
||||
|
||||
void GstEngine::EndOfStreamReached() {
|
||||
current_pipeline_.reset();
|
||||
void GstEngine::EndOfStreamReached(bool has_next_track) {
|
||||
if (!has_next_track)
|
||||
current_pipeline_.reset();
|
||||
ClearScopeBuffers();
|
||||
emit TrackEnded();
|
||||
}
|
||||
|
||||
@ -603,7 +620,7 @@ shared_ptr<GstEnginePipeline> GstEngine::CreatePipeline(const QUrl& url) {
|
||||
ret->set_forwards_buffers(true);
|
||||
ret->set_output_device(sink_, device_);
|
||||
|
||||
connect(ret.get(), SIGNAL(EndOfStreamReached()), SLOT(EndOfStreamReached()));
|
||||
connect(ret.get(), SIGNAL(EndOfStreamReached(bool)), SLOT(EndOfStreamReached(bool)));
|
||||
connect(ret.get(), SIGNAL(BufferFound(GstBuffer*)), SLOT(AddBufferToScope(GstBuffer*)));
|
||||
connect(ret.get(), SIGNAL(Error(QString)), SLOT(HandlePipelineError(QString)));
|
||||
connect(ret.get(), SIGNAL(MetadataFound(Engine::SimpleMetaBundle)),
|
||||
|
@ -97,7 +97,7 @@ class GstEngine : public Engine::Base {
|
||||
void timerEvent(QTimerEvent*);
|
||||
|
||||
private slots:
|
||||
void EndOfStreamReached();
|
||||
void EndOfStreamReached(bool has_next_track);
|
||||
void HandlePipelineError(const QString& message);
|
||||
void NewMetaData(const Engine::SimpleMetaBundle& bundle);
|
||||
void AddBufferToScope(GstBuffer* buf);
|
||||
|
@ -36,8 +36,7 @@ GstEnginePipeline::GstEnginePipeline(GstEngine* engine)
|
||||
equalizer_(NULL),
|
||||
volume_(NULL),
|
||||
audioscale_(NULL),
|
||||
audiosink_(NULL),
|
||||
event_cb_id_(0)
|
||||
audiosink_(NULL)
|
||||
{
|
||||
}
|
||||
|
||||
@ -46,8 +45,36 @@ void GstEnginePipeline::set_output_device(const QString &sink, const QString &de
|
||||
device_ = device;
|
||||
}
|
||||
|
||||
bool GstEnginePipeline::StopUriDecodeBin(gpointer bin) {
|
||||
gst_element_set_state(GST_ELEMENT(bin), GST_STATE_NULL);
|
||||
return false; // So it doesn't get called again
|
||||
}
|
||||
|
||||
bool GstEnginePipeline::ReplaceDecodeBin(const QUrl& url) {
|
||||
GstElement* new_bin = engine_->CreateElement("uridecodebin");
|
||||
if (!new_bin) return false;
|
||||
|
||||
// Destroy the old one, if any
|
||||
if (uridecodebin_) {
|
||||
gst_bin_remove(GST_BIN(pipeline_), uridecodebin_);
|
||||
|
||||
// Set its state to NULL later in the main thread
|
||||
g_idle_add(GSourceFunc(StopUriDecodeBin), uridecodebin_);
|
||||
}
|
||||
|
||||
uridecodebin_ = new_bin;
|
||||
gst_bin_add(GST_BIN(pipeline_), uridecodebin_);
|
||||
|
||||
g_object_set(G_OBJECT(uridecodebin_), "uri", url.toEncoded().constData(), NULL);
|
||||
g_signal_connect(G_OBJECT(uridecodebin_), "pad-added", G_CALLBACK(NewPadCallback), this);
|
||||
g_signal_connect(G_OBJECT(uridecodebin_), "drained", G_CALLBACK(SourceDrainedCallback), this);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GstEnginePipeline::Init(const QUrl &url) {
|
||||
pipeline_ = gst_pipeline_new("pipeline");
|
||||
url_ = url;
|
||||
|
||||
// Here we create all the parts of the gstreamer pipeline - from the source
|
||||
// to the sink. The parts of the pipeline are split up into bins:
|
||||
@ -59,16 +86,7 @@ bool GstEnginePipeline::Init(const QUrl &url) {
|
||||
// audiosink
|
||||
|
||||
// Decode bin
|
||||
if (!(uridecodebin_ = engine_->CreateElement("uridecodebin", pipeline_))) { return false; }
|
||||
g_object_set(G_OBJECT(uridecodebin_), "uri", url.toEncoded().constData(), NULL);
|
||||
g_signal_connect(G_OBJECT(uridecodebin_), "pad-added", G_CALLBACK(NewPadCallback), this);
|
||||
|
||||
// Does some stuff with ghost pads
|
||||
/*GstPad* pad = gst_element_get_pad(uridecodebin_, "sink");
|
||||
if (pad) {
|
||||
event_cb_id_ = gst_pad_add_event_probe (pad, G_CALLBACK(EventCallback), this);
|
||||
gst_object_unref(pad);
|
||||
}*/
|
||||
if (!ReplaceDecodeBin(url)) return false;
|
||||
|
||||
// Audio bin
|
||||
audiobin_ = gst_bin_new("audiobin");
|
||||
@ -119,13 +137,6 @@ bool GstEnginePipeline::Init(const QUrl &url) {
|
||||
}
|
||||
|
||||
GstEnginePipeline::~GstEnginePipeline() {
|
||||
// We don't want an EOS signal from the decodebin
|
||||
if (uridecodebin_) {
|
||||
GstPad *p = gst_element_get_pad(uridecodebin_, "sink");
|
||||
if (p)
|
||||
gst_pad_remove_event_probe(p, event_cb_id_);
|
||||
}
|
||||
|
||||
if (pipeline_) {
|
||||
gst_bus_set_sync_handler(gst_pipeline_get_bus(GST_PIPELINE(pipeline_)), NULL, NULL);
|
||||
g_source_remove(bus_cb_id_);
|
||||
@ -158,7 +169,7 @@ GstBusSyncReply GstEnginePipeline::BusCallbackSync(GstBus*, GstMessage* msg, gpo
|
||||
GstEnginePipeline* instance = reinterpret_cast<GstEnginePipeline*>(self);
|
||||
switch (GST_MESSAGE_TYPE(msg)) {
|
||||
case GST_MESSAGE_EOS:
|
||||
emit instance->EndOfStreamReached();
|
||||
emit instance->EndOfStreamReached(false);
|
||||
break;
|
||||
|
||||
case GST_MESSAGE_TAG:
|
||||
@ -249,7 +260,7 @@ void GstEnginePipeline::EventCallback(GstPad*, GstEvent* event, gpointer self) {
|
||||
|
||||
switch(event->type) {
|
||||
case GST_EVENT_EOS:
|
||||
emit instance->EndOfStreamReached();
|
||||
emit instance->EndOfStreamReached(false);
|
||||
break;
|
||||
|
||||
default:
|
||||
@ -257,6 +268,21 @@ void GstEnginePipeline::EventCallback(GstPad*, GstEvent* event, gpointer self) {
|
||||
}
|
||||
}
|
||||
|
||||
void GstEnginePipeline::SourceDrainedCallback(GstURIDecodeBin* bin, gpointer self) {
|
||||
GstEnginePipeline* instance = reinterpret_cast<GstEnginePipeline*>(self);
|
||||
|
||||
if (instance->next_url_.isValid()) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
qint64 GstEnginePipeline::position() const {
|
||||
GstFormat fmt = GST_FORMAT_TIME;
|
||||
gint64 value = 0;
|
||||
|
@ -27,6 +27,8 @@
|
||||
|
||||
class GstEngine;
|
||||
|
||||
struct GstURIDecodeBin;
|
||||
|
||||
class GstEnginePipeline : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
@ -51,7 +53,12 @@ class GstEnginePipeline : public QObject {
|
||||
QTimeLine::Direction direction = QTimeLine::Forward,
|
||||
QTimeLine::CurveShape shape = QTimeLine::LinearCurve);
|
||||
|
||||
// If this is set then it will be loaded automatically when playback finishes
|
||||
// for gapless playback
|
||||
void SetNextUrl(const QUrl& url) { next_url_ = url; }
|
||||
|
||||
// Get information about the music playback
|
||||
QUrl url() const { return url_; }
|
||||
bool is_valid() const { return valid_; }
|
||||
qint64 position() const;
|
||||
qint64 length() const;
|
||||
@ -61,7 +68,7 @@ class GstEnginePipeline : public QObject {
|
||||
void SetVolumeModifier(qreal mod);
|
||||
|
||||
signals:
|
||||
void EndOfStreamReached();
|
||||
void EndOfStreamReached(bool has_next_track);
|
||||
void MetadataFound(const Engine::SimpleMetaBundle& bundle);
|
||||
void BufferFound(GstBuffer* buf);
|
||||
void Error(const QString& message);
|
||||
@ -75,11 +82,14 @@ class GstEnginePipeline : public QObject {
|
||||
static void NewPadCallback(GstElement*, GstPad*, gpointer);
|
||||
static bool HandoffCallback(GstPad*, GstBuffer*, gpointer);
|
||||
static void EventCallback(GstPad*, GstEvent*, gpointer);
|
||||
static void SourceDrainedCallback(GstURIDecodeBin*, gpointer);
|
||||
static bool StopUriDecodeBin(gpointer bin);
|
||||
void TagMessageReceived(GstMessage*);
|
||||
QString ParseTag(GstTagList* list, const char* tag) const;
|
||||
void ErrorMessageReceived(GstMessage*);
|
||||
|
||||
void UpdateVolume();
|
||||
bool ReplaceDecodeBin(const QUrl& url);
|
||||
|
||||
private:
|
||||
static const int kGstStateTimeoutNanosecs = 10000000;
|
||||
@ -91,6 +101,9 @@ class GstEnginePipeline : public QObject {
|
||||
QString device_;
|
||||
bool forwards_buffers_;
|
||||
|
||||
QUrl url_;
|
||||
QUrl next_url_;
|
||||
|
||||
int volume_percent_;
|
||||
qreal volume_modifier_;
|
||||
|
||||
@ -111,7 +124,6 @@ class GstEnginePipeline : public QObject {
|
||||
GstElement* audioscale_;
|
||||
GstElement* audiosink_;
|
||||
|
||||
uint event_cb_id_;
|
||||
uint bus_cb_id_;
|
||||
};
|
||||
|
||||
|
@ -156,7 +156,7 @@ void Player::NextInternal(Engine::TrackChangeType change) {
|
||||
return;
|
||||
}
|
||||
|
||||
NextItem(Engine::Manual);
|
||||
NextItem(change);
|
||||
}
|
||||
|
||||
void Player::NextItem(Engine::TrackChangeType change) {
|
||||
|
Loading…
Reference in New Issue
Block a user