mirror of
https://github.com/clementine-player/Clementine
synced 2025-01-31 19:45:31 +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) {
|
void GstEngine::StartPreloading(const QUrl& url) {
|
||||||
preload_pipeline_ = CreatePipeline(url);
|
if (autocrossfade_enabled_) {
|
||||||
if (!preload_pipeline_)
|
// Have to create a new pipeline so we can crossfade between the two
|
||||||
return;
|
|
||||||
|
|
||||||
// We don't want to get metadata messages before the track starts playing -
|
preload_pipeline_ = CreatePipeline(url);
|
||||||
// we reconnect this in GstEngine::Load
|
if (!preload_pipeline_)
|
||||||
disconnect(preload_pipeline_.get(), SIGNAL(MetadataFound(Engine::SimpleMetaBundle)), this, 0);
|
return;
|
||||||
|
|
||||||
preloaded_url_ = url;
|
// We don't want to get metadata messages before the track starts playing -
|
||||||
preload_pipeline_->SetState(GST_STATE_PAUSED);
|
// 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) {
|
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) ||
|
((crossfade_enabled_ && change == Engine::Manual) ||
|
||||||
(autocrossfade_enabled_ && change == Engine::Auto));
|
(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;
|
shared_ptr<GstEnginePipeline> pipeline;
|
||||||
if (preload_pipeline_ && preloaded_url_ == url) {
|
if (preload_pipeline_ && preloaded_url_ == url) {
|
||||||
pipeline = preload_pipeline_;
|
pipeline = preload_pipeline_;
|
||||||
@ -545,8 +560,10 @@ void GstEngine::HandlePipelineError(const QString& message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void GstEngine::EndOfStreamReached() {
|
void GstEngine::EndOfStreamReached(bool has_next_track) {
|
||||||
current_pipeline_.reset();
|
if (!has_next_track)
|
||||||
|
current_pipeline_.reset();
|
||||||
|
ClearScopeBuffers();
|
||||||
emit TrackEnded();
|
emit TrackEnded();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -603,7 +620,7 @@ shared_ptr<GstEnginePipeline> GstEngine::CreatePipeline(const QUrl& url) {
|
|||||||
ret->set_forwards_buffers(true);
|
ret->set_forwards_buffers(true);
|
||||||
ret->set_output_device(sink_, device_);
|
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(BufferFound(GstBuffer*)), SLOT(AddBufferToScope(GstBuffer*)));
|
||||||
connect(ret.get(), SIGNAL(Error(QString)), SLOT(HandlePipelineError(QString)));
|
connect(ret.get(), SIGNAL(Error(QString)), SLOT(HandlePipelineError(QString)));
|
||||||
connect(ret.get(), SIGNAL(MetadataFound(Engine::SimpleMetaBundle)),
|
connect(ret.get(), SIGNAL(MetadataFound(Engine::SimpleMetaBundle)),
|
||||||
|
@ -97,7 +97,7 @@ class GstEngine : public Engine::Base {
|
|||||||
void timerEvent(QTimerEvent*);
|
void timerEvent(QTimerEvent*);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void EndOfStreamReached();
|
void EndOfStreamReached(bool has_next_track);
|
||||||
void HandlePipelineError(const QString& message);
|
void HandlePipelineError(const QString& message);
|
||||||
void NewMetaData(const Engine::SimpleMetaBundle& bundle);
|
void NewMetaData(const Engine::SimpleMetaBundle& bundle);
|
||||||
void AddBufferToScope(GstBuffer* buf);
|
void AddBufferToScope(GstBuffer* buf);
|
||||||
|
@ -36,8 +36,7 @@ GstEnginePipeline::GstEnginePipeline(GstEngine* engine)
|
|||||||
equalizer_(NULL),
|
equalizer_(NULL),
|
||||||
volume_(NULL),
|
volume_(NULL),
|
||||||
audioscale_(NULL),
|
audioscale_(NULL),
|
||||||
audiosink_(NULL),
|
audiosink_(NULL)
|
||||||
event_cb_id_(0)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,8 +45,36 @@ void GstEnginePipeline::set_output_device(const QString &sink, const QString &de
|
|||||||
device_ = device;
|
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) {
|
bool GstEnginePipeline::Init(const QUrl &url) {
|
||||||
pipeline_ = gst_pipeline_new("pipeline");
|
pipeline_ = gst_pipeline_new("pipeline");
|
||||||
|
url_ = url;
|
||||||
|
|
||||||
// Here we create all the parts of the gstreamer pipeline - from the source
|
// 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:
|
// to the sink. The parts of the pipeline are split up into bins:
|
||||||
@ -59,16 +86,7 @@ bool GstEnginePipeline::Init(const QUrl &url) {
|
|||||||
// audiosink
|
// audiosink
|
||||||
|
|
||||||
// Decode bin
|
// Decode bin
|
||||||
if (!(uridecodebin_ = engine_->CreateElement("uridecodebin", pipeline_))) { return false; }
|
if (!ReplaceDecodeBin(url)) 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);
|
|
||||||
}*/
|
|
||||||
|
|
||||||
// Audio bin
|
// Audio bin
|
||||||
audiobin_ = gst_bin_new("audiobin");
|
audiobin_ = gst_bin_new("audiobin");
|
||||||
@ -119,13 +137,6 @@ bool GstEnginePipeline::Init(const QUrl &url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
GstEnginePipeline::~GstEnginePipeline() {
|
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_) {
|
if (pipeline_) {
|
||||||
gst_bus_set_sync_handler(gst_pipeline_get_bus(GST_PIPELINE(pipeline_)), NULL, NULL);
|
gst_bus_set_sync_handler(gst_pipeline_get_bus(GST_PIPELINE(pipeline_)), NULL, NULL);
|
||||||
g_source_remove(bus_cb_id_);
|
g_source_remove(bus_cb_id_);
|
||||||
@ -158,7 +169,7 @@ GstBusSyncReply GstEnginePipeline::BusCallbackSync(GstBus*, GstMessage* msg, gpo
|
|||||||
GstEnginePipeline* instance = reinterpret_cast<GstEnginePipeline*>(self);
|
GstEnginePipeline* instance = reinterpret_cast<GstEnginePipeline*>(self);
|
||||||
switch (GST_MESSAGE_TYPE(msg)) {
|
switch (GST_MESSAGE_TYPE(msg)) {
|
||||||
case GST_MESSAGE_EOS:
|
case GST_MESSAGE_EOS:
|
||||||
emit instance->EndOfStreamReached();
|
emit instance->EndOfStreamReached(false);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case GST_MESSAGE_TAG:
|
case GST_MESSAGE_TAG:
|
||||||
@ -249,7 +260,7 @@ void GstEnginePipeline::EventCallback(GstPad*, GstEvent* event, gpointer self) {
|
|||||||
|
|
||||||
switch(event->type) {
|
switch(event->type) {
|
||||||
case GST_EVENT_EOS:
|
case GST_EVENT_EOS:
|
||||||
emit instance->EndOfStreamReached();
|
emit instance->EndOfStreamReached(false);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
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 {
|
qint64 GstEnginePipeline::position() const {
|
||||||
GstFormat fmt = GST_FORMAT_TIME;
|
GstFormat fmt = GST_FORMAT_TIME;
|
||||||
gint64 value = 0;
|
gint64 value = 0;
|
||||||
|
@ -27,6 +27,8 @@
|
|||||||
|
|
||||||
class GstEngine;
|
class GstEngine;
|
||||||
|
|
||||||
|
struct GstURIDecodeBin;
|
||||||
|
|
||||||
class GstEnginePipeline : public QObject {
|
class GstEnginePipeline : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
@ -51,7 +53,12 @@ class GstEnginePipeline : public QObject {
|
|||||||
QTimeLine::Direction direction = QTimeLine::Forward,
|
QTimeLine::Direction direction = QTimeLine::Forward,
|
||||||
QTimeLine::CurveShape shape = QTimeLine::LinearCurve);
|
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
|
// Get information about the music playback
|
||||||
|
QUrl url() const { return url_; }
|
||||||
bool is_valid() const { return valid_; }
|
bool is_valid() const { return valid_; }
|
||||||
qint64 position() const;
|
qint64 position() const;
|
||||||
qint64 length() const;
|
qint64 length() const;
|
||||||
@ -61,7 +68,7 @@ class GstEnginePipeline : public QObject {
|
|||||||
void SetVolumeModifier(qreal mod);
|
void SetVolumeModifier(qreal mod);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void EndOfStreamReached();
|
void EndOfStreamReached(bool has_next_track);
|
||||||
void MetadataFound(const Engine::SimpleMetaBundle& bundle);
|
void MetadataFound(const Engine::SimpleMetaBundle& bundle);
|
||||||
void BufferFound(GstBuffer* buf);
|
void BufferFound(GstBuffer* buf);
|
||||||
void Error(const QString& message);
|
void Error(const QString& message);
|
||||||
@ -75,11 +82,14 @@ class GstEnginePipeline : public QObject {
|
|||||||
static void NewPadCallback(GstElement*, GstPad*, gpointer);
|
static void NewPadCallback(GstElement*, GstPad*, gpointer);
|
||||||
static bool HandoffCallback(GstPad*, GstBuffer*, gpointer);
|
static bool HandoffCallback(GstPad*, GstBuffer*, gpointer);
|
||||||
static void EventCallback(GstPad*, GstEvent*, gpointer);
|
static void EventCallback(GstPad*, GstEvent*, gpointer);
|
||||||
|
static void SourceDrainedCallback(GstURIDecodeBin*, gpointer);
|
||||||
|
static bool StopUriDecodeBin(gpointer bin);
|
||||||
void TagMessageReceived(GstMessage*);
|
void TagMessageReceived(GstMessage*);
|
||||||
QString ParseTag(GstTagList* list, const char* tag) const;
|
QString ParseTag(GstTagList* list, const char* tag) const;
|
||||||
void ErrorMessageReceived(GstMessage*);
|
void ErrorMessageReceived(GstMessage*);
|
||||||
|
|
||||||
void UpdateVolume();
|
void UpdateVolume();
|
||||||
|
bool ReplaceDecodeBin(const QUrl& url);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static const int kGstStateTimeoutNanosecs = 10000000;
|
static const int kGstStateTimeoutNanosecs = 10000000;
|
||||||
@ -91,6 +101,9 @@ class GstEnginePipeline : public QObject {
|
|||||||
QString device_;
|
QString device_;
|
||||||
bool forwards_buffers_;
|
bool forwards_buffers_;
|
||||||
|
|
||||||
|
QUrl url_;
|
||||||
|
QUrl next_url_;
|
||||||
|
|
||||||
int volume_percent_;
|
int volume_percent_;
|
||||||
qreal volume_modifier_;
|
qreal volume_modifier_;
|
||||||
|
|
||||||
@ -111,7 +124,6 @@ class GstEnginePipeline : public QObject {
|
|||||||
GstElement* audioscale_;
|
GstElement* audioscale_;
|
||||||
GstElement* audiosink_;
|
GstElement* audiosink_;
|
||||||
|
|
||||||
uint event_cb_id_;
|
|
||||||
uint bus_cb_id_;
|
uint bus_cb_id_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -156,7 +156,7 @@ void Player::NextInternal(Engine::TrackChangeType change) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
NextItem(Engine::Manual);
|
NextItem(change);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::NextItem(Engine::TrackChangeType change) {
|
void Player::NextItem(Engine::TrackChangeType change) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user