Improvements to gstreamer backend.

- Use PlayBin instead of URIDecodeBin
- Change QUrl to QByteArray in pipeline
- Move URL stuff to FixupUrl() in GstEngine
This commit is contained in:
Jonas Kvinge
2018-04-02 03:43:56 +02:00
parent 0891bb0128
commit 1817127a90
5 changed files with 203 additions and 269 deletions

View File

@@ -34,6 +34,7 @@
#include <QObject> #include <QObject>
#include <QUrl> #include <QUrl>
#include <QVariant> #include <QVariant>
#include <QByteArray>
#include "enginetype.h" #include "enginetype.h"
#include "engine_fwd.h" #include "engine_fwd.h"

View File

@@ -262,8 +262,7 @@ Engine::State GstEngine::state() const {
void GstEngine::ConsumeBuffer(GstBuffer *buffer, int pipeline_id) { void GstEngine::ConsumeBuffer(GstBuffer *buffer, int pipeline_id) {
// Schedule this to run in the GUI thread. The buffer gets added to the // Schedule this to run in the GUI thread. The buffer gets added to the queue and unreffed by UpdateScope.
// queue and unreffed by UpdateScope.
if (!QMetaObject::invokeMethod(this, "AddBufferToScope", Q_ARG(GstBuffer*, buffer), Q_ARG(int, pipeline_id))) { if (!QMetaObject::invokeMethod(this, "AddBufferToScope", Q_ARG(GstBuffer*, buffer), Q_ARG(int, pipeline_id))) {
qLog(Warning) << "Failed to invoke AddBufferToScope on GstEngine"; qLog(Warning) << "Failed to invoke AddBufferToScope on GstEngine";
} }
@@ -360,26 +359,47 @@ void GstEngine::StartPreloading(const QUrl &url, bool force_stop_at_end, qint64
EnsureInitialised(); EnsureInitialised();
QUrl gst_url = FixupUrl(url); QByteArray gst_url = FixupUrl(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, beginning_nanosec, force_stop_at_end ? end_nanosec : 0); current_pipeline_->SetNextUrl(gst_url, beginning_nanosec, force_stop_at_end ? end_nanosec : 0);
} }
QUrl GstEngine::FixupUrl(const QUrl &url) { QByteArray GstEngine::FixupUrl(const QUrl &url) {
QUrl copy = url; EnsureInitialised();
QByteArray uri;
// It's a file:// url with a hostname set. QUrl::fromLocalFile does this when given a \\host\share\file path on Windows. Munge it back into a path that gstreamer will recognise. // It's a file:// url with a hostname set. QUrl::fromLocalFile does this when given a \\host\share\file path on Windows. Munge it back into a path that gstreamer will recognise.
if (url.scheme() == "file" && !url.host().isEmpty()) { if (url.scheme() == "file" && !url.host().isEmpty()) {
copy.setPath("//" + copy.host() + copy.path()); QString str = "//" + url.host() + url.path();
copy.setHost(QString()); uri = str.toLocal8Bit();
}
else if (url.scheme() == "cdda") {
QString str;
if (url.path().isEmpty()) {
str = url.toString();
str.remove(str.lastIndexOf(QChar('a')), 1);
}
else {
// Currently, Gstreamer can't handle input CD devices inside cdda URL.
// So we handle them ourselve: we extract the track number and re-create an URL with only cdda:// + the track number (which can be handled by Gstreamer).
// We keep the device in mind, and we will set it later using SourceSetupCallback
QStringList path = url.path().split('/');
str = QString("cdda://%1a").arg(path.takeLast());
QString device = path.join("/");
current_pipeline_->SetSourceDevice(device);
}
uri = str.toLocal8Bit();
}
else {
uri = url.toEncoded();
} }
return copy; return uri;
} }
@@ -389,7 +409,7 @@ bool GstEngine::Load(const QUrl &url, Engine::TrackChangeFlags change, bool forc
Engine::Base::Load(url, change, force_stop_at_end, beginning_nanosec, end_nanosec); Engine::Base::Load(url, change, force_stop_at_end, beginning_nanosec, end_nanosec);
QUrl gst_url = FixupUrl(url); QByteArray gst_url = FixupUrl(url);
bool crossfade = current_pipeline_ && ((crossfade_enabled_ && change & Engine::Manual) || (autocrossfade_enabled_ && change & Engine::Auto) || ((crossfade_enabled_ || autocrossfade_enabled_) && change & Engine::Intro)); bool crossfade = current_pipeline_ && ((crossfade_enabled_ && change & Engine::Manual) || (autocrossfade_enabled_ && change & Engine::Auto) || ((crossfade_enabled_ || autocrossfade_enabled_) && change & Engine::Intro));
@@ -477,7 +497,7 @@ void GstEngine::PlayDone(QFuture<GstStateChangeReturn> future, const quint64 off
if (ret == GST_STATE_CHANGE_FAILURE) { if (ret == GST_STATE_CHANGE_FAILURE) {
// Failure, but we got a redirection URL - try loading that instead // Failure, but we got a redirection URL - try loading that instead
QUrl redirect_url = current_pipeline_->redirect_url(); QByteArray redirect_url = current_pipeline_->redirect_url();
if (!redirect_url.isEmpty() && redirect_url != current_pipeline_->url()) { if (!redirect_url.isEmpty() && redirect_url != current_pipeline_->url()) {
qLog(Info) << "Redirecting to" << redirect_url; qLog(Info) << "Redirecting to" << redirect_url;
current_pipeline_ = CreatePipeline(redirect_url, end_nanosec_); current_pipeline_ = CreatePipeline(redirect_url, end_nanosec_);
@@ -511,8 +531,7 @@ void GstEngine::Stop(bool stop_after) {
url_ = QUrl(); // To ensure we return Empty from state() url_ = QUrl(); // To ensure we return Empty from state()
beginning_nanosec_ = end_nanosec_ = 0; beginning_nanosec_ = end_nanosec_ = 0;
// Check if we started a fade out. If it isn't finished yet and the user // Check if we started a fade out. If it isn't finished yet and the user pressed stop, we cancel the fader and just stop the playback.
// pressed stop, we cancel the fader and just stop the playback.
if (is_fading_out_to_pause_) { if (is_fading_out_to_pause_) {
disconnect(current_pipeline_.get(), SIGNAL(FaderFinished()), 0, 0); disconnect(current_pipeline_.get(), SIGNAL(FaderFinished()), 0, 0);
is_fading_out_to_pause_ = false; is_fading_out_to_pause_ = false;
@@ -552,8 +571,7 @@ void GstEngine::Pause() {
if (!current_pipeline_ || current_pipeline_->is_buffering()) return; if (!current_pipeline_ || current_pipeline_->is_buffering()) return;
// Check if we started a fade out. If it isn't finished yet and the user // Check if we started a fade out. If it isn't finished yet and the user pressed play, we inverse the fader and resume the playback.
// pressed play, we inverse the fader and resume the playback.
if (is_fading_out_to_pause_) { if (is_fading_out_to_pause_) {
disconnect(current_pipeline_.get(), SIGNAL(FaderFinished()), 0, 0); disconnect(current_pipeline_.get(), SIGNAL(FaderFinished()), 0, 0);
current_pipeline_->StartFader(fadeout_pause_duration_nanosec_, QTimeLine::Forward, QTimeLine::EaseInOutCurve, false); current_pipeline_->StartFader(fadeout_pause_duration_nanosec_, QTimeLine::Forward, QTimeLine::EaseInOutCurve, false);
@@ -699,9 +717,8 @@ void GstEngine::HandlePipelineError(int pipeline_id, const QString &message, int
// unable to play media stream with this url // unable to play media stream with this url
emit InvalidSongRequested(url_); emit InvalidSongRequested(url_);
// TODO: the types of errors listed below won't be shown to user - they will // TODO: the types of errors listed below won't be shown to user
// get logged and the current song will be skipped; instead of maintaining // they will get logged and the current song will be skipped; instead of maintaining the list we should probably:
// the list we should probably:
// - don't report any engine's errors to user (always just log and skip) // - don't report any engine's errors to user (always just log and skip)
// - come up with a less intrusive error box (not a dialog but a notification // - come up with a less intrusive error box (not a dialog but a notification
// popup of some kind) and then report all errors // popup of some kind) and then report all errors
@@ -813,6 +830,16 @@ shared_ptr<GstEnginePipeline> GstEngine::CreatePipeline(const QUrl &url, qint64
return ret; return ret;
} }
if (!ret->InitFromUrl(url.toEncoded(), end_nanosec)) ret.reset();
return ret;
}
shared_ptr<GstEnginePipeline> GstEngine::CreatePipeline(const QByteArray &url, qint64 end_nanosec) {
shared_ptr<GstEnginePipeline> ret = CreatePipeline();
if (!ret->InitFromUrl(url, end_nanosec)) ret.reset(); if (!ret->InitFromUrl(url, end_nanosec)) ret.reset();
return ret; return ret;

View File

@@ -138,7 +138,6 @@ class GstEngine : public Engine::Base, public BufferConsumer {
void BufferingFinished(); void BufferingFinished();
private: private:
PluginDetailsList GetPluginList(const QString &classname) const; PluginDetailsList GetPluginList(const QString &classname) const;
void StartFadeout(); void StartFadeout();
@@ -149,17 +148,17 @@ class GstEngine : public Engine::Base, public BufferConsumer {
std::shared_ptr<GstEnginePipeline> CreatePipeline(); std::shared_ptr<GstEnginePipeline> CreatePipeline();
std::shared_ptr<GstEnginePipeline> CreatePipeline(const QUrl &url, qint64 end_nanosec); std::shared_ptr<GstEnginePipeline> CreatePipeline(const QUrl &url, qint64 end_nanosec);
std::shared_ptr<GstEnginePipeline> CreatePipeline(const QByteArray &url, qint64 end_nanosec);
void UpdateScope(int chunk_length); void UpdateScope(int chunk_length);
static QUrl FixupUrl(const QUrl &url); QByteArray FixupUrl(const QUrl &url);
private: private:
static const qint64 kTimerIntervalNanosec = 1000 *kNsecPerMsec; // 1s static const qint64 kTimerIntervalNanosec = 1000 *kNsecPerMsec; // 1s
static const qint64 kPreloadGapNanosec = 2000 *kNsecPerMsec; // 2s static const qint64 kPreloadGapNanosec = 3000 *kNsecPerMsec; // 3s
static const qint64 kSeekDelayNanosec = 100 *kNsecPerMsec; // 100msec static const qint64 kSeekDelayNanosec = 100 *kNsecPerMsec; // 100msec
static const char *kHypnotoadPipeline;
static const char *kEnterprisePipeline; static const char *kEnterprisePipeline;
TaskManager *task_manager_; TaskManager *task_manager_;
@@ -215,6 +214,4 @@ class GstEngine : public Engine::Base, public BufferConsumer {
}; };
//Q_DECLARE_METATYPE(GstEngine::OutputDetails)
#endif /* GSTENGINE_H */ #endif /* GSTENGINE_H */

View File

@@ -25,8 +25,13 @@
#include <QCoreApplication> #include <QCoreApplication>
#include <QDir> #include <QDir>
#include <QPair> #include <QPair>
#include <QRegExp> #include <QByteArray>
#include <QVariant>
#include <QString>
#include <QUuid> #include <QUuid>
#include <QList>
#include <QMetaObject>
#include <QMutexLocker>
#include "bufferconsumer.h" #include "bufferconsumer.h"
#include "gstelementdeleter.h" #include "gstelementdeleter.h"
@@ -56,9 +61,6 @@ GstEnginePipeline::GstEnginePipeline(GstEngine *engine)
sink_(GstEngine::kAutoSink), sink_(GstEngine::kAutoSink),
segment_start_(0), segment_start_(0),
segment_start_received_(false), segment_start_received_(false),
emit_track_ended_on_stream_start_(false),
emit_track_ended_on_time_discontinuity_(false),
last_buffer_offset_(0),
eq_enabled_(false), eq_enabled_(false),
eq_preamp_(0), eq_preamp_(0),
stereo_balance_(0.0f), stereo_balance_(0.0f),
@@ -78,11 +80,10 @@ GstEnginePipeline::GstEnginePipeline(GstEngine *engine)
pipeline_is_initialised_(false), pipeline_is_initialised_(false),
pipeline_is_connected_(false), pipeline_is_connected_(false),
pending_seek_nanosec_(-1), pending_seek_nanosec_(-1),
last_known_position_ns_(0), next_uri_set_(false),
volume_percent_(100), volume_percent_(100),
volume_modifier_(1.0), volume_modifier_(1.0),
pipeline_(nullptr), pipeline_(nullptr),
uridecodebin_(nullptr),
audiobin_(nullptr), audiobin_(nullptr),
queue_(nullptr), queue_(nullptr),
audioconvert_(nullptr), audioconvert_(nullptr),
@@ -130,50 +131,23 @@ void GstEnginePipeline::set_mono_playback(bool enabled) {
mono_playback_ = enabled; mono_playback_ = enabled;
} }
bool GstEnginePipeline::ReplaceDecodeBin(GstElement *new_bin) { bool GstEnginePipeline::InitDecodeBin(GstElement *decode_bin) {
if (!new_bin) return false; if (!decode_bin) return false;
// Destroy the old elements if they are set pipeline_ = gst_pipeline_new("pipeline");
// Note that the caller to this function MUST schedule the old uridecodebin_
// for deletion in the main thread.
if (uridecodebin_) {
gst_bin_remove(GST_BIN(pipeline_), uridecodebin_);
}
uridecodebin_ = new_bin; gst_bin_add(GST_BIN(pipeline_), decode_bin);
segment_start_ = 0;
segment_start_received_ = false; if (!InitAudioBin()) return false;
pipeline_is_connected_ = false; gst_bin_add(GST_BIN(pipeline_), audiobin_);
gst_bin_add(GST_BIN(pipeline_), uridecodebin_);
gst_element_link(decode_bin, audiobin_);
return true; return true;
} }
bool GstEnginePipeline::ReplaceDecodeBin(const QUrl &url) {
QByteArray uri;
if (url.scheme() == "cdda") {
QString str = url.toString();
str.remove(str.lastIndexOf(QChar('a')), 1);
uri = str.toLocal8Bit();;
}
else {
uri = url.toEncoded();
}
GstElement *new_bin = nullptr;
new_bin = engine_->CreateElement("uridecodebin");
g_object_set(G_OBJECT(new_bin), "uri", uri.constData(), nullptr);
CHECKED_GCONNECT(G_OBJECT(new_bin), "drained", &SourceDrainedCallback, this);
CHECKED_GCONNECT(G_OBJECT(new_bin), "pad-added", &NewPadCallback, this);
CHECKED_GCONNECT(G_OBJECT(new_bin), "notify::source", &SourceSetupCallback, this);
return ReplaceDecodeBin(new_bin);
}
GstElement *GstEnginePipeline::CreateDecodeBinFromString(const char *pipeline) { GstElement *GstEnginePipeline::CreateDecodeBinFromString(const char *pipeline) {
GError *error = nullptr; GError *error = nullptr;
@@ -196,7 +170,7 @@ GstElement *GstEnginePipeline::CreateDecodeBinFromString(const char *pipeline) {
} }
bool GstEnginePipeline::Init() { bool GstEnginePipeline::InitAudioBin() {
// 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: // 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:
// uri decode bin -> audio bin // uri decode bin -> audio bin
@@ -216,7 +190,7 @@ bool GstEnginePipeline::Init() {
// Audio bin // Audio bin
audiobin_ = gst_bin_new("audiobin"); audiobin_ = gst_bin_new("audiobin");
//audiobin_ = gst_bin_new("playbackbin"); //audiobin_ = gst_bin_new("playbackbin");
gst_bin_add(GST_BIN(pipeline_), audiobin_); //gst_bin_add(GST_BIN(pipeline_), audiobin_);
// Create the sink // Create the sink
if (!(audiosink_ = engine_->CreateElement(sink_, audiobin_))) return false; if (!(audiosink_ = engine_->CreateElement(sink_, audiobin_))) return false;
@@ -232,7 +206,6 @@ bool GstEnginePipeline::Init() {
g_object_set(G_OBJECT(audiosink_), "device", device_.toInt(), nullptr); g_object_set(G_OBJECT(audiosink_), "device", device_.toInt(), nullptr);
break; break;
case QVariant::String: case QVariant::String:
//qLog(Debug) << device_.toString().toUtf8().constData();
//qLog(Info) << "g_object_set: " << device_.toString().toUtf8().constData(); //qLog(Info) << "g_object_set: " << device_.toString().toUtf8().constData();
//g_object_set(G_OBJECT(audiosink_), "device", device_.toString().toUtf8().constData(), nullptr); //g_object_set(G_OBJECT(audiosink_), "device", device_.toString().toUtf8().constData(), nullptr);
g_object_set(audiosink_, "device", device_.toString().toUtf8().constData(), nullptr); g_object_set(audiosink_, "device", device_.toString().toUtf8().constData(), nullptr);
@@ -276,10 +249,8 @@ bool GstEnginePipeline::Init() {
return false; return false;
} }
// Create the replaygain elements if it's enabled. event_probe is the // Create the replaygain elements if it's enabled. event_probe is the audioconvert element we attach the probe to, which will change depending on whether replaygain is enabled.
// audioconvert element we attach the probe to, which will change depending // convert_sink is the element after the first audioconvert, which again will change.
// on whether replaygain is enabled. convert_sink is the element after the
// first audioconvert, which again will change.
GstElement *event_probe = audioconvert_; GstElement *event_probe = audioconvert_;
GstElement *convert_sink = tee; GstElement *convert_sink = tee;
@@ -306,8 +277,7 @@ bool GstEnginePipeline::Init() {
gst_object_unref(pad); gst_object_unref(pad);
// Add a data probe on the src pad of the audioconvert element for our scope. // Add a data probe on the src pad of the audioconvert element for our scope.
// We do it here because we want pre-equalized and pre-volume samples // We do it here because we want pre-equalized and pre-volume samples so that our visualization are not be affected by them.
// so that our visualization are not be affected by them.
pad = gst_element_get_static_pad(event_probe, "src"); pad = gst_element_get_static_pad(event_probe, "src");
gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_EVENT_UPSTREAM, &EventHandoffCallback, this, NULL); gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_EVENT_UPSTREAM, &EventHandoffCallback, this, NULL);
gst_object_unref(pad); gst_object_unref(pad);
@@ -317,11 +287,10 @@ bool GstEnginePipeline::Init() {
// Setting the equalizer bands: // Setting the equalizer bands:
// //
// GStreamer's GstIirEqualizerNBands sets up shelve filters for the first and // GStreamer's GstIirEqualizerNBands sets up shelve filters for the first and last bands as corner cases.
// last bands as corner cases. That was causing the "inverted slider" bug. // That was causing the "inverted slider" bug.
// As a workaround, we create two dummy bands at both ends of the spectrum. // As a workaround, we create two dummy bands at both ends of the spectrum.
// This causes the actual first and last adjustable bands to be // This causes the actual first and last adjustable bands to be implemented using band-pass filters.
// implemented using band-pass filters.
g_object_set(G_OBJECT(equalizer_), "num-bands", 10 + 2, nullptr); g_object_set(G_OBJECT(equalizer_), "num-bands", 10 + 2, nullptr);
@@ -351,11 +320,8 @@ bool GstEnginePipeline::Init() {
// Set the stereo balance. // Set the stereo balance.
g_object_set(G_OBJECT(stereo_panorama_), "panorama", stereo_balance_, nullptr); g_object_set(G_OBJECT(stereo_panorama_), "panorama", stereo_balance_, nullptr);
// Set the buffer duration. We set this on this queue instead of the // Set the buffer duration. We set this on this queue instead of the decode bin (in ReplaceDecodeBin()) because setting it on the decode bin only affects network sources.
// decode bin (in ReplaceDecodeBin()) because setting it on the decode bin // Disable the default buffer and byte limits, so we only buffer based on time.
// only affects network sources.
// Disable the default buffer and byte limits, so we only buffer based on
// time.
g_object_set(G_OBJECT(queue_), "max-size-buffers", 0, nullptr); g_object_set(G_OBJECT(queue_), "max-size-buffers", 0, nullptr);
g_object_set(G_OBJECT(queue_), "max-size-bytes", 0, nullptr); g_object_set(G_OBJECT(queue_), "max-size-bytes", 0, nullptr);
@@ -401,61 +367,35 @@ bool GstEnginePipeline::Init() {
gst_bus_set_sync_handler(gst_pipeline_get_bus(GST_PIPELINE(pipeline_)), BusCallbackSync, this, nullptr); gst_bus_set_sync_handler(gst_pipeline_get_bus(GST_PIPELINE(pipeline_)), BusCallbackSync, this, nullptr);
bus_cb_id_ = gst_bus_add_watch(gst_pipeline_get_bus(GST_PIPELINE(pipeline_)), BusCallback, this); bus_cb_id_ = gst_bus_add_watch(gst_pipeline_get_bus(GST_PIPELINE(pipeline_)), BusCallback, this);
MaybeLinkDecodeToAudio();
return true; return true;
} }
void GstEnginePipeline::MaybeLinkDecodeToAudio() {
if (!uridecodebin_ || !audiobin_) return;
GstPad *pad = gst_element_get_static_pad(uridecodebin_, "src");
if (!pad) return;
gst_object_unref(pad);
gst_element_link(uridecodebin_, audiobin_);
}
bool GstEnginePipeline::InitFromString(const QString &pipeline) { bool GstEnginePipeline::InitFromString(const QString &pipeline) {
pipeline_ = gst_pipeline_new("pipeline"); GstElement *new_bin = CreateDecodeBinFromString(pipeline.toUtf8().constData());
return InitDecodeBin(new_bin);
GstElement *new_bin = CreateDecodeBinFromString(pipeline.toLatin1().constData());
if (!new_bin) {
return false;
}
if (!ReplaceDecodeBin(new_bin)) return false;
if (!Init()) return false;
return gst_element_link(new_bin, audiobin_);
} }
bool GstEnginePipeline::InitFromUrl(const QUrl &url, qint64 end_nanosec) { bool GstEnginePipeline::InitFromUrl(const QByteArray &url, qint64 end_nanosec) {
pipeline_ = gst_pipeline_new("pipeline");
if (url.scheme() == "cdda" && !url.path().isEmpty()) {
// Currently, Gstreamer can't handle input CD devices inside cdda URL.
// So we handle them ourselve: we extract the track number and re-create an URL with only cdda:// + the track number (which can be handled by Gstreamer).
// We keep the device in mind, and we will set it later using SourceSetupCallback
QStringList path = url.path().split('/');
url_ = QUrl(QString("cdda://%1").arg(path.takeLast()));
source_device_ = path.join("/");
}
else {
url_ = url;
}
end_offset_nanosec_ = end_nanosec; end_offset_nanosec_ = end_nanosec;
// Decode bin pipeline_ = engine_->CreateElement("playbin");
if (!ReplaceDecodeBin(url_)) return false; g_object_set(G_OBJECT(pipeline_), "uri", url.constData(), nullptr);
CHECKED_GCONNECT(G_OBJECT(pipeline_), "about-to-finish", &AboutToFinishCallback, this);
return Init(); CHECKED_GCONNECT(G_OBJECT(pipeline_), "pad-added", &NewPadCallback, this);
CHECKED_GCONNECT(G_OBJECT(pipeline_), "notify::source", &SourceSetupCallback, this);
if (!InitAudioBin()) return false;
// Set playbin's sink to be our costum audio-sink.
g_object_set(GST_OBJECT(pipeline_), "audio-sink", audiobin_, NULL);
pipeline_is_connected_ = true;
return true;
} }
@@ -497,7 +437,7 @@ gboolean GstEnginePipeline::BusCallback(GstBus*, GstMessage *msg, gpointer self)
} }
GstBusSyncReply GstEnginePipeline::BusCallbackSync(GstBus*, GstMessage *msg, gpointer self) { GstBusSyncReply GstEnginePipeline::BusCallbackSync(GstBus *, GstMessage *msg, gpointer self) {
GstEnginePipeline *instance = reinterpret_cast<GstEnginePipeline*>(self); GstEnginePipeline *instance = reinterpret_cast<GstEnginePipeline*>(self);
@@ -533,11 +473,7 @@ GstBusSyncReply GstEnginePipeline::BusCallbackSync(GstBus*, GstMessage *msg, gpo
break; break;
case GST_MESSAGE_STREAM_START: case GST_MESSAGE_STREAM_START:
if (instance->emit_track_ended_on_stream_start_) { instance->StreamStartMessageReceived();
qLog(Debug) << "New segment started, EOS will signal on next buffer discontinuity";
instance->emit_track_ended_on_stream_start_ = false;
instance->emit_track_ended_on_time_discontinuity_ = true;
}
break; break;
default: default:
@@ -564,9 +500,25 @@ void GstEnginePipeline::StreamStatusMessageReceived(GstMessage *msg) {
} }
void GstEnginePipeline::TaskEnterCallback(GstTask*, GThread*, gpointer) { void GstEnginePipeline::StreamStartMessageReceived() {
// Bump the priority of the thread only on OS X if (next_uri_set_) {
next_uri_set_ = false;
url_ = next_url_;
end_offset_nanosec_ = next_end_offset_nanosec_;
next_url_ = QByteArray();
next_beginning_offset_nanosec_ = 0;
next_end_offset_nanosec_ = 0;
emit EndOfStreamReached(id(), true);
}
}
void GstEnginePipeline::TaskEnterCallback(GstTask *, GThread *, gpointer) {
// Bump the priority of the thread only on OS X
#ifdef Q_OS_DARWIN #ifdef Q_OS_DARWIN
sched_param param; sched_param param;
@@ -585,10 +537,8 @@ void GstEnginePipeline::ElementMessageReceived(GstMessage *msg) {
if (gst_structure_has_name(structure, "redirect")) { if (gst_structure_has_name(structure, "redirect")) {
const char *uri = gst_structure_get_string(structure, "new-location"); const char *uri = gst_structure_get_string(structure, "new-location");
// Set the redirect URL. In mmssrc redirect messages come during the // Set the redirect URL. In mmssrc redirect messages come during the initial state change to PLAYING, so callers can pick up this URL after the state change has failed.
// initial state change to PLAYING, so callers can pick up this URL after redirect_url_ = uri;
// the state change has failed.
redirect_url_ = QUrl::fromEncoded(uri);
} }
} }
@@ -606,6 +556,16 @@ void GstEnginePipeline::ErrorMessageReceived(GstMessage *msg) {
g_error_free(error); g_error_free(error);
free(debugs); free(debugs);
if (pipeline_is_initialised_ && next_uri_set_ && (domain == GST_RESOURCE_ERROR || domain == GST_STREAM_ERROR)) {
// A track is still playing and the next uri is not playable. We ignore the error here so it can play until the end.
// But there is no message send to the bus when the current track finishes, we have to add an EOS ourself.
qLog(Debug) << "Ignoring error when loading next track";
GstPad* sinkpad = gst_element_get_static_pad(audiobin_, "sink");
gst_pad_send_event(sinkpad, gst_event_new_eos());
gst_object_unref(sinkpad);
return;
}
if (!redirect_url_.isEmpty() && debugstr.contains("A redirect message was posted on the bus and should have been handled by the application.")) { if (!redirect_url_.isEmpty() && debugstr.contains("A redirect message was posted on the bus and should have been handled by the application.")) {
// mmssrc posts a message on the bus *and* makes an error message when it wants to do a redirect. // mmssrc posts a message on the bus *and* makes an error message when it wants to do a redirect.
// We handle the message, but now we have to ignore the error too. // We handle the message, but now we have to ignore the error too.
@@ -671,25 +631,24 @@ void GstEnginePipeline::StateChangedMessageReceived(GstMessage *msg) {
if (pipeline_is_initialised_ && new_state != GST_STATE_PAUSED && new_state != GST_STATE_PLAYING) { if (pipeline_is_initialised_ && new_state != GST_STATE_PAUSED && new_state != GST_STATE_PLAYING) {
pipeline_is_initialised_ = false; pipeline_is_initialised_ = false;
if (next_uri_set_ && new_state == GST_STATE_READY) {
// Revert uri and go back to PLAY state again
next_uri_set_ = false;
g_object_set(G_OBJECT(pipeline_), "uri", url_.constData(), nullptr);
SetState(GST_STATE_PLAYING);
}
} }
} }
void GstEnginePipeline::BufferingMessageReceived(GstMessage *msg) { void GstEnginePipeline::BufferingMessageReceived(GstMessage *msg) {
// Only handle buffering messages from the queue2 element in audiobin - not // Only handle buffering messages from the queue2 element in audiobin - not the one that's created automatically by uridecodebin.
// the one that's created automatically by uridecodebin.
if (GST_ELEMENT(GST_MESSAGE_SRC(msg)) != queue_) { if (GST_ELEMENT(GST_MESSAGE_SRC(msg)) != queue_) {
return; return;
} }
// If we are loading new next track, we don't have to pause the playback.
// The buffering is for the next track and not the current one.
if (emit_track_ended_on_stream_start_) {
qLog(Debug) << "Buffering next track";
return;
}
int percent = 0; int percent = 0;
gst_message_parse_buffering(msg, &percent); gst_message_parse_buffering(msg, &percent);
@@ -727,9 +686,7 @@ void GstEnginePipeline::NewPadCallback(GstElement*, GstPad *pad, gpointer self)
gst_pad_link(pad, audiopad); gst_pad_link(pad, audiopad);
gst_object_unref(audiopad); gst_object_unref(audiopad);
// Offset the timestamps on all the buffers coming out of the decodebin so // Offset the timestamps on all the buffers coming out of the decodebin so they line up exactly with the end of the last buffer from the old decodebin.
// they line up exactly with the end of the last buffer from the old
// decodebin.
// "Running time" is the time since the last flushing seek. // "Running time" is the time since the last flushing seek.
GstClockTime running_time = gst_segment_to_running_time(&instance->last_decodebin_segment_, GST_FORMAT_TIME, instance->last_decodebin_segment_.position); GstClockTime running_time = gst_segment_to_running_time(&instance->last_decodebin_segment_, GST_FORMAT_TIME, instance->last_decodebin_segment_.position);
gst_pad_set_offset(pad, running_time); gst_pad_set_offset(pad, running_time);
@@ -750,9 +707,7 @@ GstPadProbeReturn GstEnginePipeline::DecodebinProbe(GstPad *pad, GstPadProbeInfo
const GstPadProbeType info_type = GST_PAD_PROBE_INFO_TYPE(info); const GstPadProbeType info_type = GST_PAD_PROBE_INFO_TYPE(info);
if (info_type & GST_PAD_PROBE_TYPE_BUFFER) { if (info_type & GST_PAD_PROBE_TYPE_BUFFER) {
// The decodebin produced a buffer. Record its end time, so we can offset // The decodebin produced a buffer. Record its end time, so we can offset the buffers produced by the next decodebin when transitioning to the next song.
// the buffers produced by the next decodebin when transitioning to the next
// song.
GstBuffer *buffer = GST_PAD_PROBE_INFO_BUFFER(info); GstBuffer *buffer = GST_PAD_PROBE_INFO_BUFFER(info);
GstClockTime timestamp = GST_BUFFER_TIMESTAMP(buffer); GstClockTime timestamp = GST_BUFFER_TIMESTAMP(buffer);
@@ -772,13 +727,11 @@ GstPadProbeReturn GstEnginePipeline::DecodebinProbe(GstPad *pad, GstPadProbeInfo
GstEventType event_type = GST_EVENT_TYPE(event); GstEventType event_type = GST_EVENT_TYPE(event);
if (event_type == GST_EVENT_SEGMENT) { if (event_type == GST_EVENT_SEGMENT) {
// A new segment started, we need to save this to calculate running time // A new segment started, we need to save this to calculate running time offsets later.
// offsets later.
gst_event_copy_segment(event, &instance->last_decodebin_segment_); gst_event_copy_segment(event, &instance->last_decodebin_segment_);
} }
else if (event_type == GST_EVENT_FLUSH_START) { else if (event_type == GST_EVENT_FLUSH_START) {
// A flushing seek resets the running time to 0, so remove any offset // A flushing seek resets the running time to 0, so remove any offset we set on this pad before.
// we set on this pad before.
gst_pad_set_offset(pad, 0); gst_pad_set_offset(pad, 0);
} }
} }
@@ -803,32 +756,23 @@ GstPadProbeReturn GstEnginePipeline::HandoffCallback(GstPad*, GstPadProbeInfo *i
consumer->ConsumeBuffer(buf, instance->id()); consumer->ConsumeBuffer(buf, instance->id());
} }
// Calculate the end time of this buffer so we can stop playback if it's // Calculate the end time of this buffer so we can stop playback if it's after the end time of this song.
// after the end time of this song.
if (instance->end_offset_nanosec_ > 0) { if (instance->end_offset_nanosec_ > 0) {
quint64 start_time = GST_BUFFER_TIMESTAMP(buf) - instance->segment_start_; quint64 start_time = GST_BUFFER_TIMESTAMP(buf) - instance->segment_start_;
quint64 duration = GST_BUFFER_DURATION(buf); quint64 duration = GST_BUFFER_DURATION(buf);
quint64 end_time = start_time + duration; quint64 end_time = start_time + duration;
if (end_time > instance->end_offset_nanosec_) { if (end_time > instance->end_offset_nanosec_) {
if (instance->has_next_valid_url()) { if (instance->has_next_valid_url() && instance->next_url_ == instance->url_ && instance->next_beginning_offset_nanosec_ == instance->end_offset_nanosec_) {
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.
// 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->end_offset_nanosec_ = instance->next_end_offset_nanosec_;
instance->next_url_ = QUrl(); instance->next_url_ = QByteArray();
instance->next_beginning_offset_nanosec_ = 0; instance->next_beginning_offset_nanosec_ = 0;
instance->next_end_offset_nanosec_ = 0; instance->next_end_offset_nanosec_ = 0;
// GstEngine will try to seek to the start of the new section, but // GstEngine will try to seek to the start of the new section, but we're already there so ignore it.
// we're already there so ignore it.
instance->ignore_next_seek_ = true; instance->ignore_next_seek_ = true;
emit instance->EndOfStreamReached(instance->id(), true); emit instance->EndOfStreamReached(instance->id(), true);
}
else {
// We have a next song but we can't cheat, so move to it normally.
instance->TransitionToNext();
}
} }
else { else {
// There's no next song // There's no next song
@@ -837,16 +781,6 @@ GstPadProbeReturn GstEnginePipeline::HandoffCallback(GstPad*, GstPadProbeInfo *i
} }
} }
if (instance->emit_track_ended_on_time_discontinuity_) {
if (GST_BUFFER_FLAG_IS_SET(buf, GST_BUFFER_FLAG_DISCONT) || GST_BUFFER_OFFSET(buf) < instance->last_buffer_offset_) {
qLog(Debug) << "Buffer discontinuity - emitting EOS";
instance->emit_track_ended_on_time_discontinuity_ = false;
emit instance->EndOfStreamReached(instance->id(), true);
}
}
instance->last_buffer_offset_ = GST_BUFFER_OFFSET(buf);
return GST_PAD_PROBE_OK; return GST_PAD_PROBE_OK;
} }
@@ -861,8 +795,7 @@ GstPadProbeReturn GstEnginePipeline::EventHandoffCallback(GstPad*, GstPadProbeIn
switch (GST_EVENT_TYPE(e)) { switch (GST_EVENT_TYPE(e)) {
case GST_EVENT_SEGMENT: case GST_EVENT_SEGMENT:
if (!instance->segment_start_received_) { if (!instance->segment_start_received_) {
// The segment start time is used to calculate the proper offset of data // The segment start time is used to calculate the proper offset of data buffers from the start of the stream
// buffers from the start of the stream
const GstSegment *segment = nullptr; const GstSegment *segment = nullptr;
gst_event_parse_segment(e, &segment); gst_event_parse_segment(e, &segment);
instance->segment_start_ = segment->start; instance->segment_start_ = segment->start;
@@ -878,17 +811,20 @@ GstPadProbeReturn GstEnginePipeline::EventHandoffCallback(GstPad*, GstPadProbeIn
} }
void GstEnginePipeline::SourceDrainedCallback(GstURIDecodeBin *bin, gpointer self) { void GstEnginePipeline::AboutToFinishCallback(GstPlayBin *bin, gpointer self) {
GstEnginePipeline *instance = reinterpret_cast<GstEnginePipeline*>(self); GstEnginePipeline* instance = reinterpret_cast<GstEnginePipeline*>(self);
if (instance->has_next_valid_url()) { if (instance->has_next_valid_url() && !instance->next_uri_set_) {
instance->TransitionToNext(); // Set the next uri. When the current song ends it will be played automatically and a STREAM_START message is send to the bus.
// When the next uri is not playable an error message is send when the pipeline goes to PLAY (or PAUSE) state or immediately if it is currently in PLAY state.
instance->next_uri_set_ = true;
g_object_set(G_OBJECT(instance->pipeline_), "uri", instance->next_url_.constData(), nullptr);
} }
} }
void GstEnginePipeline::SourceSetupCallback(GstURIDecodeBin *bin, GParamSpec *pspec, gpointer self) { void GstEnginePipeline::SourceSetupCallback(GstPlayBin *bin, GParamSpec *pspec, gpointer self) {
GstEnginePipeline *instance = reinterpret_cast<GstEnginePipeline*>(self); GstEnginePipeline *instance = reinterpret_cast<GstEnginePipeline*>(self);
GstElement *element; GstElement *element;
@@ -913,32 +849,12 @@ void GstEnginePipeline::SourceSetupCallback(GstURIDecodeBin *bin, GParamSpec *ps
#endif #endif
} }
} // If the pipeline was buffering we stop that now.
if (instance->buffering_) {
void GstEnginePipeline::TransitionToNext() { instance->buffering_ = false;
emit instance->BufferingFinished();
GstElement *old_decode_bin = uridecodebin_; instance->SetState(GST_STATE_PLAYING);
}
ignore_tags_ = true;
ReplaceDecodeBin(next_url_);
gst_element_set_state(uridecodebin_, GST_STATE_PLAYING);
MaybeLinkDecodeToAudio();
url_ = next_url_;
end_offset_nanosec_ = next_end_offset_nanosec_;
next_url_ = QUrl();
next_beginning_offset_nanosec_ = 0;
next_end_offset_nanosec_ = 0;
// This function gets called when the source has been drained, even if the song hasn't finished playing yet.
// We'll get a new stream when it really does finish, so emit TrackEnded then.
emit_track_ended_on_stream_start_ = 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;
} }
@@ -986,6 +902,14 @@ bool GstEnginePipeline::Seek(qint64 nanosec) {
return true; return true;
} }
if (next_uri_set_) {
qDebug() << "MYTODO: gstenginepipeline.seek: seeking after Transition";
pending_seek_nanosec_ = nanosec;
SetState(GST_STATE_READY);
return true;
}
pending_seek_nanosec_ = -1; pending_seek_nanosec_ = -1;
last_known_position_ns_ = nanosec; last_known_position_ns_ = nanosec;
return gst_element_seek_simple(pipeline_, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH, nanosec); return gst_element_seek_simple(pipeline_, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH, nanosec);
@@ -1119,6 +1043,7 @@ void GstEnginePipeline::timerEvent(QTimerEvent *e) {
} }
QObject::timerEvent(e); QObject::timerEvent(e);
} }
void GstEnginePipeline::AddBufferConsumer(BufferConsumer *consumer) { void GstEnginePipeline::AddBufferConsumer(BufferConsumer *consumer) {
@@ -1136,7 +1061,7 @@ void GstEnginePipeline::RemoveAllBufferConsumers() {
buffer_consumers_.clear(); buffer_consumers_.clear();
} }
void GstEnginePipeline::SetNextUrl(const QUrl &url, qint64 beginning_nanosec, qint64 end_nanosec) { void GstEnginePipeline::SetNextUrl(const QByteArray &url, qint64 beginning_nanosec, qint64 end_nanosec) {
next_url_ = url; next_url_ = url;
next_beginning_offset_nanosec_ = beginning_nanosec; next_beginning_offset_nanosec_ = beginning_nanosec;

View File

@@ -33,7 +33,8 @@
#include <QObject> #include <QObject>
#include <QThreadPool> #include <QThreadPool>
#include <QTimeLine> #include <QTimeLine>
#include <QUrl> #include <QByteArray>
#include <QVariant>
#include "engine_fwd.h" #include "engine_fwd.h"
@@ -42,7 +43,7 @@ class GstEngine;
class BufferConsumer; class BufferConsumer;
struct GstQueue; struct GstQueue;
struct GstURIDecodeBin; struct GstPlayBin;
class GstEnginePipeline : public QObject { class GstEnginePipeline : public QObject {
Q_OBJECT Q_OBJECT
@@ -62,7 +63,7 @@ class GstEnginePipeline : public QObject {
void set_mono_playback(bool enabled); void set_mono_playback(bool enabled);
// Creates the pipeline, returns false on error // Creates the pipeline, returns false on error
bool InitFromUrl(const QUrl &url, qint64 end_nanosec); bool InitFromUrl(const QByteArray &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.
@@ -79,30 +80,27 @@ class GstEnginePipeline : public QObject {
void SetStereoBalance(float value); void SetStereoBalance(float value);
void StartFader(qint64 duration_nanosec, QTimeLine::Direction direction = QTimeLine::Forward, QTimeLine::CurveShape shape = QTimeLine::LinearCurve, bool use_fudge_timer = true); void StartFader(qint64 duration_nanosec, QTimeLine::Direction direction = QTimeLine::Forward, QTimeLine::CurveShape shape = QTimeLine::LinearCurve, bool use_fudge_timer = true);
// 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 QByteArray &url, qint64 beginning_nanosec, qint64 end_nanosec);
void SetNextUrl(const QUrl &url, qint64 beginning_nanosec, qint64 end_nanosec); bool has_next_valid_url() const { return !next_url_.isNull() && !next_url_.isEmpty(); }
bool has_next_valid_url() const { return next_url_.isValid(); }
void SetSourceDevice(QString device) { source_device_ = device; }
// Get information about the music playback // Get information about the music playback
QUrl url() const { return url_; } QByteArray url() const { return url_; }
bool is_valid() const { return valid_; } bool is_valid() const { return valid_; }
// Please note that this method (unlike GstEngine's.position()) is // Please note that this method (unlike GstEngine's.position()) is multiple-section media unaware.
// multiple-section media unaware.
qint64 position() const; qint64 position() const;
// Please note that this method (unlike GstEngine's.length()) is // Please note that this method (unlike GstEngine's.length()) is multiple-section media unaware.
// multiple-section media unaware.
qint64 length() const; qint64 length() const;
// Returns this pipeline's state. May return GST_STATE_NULL if the state check // Returns this pipeline's state. May return GST_STATE_NULL if the state check timed out. The timeout value is a reasonable default.
// timed out. The timeout value is a reasonable default.
GstState state() const; GstState state() const;
qint64 segment_start() const { return segment_start_; } qint64 segment_start() const { return segment_start_; }
// Don't allow the user to change the playback state (playing/paused) while // Don't allow the user to change the playback state (playing/paused) while the pipeline is buffering.
// the pipeline is buffering.
bool is_buffering() const { return buffering_; } bool is_buffering() const { return buffering_; }
QUrl redirect_url() const { return redirect_url_; } QByteArray redirect_url() const { return redirect_url_; }
QString source_device() const { return source_device_; } QString source_device() const { return source_device_; }
@@ -125,16 +123,15 @@ signals:
void timerEvent(QTimerEvent*); void timerEvent(QTimerEvent*);
private: private:
// Static callbacks. The GstEnginePipeline instance is passed in the last // Static callbacks. The GstEnginePipeline instance is passed in the last argument.
// argument.
static GstBusSyncReply BusCallbackSync(GstBus*, GstMessage*, gpointer); static GstBusSyncReply BusCallbackSync(GstBus*, GstMessage*, gpointer);
static gboolean BusCallback(GstBus*, GstMessage*, gpointer); static gboolean BusCallback(GstBus*, GstMessage*, gpointer);
static void NewPadCallback(GstElement*, GstPad*, gpointer); static void NewPadCallback(GstElement*, GstPad*, gpointer);
static GstPadProbeReturn HandoffCallback(GstPad*, GstPadProbeInfo*, gpointer); static GstPadProbeReturn HandoffCallback(GstPad*, GstPadProbeInfo*, gpointer);
static GstPadProbeReturn EventHandoffCallback(GstPad*, GstPadProbeInfo*, gpointer); static GstPadProbeReturn EventHandoffCallback(GstPad*, GstPadProbeInfo*, gpointer);
static void AboutToFinishCallback(GstPlayBin*, gpointer);
static GstPadProbeReturn DecodebinProbe(GstPad*, GstPadProbeInfo*, gpointer); static GstPadProbeReturn DecodebinProbe(GstPad*, GstPadProbeInfo*, gpointer);
static void SourceDrainedCallback(GstURIDecodeBin*, gpointer); static void SourceSetupCallback(GstPlayBin*, GParamSpec* pspec, gpointer);
static void SourceSetupCallback(GstURIDecodeBin*, GParamSpec *pspec, gpointer);
static void TaskEnterCallback(GstTask*, GThread*, gpointer); static void TaskEnterCallback(GstTask*, GThread*, gpointer);
void TagMessageReceived(GstMessage*); void TagMessageReceived(GstMessage*);
@@ -143,23 +140,17 @@ signals:
void StateChangedMessageReceived(GstMessage*); void StateChangedMessageReceived(GstMessage*);
void BufferingMessageReceived(GstMessage*); void BufferingMessageReceived(GstMessage*);
void StreamStatusMessageReceived(GstMessage*); void StreamStatusMessageReceived(GstMessage*);
void StreamStartMessageReceived();
QString ParseTag(GstTagList *list, const char *tag) const; QString ParseTag(GstTagList *list, const char *tag) const;
bool Init(); bool InitDecodeBin(GstElement* new_bin);
bool InitAudioBin();
GstElement *CreateDecodeBinFromString(const char *pipeline); GstElement *CreateDecodeBinFromString(const char *pipeline);
void UpdateVolume(); void UpdateVolume();
void UpdateEqualizer(); void UpdateEqualizer();
void UpdateStereoBalance(); void UpdateStereoBalance();
bool ReplaceDecodeBin(GstElement *new_bin);
bool ReplaceDecodeBin(const QUrl &url);
void TransitionToNext();
// If the decodebin is special (ie. not really a uridecodebin) then it'll have
// a src pad immediately and we can link it after everything's created.
void MaybeLinkDecodeToAudio();
private slots: private slots:
void FaderTimelineFinished(); void FaderTimelineFinished();
@@ -174,11 +165,8 @@ signals:
GstEngine *engine_; GstEngine *engine_;
// Using == to compare two pipelines is a bad idea, because new ones often // Using == to compare two pipelines is a bad idea, because new ones often get created in the same address as old ones. This ID will be unique for each pipeline.
// get created in the same address as old ones. This ID will be unique for // Threading warning: access to the static ID field isn't protected by a mutex because all pipeline creation is currently done in the main thread.
// each pipeline.
// Threading warning: access to the static ID field isn't protected by a
// mutex because all pipeline creation is currently done in the main thread.
static int sId; static int sId;
int id_; int id_;
@@ -192,9 +180,6 @@ signals:
QMutex buffer_consumers_mutex_; QMutex buffer_consumers_mutex_;
qint64 segment_start_; qint64 segment_start_;
bool segment_start_received_; bool segment_start_received_;
bool emit_track_ended_on_stream_start_;
bool emit_track_ended_on_time_discontinuity_;
qint64 last_buffer_offset_;
// Equalizer // Equalizer
bool eq_enabled_; bool eq_enabled_;
@@ -220,8 +205,8 @@ signals:
bool mono_playback_; bool mono_playback_;
// The URL that is currently playing, and the URL that is to be preloaded when the current track is close to finishing. // The URL that is currently playing, and the URL that is to be preloaded when the current track is close to finishing.
QUrl url_; QByteArray url_;
QUrl next_url_; QByteArray next_url_;
// If this is > 0 then the pipeline will be forced to stop when playback goes past this position. // If this is > 0 then the pipeline will be forced to stop when playback goes past this position.
qint64 end_offset_nanosec_; qint64 end_offset_nanosec_;
@@ -237,7 +222,7 @@ signals:
bool ignore_tags_; bool ignore_tags_;
// When the gstreamer source requests a redirect we store the URL here and callers can pick it up after the state change to PLAYING fails. // When the gstreamer source requests a redirect we store the URL here and callers can pick it up after the state change to PLAYING fails.
QUrl redirect_url_; QByteArray redirect_url_;
// When we need to specify the device to use as source (for CD device) // When we need to specify the device to use as source (for CD device)
QString source_device_; QString source_device_;
@@ -248,12 +233,13 @@ signals:
qint64 pending_seek_nanosec_; qint64 pending_seek_nanosec_;
// We can only use gst_element_query_position() when the pipeline is in // We can only use gst_element_query_position() when the pipeline is in
// PAUSED nor PLAYING state. Whenever we get a new position (e.g. after a // PAUSED nor PLAYING state. Whenever we get a new position (e.g. after a correct call to gst_element_query_position() or after a seek), we store
// correct call to gst_element_query_position() or after a seek), we store // it here so that we can use it when using gst_element_query_position() is not possible.
// it here so that we can use it when using gst_element_query_position() is
// not possible.
mutable gint64 last_known_position_ns_; mutable gint64 last_known_position_ns_;
// Complete the transition to the next song when it starts playing
bool next_uri_set_;
int volume_percent_; int volume_percent_;
qreal volume_modifier_; qreal volume_modifier_;
@@ -263,9 +249,7 @@ signals:
GstElement *pipeline_; GstElement *pipeline_;
// Bins // The audiobin is either linked with a decodebin or set as sink of the playbin pipeline.
// uridecodebin ! audiobin
GstElement *uridecodebin_;
GstElement *audiobin_; GstElement *audiobin_;
// Elements in the audiobin. See comments in Init()'s definition. // Elements in the audiobin. See comments in Init()'s definition.