diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index c91e4fd6e..3995d5bf9 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -1306,7 +1306,7 @@ void MainWindow::Exit() { else { if (app_->player()->engine()->is_fadeout_enabled()) { // To shut down the application when fadeout will be finished - QObject::connect(&*app_->player()->engine(), &EngineBase::FadeoutFinishedSignal, this, &MainWindow::DoExit); + QObject::connect(&*app_->player()->engine(), &EngineBase::Finished, this, &MainWindow::DoExit); if (app_->player()->GetState() == EngineBase::State::Playing) { app_->player()->Stop(); ignore_close_ = true; diff --git a/src/core/mutex_protected.h b/src/core/mutex_protected.h new file mode 100644 index 000000000..8de7a09bd --- /dev/null +++ b/src/core/mutex_protected.h @@ -0,0 +1,65 @@ +/* + * Strawberry Music Player + * Copyright 2024, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef MUTEX_PROTECTED_H +#define MUTEX_PROTECTED_H + +#include + +#include +#include + +template +class mutex_protected : public boost::noncopyable { + public: + mutex_protected(const mutex_protected &value) : value_(value.value()) {} + mutex_protected(const T value) : value_(value) {} + ~mutex_protected() {} + + T value() const { + QMutexLocker l(&mutex_); + return value_; + } + + T operator==(const mutex_protected &value) const { + QMutexLocker l(&mutex_); + return value == value_; + } + + T operator==(const T value) const { + QMutexLocker l(&mutex_); + return value == value_; + } + + void operator=(const mutex_protected &value) { + QMutexLocker l(&mutex_); + value_ = value.value(); + } + + void operator=(const T value) { + QMutexLocker l(&mutex_); + value_ = value; + } + + private: + T value_; + mutable QMutex mutex_; +}; + +#endif // MUTEX_PROTECTED_H diff --git a/src/engine/enginebase.h b/src/engine/enginebase.h index 7c5614278..2fab9e9c5 100644 --- a/src/engine/enginebase.h +++ b/src/engine/enginebase.h @@ -168,8 +168,6 @@ class EngineBase : public QObject { void TrackEnded(); - void FadeoutFinishedSignal(); - void StatusText(const QString &text); void Error(const QString &text); @@ -188,6 +186,8 @@ class EngineBase : public QObject { void VolumeChanged(const uint volume); + void Finished(); + protected: bool exclusive_mode_; bool volume_control_; diff --git a/src/engine/gstengine.cpp b/src/engine/gstengine.cpp index 9662dbfbc..854a2255d 100644 --- a/src/engine/gstengine.cpp +++ b/src/engine/gstengine.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -604,33 +605,43 @@ void GstEngine::EndOfStreamReached(const int pipeline_id, const bool has_next_tr void GstEngine::HandlePipelineError(const int pipeline_id, const int domain, const int error_code, const QString &message, const QString &debugstr) { - if (!current_pipeline_ || current_pipeline_->id() != pipeline_id) return; - qLog(Error) << "GStreamer error:" << domain << error_code << message; - FinishPipeline(current_pipeline_); - current_pipeline_ = GstEnginePipelinePtr(); - - BufferingFinished(); - Q_EMIT StateChanged(State::Error); - - if ( - (domain == static_cast(GST_RESOURCE_ERROR) && ( - error_code == static_cast(GST_RESOURCE_ERROR_NOT_FOUND) || - error_code == static_cast(GST_RESOURCE_ERROR_OPEN_READ) || - error_code == static_cast(GST_RESOURCE_ERROR_NOT_AUTHORIZED) - )) - || (domain == static_cast(GST_STREAM_ERROR)) - ) { - Q_EMIT InvalidSongRequested(stream_url_); - } - else { - Q_EMIT FatalError(); - } - Q_EMIT Error(message); Q_EMIT Error(debugstr); + if (fadeout_pause_pipeline_ && pipeline_id == fadeout_pause_pipeline_->id()) { + StopFadeoutPause(); + } + + if (current_pipeline_ && current_pipeline_->id() == pipeline_id) { + + FinishPipeline(current_pipeline_); + current_pipeline_ = GstEnginePipelinePtr(); + + BufferingFinished(); + Q_EMIT StateChanged(State::Error); + + if ( + (domain == static_cast(GST_RESOURCE_ERROR) && ( + error_code == static_cast(GST_RESOURCE_ERROR_NOT_FOUND) || + error_code == static_cast(GST_RESOURCE_ERROR_OPEN_READ) || + error_code == static_cast(GST_RESOURCE_ERROR_NOT_AUTHORIZED) + )) + || (domain == static_cast(GST_STREAM_ERROR)) + ) { + Q_EMIT InvalidSongRequested(stream_url_); + } + else { + Q_EMIT FatalError(); + } + } + + else if (fadeout_pipelines_.contains(pipeline_id)) { + GstEnginePipelinePtr pipeline = fadeout_pipelines_.take(pipeline_id); + FinishPipeline(pipeline); + } + } void GstEngine::NewMetaData(const int pipeline_id, const EngineMetadata &engine_metadata) { @@ -663,22 +674,21 @@ void GstEngine::FadeoutFinished(const int pipeline_id) { return; } - GstEnginePipelinePtr pipeline = fadeout_pipelines_.value(pipeline_id); - fadeout_pipelines_.remove(pipeline_id); - FinishPipeline(pipeline); + GstEnginePipelinePtr pipeline = fadeout_pipelines_.take(pipeline_id); - Q_EMIT FadeoutFinishedSignal(); + FinishPipeline(pipeline); } void GstEngine::FadeoutPauseFinished() { + if (!fadeout_pause_pipeline_) return; + fadeout_pause_pipeline_->SetStateAsync(GST_STATE_PAUSED); Q_EMIT StateChanged(State::Paused); StopTimers(); has_faded_out_to_pause_ = true; fadeout_pause_pipeline_ = GstEnginePipelinePtr(); - Q_EMIT FadeoutFinishedSignal(); } @@ -703,26 +713,40 @@ void GstEngine::PlayDone(const GstStateChangeReturn ret, const bool pause, const if (ret == GST_STATE_CHANGE_FAILURE) { // Failure, but we got a redirection URL - try loading that instead - const QByteArray redirect_url = current_pipeline_->redirect_url(); - if (!redirect_url.isEmpty() && redirect_url != current_pipeline_->gst_url()) { + GstEnginePipelinePtr old_pipeline = current_pipeline_; + current_pipeline_ = GstEnginePipelinePtr(); + QByteArray redirect_url; + { + QMutexLocker l(old_pipeline->mutex_redirect_url()); + redirect_url = old_pipeline->redirect_url(); + redirect_url.detach(); + } + QByteArray gst_url; + { + QMutexLocker l(old_pipeline->mutex_url()); + gst_url = old_pipeline->gst_url(); + gst_url.detach(); + } + if (!redirect_url.isEmpty() && redirect_url != gst_url) { qLog(Info) << "Redirecting to" << redirect_url; - GstEnginePipelinePtr old_pipeline = current_pipeline_; - current_pipeline_ = GstEnginePipelinePtr(); - if (old_pipeline) { - FinishPipeline(old_pipeline); + QUrl media_url; + QUrl stream_url; + { + QMutexLocker l(old_pipeline->mutex_url()); + media_url = old_pipeline->media_url(); + media_url.detach(); + stream_url = old_pipeline->stream_url(); + stream_url.detach(); } - current_pipeline_ = CreatePipeline(current_pipeline_->media_url(), current_pipeline_->stream_url(), redirect_url, end_nanosec_, current_pipeline_->ebur128_loudness_normalizing_gain_db()); + current_pipeline_ = CreatePipeline(media_url, stream_url, redirect_url, end_nanosec_, old_pipeline->ebur128_loudness_normalizing_gain_db()); + FinishPipeline(old_pipeline); Play(pause, offset_nanosec); return; } // Failure - give up qLog(Warning) << "Could not set thread to PLAYING."; - GstEnginePipelinePtr old_pipeline = current_pipeline_; - current_pipeline_ = GstEnginePipelinePtr(); - if (old_pipeline) { - FinishPipeline(old_pipeline); - } + FinishPipeline(old_pipeline); BufferingFinished(); return; } @@ -806,7 +830,14 @@ void GstEngine::StartFadeout(GstEnginePipelinePtr pipeline) { return; } - QObject::disconnect(&*pipeline, nullptr, this, nullptr); + QObject::disconnect(&*pipeline, &GstEnginePipeline::FaderFinished, this, &GstEngine::FadeoutPauseFinished); + QObject::disconnect(&*pipeline, &GstEnginePipeline::EndOfStreamReached, this, &GstEngine::EndOfStreamReached); + QObject::disconnect(&*pipeline, &GstEnginePipeline::MetadataFound, this, &GstEngine::NewMetaData); + QObject::disconnect(&*pipeline, &GstEnginePipeline::BufferingStarted, this, &GstEngine::BufferingStarted); + QObject::disconnect(&*pipeline, &GstEnginePipeline::BufferingProgress, this, &GstEngine::BufferingProgress); + QObject::disconnect(&*pipeline, &GstEnginePipeline::BufferingFinished, this, &GstEngine::BufferingFinished); + QObject::disconnect(&*pipeline, &GstEnginePipeline::VolumeChanged, this, &EngineBase::UpdateVolume); + QObject::disconnect(&*pipeline, &GstEnginePipeline::AboutToFinish, this, &EngineBase::EmitAboutToFinish); fadeout_pipelines_.insert(pipeline->id(), pipeline); pipeline->RemoveAllBufferConsumers(); @@ -954,6 +985,10 @@ void GstEngine::PipelineFinished(const int pipeline_id) { qLog(Debug) << (current_pipeline_ ? 1 : 0) + old_pipelines_.count() << "pipelines are active"; + if (!current_pipeline_ && old_pipelines_.isEmpty()) { + Q_EMIT Finished(); + } + } void GstEngine::UpdateScope(const int chunk_length) { @@ -1038,15 +1073,23 @@ void GstEngine::StreamDiscovered(GstDiscoverer*, GstDiscovererInfo *info, GError GstDiscovererStreamInfo *stream_info = reinterpret_cast(g_list_first(audio_streams)->data); EngineMetadata engine_metadata; - if (discovered_url == instance->current_pipeline_->gst_url()) { - engine_metadata.type = EngineMetadata::Type::Current; - engine_metadata.media_url = instance->current_pipeline_->media_url(); - engine_metadata.stream_url = instance->current_pipeline_->stream_url(); + bool match = false; + { + QMutexLocker l(instance->current_pipeline_->mutex_url()); + if (discovered_url == instance->current_pipeline_->gst_url()) { + match = true; + engine_metadata.type = EngineMetadata::Type::Current; + engine_metadata.media_url = instance->current_pipeline_->media_url(); + engine_metadata.stream_url = instance->current_pipeline_->stream_url(); + } } - else if (discovered_url == instance->current_pipeline_->next_gst_url()) { - engine_metadata.type = EngineMetadata::Type::Next; - engine_metadata.media_url = instance->current_pipeline_->next_media_url(); - engine_metadata.stream_url = instance->current_pipeline_->next_stream_url(); + if (!match) { + QMutexLocker l(instance->current_pipeline_->mutex_next_url()); + if (discovered_url == instance->current_pipeline_->next_gst_url()) { + engine_metadata.type = EngineMetadata::Type::Next; + engine_metadata.media_url = instance->current_pipeline_->next_media_url(); + engine_metadata.stream_url = instance->current_pipeline_->next_stream_url(); + } } engine_metadata.samplerate = static_cast(gst_discoverer_audio_info_get_sample_rate(GST_DISCOVERER_AUDIO_INFO(stream_info))); engine_metadata.bitdepth = static_cast(gst_discoverer_audio_info_get_depth(GST_DISCOVERER_AUDIO_INFO(stream_info))); diff --git a/src/engine/gstenginepipeline.cpp b/src/engine/gstenginepipeline.cpp index 5f5a205ec..ad35a7b6e 100644 --- a/src/engine/gstenginepipeline.cpp +++ b/src/engine/gstenginepipeline.cpp @@ -45,6 +45,7 @@ #include #include #include +#include #include #include #include @@ -87,7 +88,6 @@ int GstEnginePipeline::sId = 1; GstEnginePipeline::GstEnginePipeline(QObject *parent) : QObject(parent), id_(sId++), - valid_(false), exclusive_mode_(false), volume_enabled_(true), fading_enabled_(false), @@ -119,6 +119,7 @@ GstEnginePipeline::GstEnginePipeline(QObject *parent) ignore_tags_(false), pipeline_connected_(false), pipeline_active_(false), + buffering_(false), pending_state_(GST_STATE_NULL), pending_seek_nanosec_(-1), last_known_position_ns_(0), @@ -127,7 +128,7 @@ GstEnginePipeline::GstEnginePipeline(QObject *parent) volume_set_(false), volume_internal_(-1.0), volume_percent_(100), - buffering_(false), + fader_active_(false), use_fudge_timer_(false), pipeline_(nullptr), audiobin_(nullptr), @@ -171,13 +172,13 @@ GstEnginePipeline::~GstEnginePipeline() { } gst_object_unref(GST_OBJECT(pipeline_)); pipeline_ = nullptr; - if (audiobin_ && !pipeline_connected_) { + if (audiobin_ && !pipeline_connected_.value()) { gst_object_unref(GST_OBJECT(audiobin_)); } audiobin_ = nullptr; } - qLog(Debug) << "Pipeline" << id_ << "deleted"; + qLog(Debug) << "Pipeline" << id() << "deleted"; } @@ -238,10 +239,13 @@ void GstEnginePipeline::set_buffer_high_watermark(const double value) { } void GstEnginePipeline::set_proxy_settings(const QString &address, const bool authentication, const QString &user, const QString &pass) { + + QMutexLocker l(&mutex_proxy_); proxy_address_ = address; proxy_authentication_ = authentication; proxy_user_ = user; proxy_pass_ = pass; + } void GstEnginePipeline::set_channels(const bool enabled, const int channels) { @@ -268,6 +272,7 @@ void GstEnginePipeline::set_spotify_login(const QString &spotify_username, const spotify_password_ = spotify_password; } + #endif // HAVE_SPOTIFY QString GstEnginePipeline::GstStateText(const GstState state) { @@ -291,7 +296,7 @@ QString GstEnginePipeline::GstStateText(const GstState state) { GstElement *GstEnginePipeline::CreateElement(const QString &factory_name, const QString &name, GstElement *bin, QString &error) const { - QString unique_name = QLatin1String("pipeline") + QLatin1Char('-') + QString::number(id_) + QLatin1Char('-') + (name.isEmpty() ? factory_name : name); + QString unique_name = QLatin1String("pipeline") + QLatin1Char('-') + QString::number(id()) + QLatin1Char('-') + (name.isEmpty() ? factory_name : name); GstElement *element = gst_element_factory_make(factory_name.toUtf8().constData(), unique_name.toUtf8().constData()); if (!element) { @@ -310,6 +315,7 @@ void GstEnginePipeline::Disconnect() { if (pipeline_) { if (fader_) { + fader_active_ = false; if (fader_->state() != QTimeLine::NotRunning) { fader_->stop(); } @@ -379,7 +385,7 @@ void GstEnginePipeline::Disconnect() { bool GstEnginePipeline::Finish() { - qLog(Debug) << "Finishing pipeline" << id_; + qLog(Debug) << "Finishing pipeline" << id(); finish_requested_ = true; @@ -392,15 +398,19 @@ bool GstEnginePipeline::Finish() { SetStateAsync(GST_STATE_NULL); } - return finished_; + return finished_.value(); } bool GstEnginePipeline::InitFromUrl(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 end_nanosec, const double ebur128_loudness_normalizing_gain_db, QString &error) { - media_url_ = media_url; - stream_url_ = stream_url; - gst_url_ = gst_url; + { + QMutexLocker l(&mutex_url_); + media_url_ = media_url; + stream_url_ = stream_url; + gst_url_ = gst_url; + } + end_offset_nanosec_ = end_nanosec; ebur128_loudness_normalizing_gain_db_ = ebur128_loudness_normalizing_gain_db; @@ -448,7 +458,10 @@ bool GstEnginePipeline::InitFromUrl(const QUrl &media_url, const QUrl &stream_ur flags &= ~GST_PLAY_FLAG_SOFT_VOLUME; g_object_set(G_OBJECT(pipeline_), "flags", flags, nullptr); - g_object_set(G_OBJECT(pipeline_), "uri", gst_url.constData(), nullptr); + { + QMutexLocker l(&mutex_url_); + g_object_set(G_OBJECT(pipeline_), "uri", gst_url.constData(), nullptr); + } pipeline_connected_ = true; @@ -968,7 +981,7 @@ GstPadProbeReturn GstEnginePipeline::UpstreamEventsProbeCallback(GstPad *pad, Gs switch (GST_EVENT_TYPE(e)) { case GST_EVENT_SEGMENT: - if (!instance->segment_start_received_) { + if (!instance->segment_start_received_.value()) { // The segment start time is used to calculate the proper offset of data buffers from the start of the stream const GstSegment *segment = nullptr; gst_event_parse_segment(e, &segment); @@ -1008,7 +1021,7 @@ void GstEnginePipeline::ElementAddedCallback(GstBin *bin, GstBin *sub_bin, GstEl } instance->SetupVolume(volume); - instance->SetVolume(instance->volume_percent_); + instance->SetVolume(instance->volume_percent_.value()); } @@ -1036,11 +1049,14 @@ void GstEnginePipeline::SourceSetupCallback(GstElement *playbin, GstElement *sou GstEnginePipeline *instance = reinterpret_cast(self); - if (g_object_class_find_property(G_OBJECT_GET_CLASS(source), "device") && !instance->source_device().isEmpty()) { - // Gstreamer is not able to handle device in URL (referring to Gstreamer documentation, this might be added in the future). - // Despite that, for now we include device inside URL: we decompose it during Init and set device here, when this callback is called. - qLog(Debug) << "Setting device"; - g_object_set(source, "device", instance->source_device().toLocal8Bit().constData(), nullptr); + { + QMutexLocker l(&instance->mutex_source_device_); + if (g_object_class_find_property(G_OBJECT_GET_CLASS(source), "device") && !instance->source_device().isEmpty()) { + // Gstreamer is not able to handle device in URL (referring to Gstreamer documentation, this might be added in the future). + // Despite that, for now we include device inside URL: we decompose it during Init and set device here, when this callback is called. + qLog(Debug) << "Setting device"; + g_object_set(source, "device", instance->source_device().toLocal8Bit().constData(), nullptr); + } } if (g_object_class_find_property(G_OBJECT_GET_CLASS(source), "user-agent")) { @@ -1050,40 +1066,46 @@ void GstEnginePipeline::SourceSetupCallback(GstElement *playbin, GstElement *sou } if (g_object_class_find_property(G_OBJECT_GET_CLASS(source), "ssl-strict")) { - qLog(Debug) << "Turning" << (instance->strict_ssl_enabled_ ? "on" : "off") << "strict SSL"; - g_object_set(source, "ssl-strict", instance->strict_ssl_enabled_ ? TRUE : FALSE, nullptr); + qLog(Debug) << "Turning" << (instance->strict_ssl_enabled_.value() ? "on" : "off") << "strict SSL"; + g_object_set(source, "ssl-strict", instance->strict_ssl_enabled_.value() ? TRUE : FALSE, nullptr); } - if (!instance->proxy_address_.isEmpty() && g_object_class_find_property(G_OBJECT_GET_CLASS(source), "proxy")) { - qLog(Debug) << "Setting proxy to" << instance->proxy_address_; - g_object_set(source, "proxy", instance->proxy_address_.toUtf8().constData(), nullptr); - if (instance->proxy_authentication_ && - g_object_class_find_property(G_OBJECT_GET_CLASS(source), "proxy-id") && - g_object_class_find_property(G_OBJECT_GET_CLASS(source), "proxy-pw") && - !instance->proxy_user_.isEmpty() && - !instance->proxy_pass_.isEmpty()) - { - g_object_set(source, "proxy-id", instance->proxy_user_.toUtf8().constData(), "proxy-pw", instance->proxy_pass_.toUtf8().constData(), nullptr); + { + QMutexLocker l(&instance->mutex_proxy_); + if (!instance->proxy_address_.isEmpty() && g_object_class_find_property(G_OBJECT_GET_CLASS(source), "proxy")) { + qLog(Debug) << "Setting proxy to" << instance->proxy_address_; + g_object_set(source, "proxy", instance->proxy_address_.toUtf8().constData(), nullptr); + if (instance->proxy_authentication_ && + g_object_class_find_property(G_OBJECT_GET_CLASS(source), "proxy-id") && + g_object_class_find_property(G_OBJECT_GET_CLASS(source), "proxy-pw") && + !instance->proxy_user_.isEmpty() && + !instance->proxy_pass_.isEmpty()) + { + g_object_set(source, "proxy-id", instance->proxy_user_.toUtf8().constData(), "proxy-pw", instance->proxy_pass_.toUtf8().constData(), nullptr); + } } } #ifdef HAVE_SPOTIFY - if (instance->media_url_.scheme() == QStringLiteral("spotify")) { - if (g_object_class_find_property(G_OBJECT_GET_CLASS(source), "bitrate")) { - g_object_set(source, "bitrate", 2, nullptr); - } - if (!instance->spotify_username_.isEmpty() && - !instance->spotify_password_.isEmpty() && - g_object_class_find_property(G_OBJECT_GET_CLASS(source), "username") && - g_object_class_find_property(G_OBJECT_GET_CLASS(source), "password")) { - g_object_set(source, "username", instance->spotify_username_.toUtf8().constData(), nullptr); - g_object_set(source, "password", instance->spotify_password_.toUtf8().constData(), nullptr); + { + QMutexLocker l(&instance->mutex_url_); + if (instance->media_url_.scheme() == QStringLiteral("spotify")) { + if (g_object_class_find_property(G_OBJECT_GET_CLASS(source), "bitrate")) { + g_object_set(source, "bitrate", 2, nullptr); + } + if (!instance->spotify_username_.isEmpty() && + !instance->spotify_password_.isEmpty() && + g_object_class_find_property(G_OBJECT_GET_CLASS(source), "username") && + g_object_class_find_property(G_OBJECT_GET_CLASS(source), "password")) { + g_object_set(source, "username", instance->spotify_username_.toUtf8().constData(), nullptr); + g_object_set(source, "password", instance->spotify_password_.toUtf8().constData(), nullptr); + } } } #endif // If the pipeline was buffering we stop that now. - if (instance->buffering_) { + if (instance->buffering_.value()) { qLog(Debug) << "Buffering finished"; instance->buffering_ = false; Q_EMIT instance->BufferingFinished(); @@ -1099,12 +1121,13 @@ void GstEnginePipeline::NotifyVolumeCallback(GstElement *element, GParamSpec *pa GstEnginePipeline *instance = reinterpret_cast(self); - if (!instance->volume_set_) return; + if (!instance->volume_set_.value()) return; - g_object_get(G_OBJECT(instance->volume_), "volume", &instance->volume_internal_, nullptr); + const double volume_internal = instance->volume_internal_.value(); + g_object_get(G_OBJECT(instance->volume_), "volume", &volume_internal, nullptr); - const uint volume_percent = static_cast(qBound(0L, lround(instance->volume_internal_ / 0.01), 100L)); - if (volume_percent != instance->volume_percent_) { + const uint volume_percent = static_cast(qBound(0L, lround(instance->volume_internal_.value() / 0.01), 100L)); + if (volume_percent != instance->volume_percent_.value()) { instance->volume_percent_ = volume_percent; Q_EMIT instance->VolumeChanged(volume_percent); } @@ -1137,8 +1160,8 @@ void GstEnginePipeline::PadAddedCallback(GstElement *element, GstPad *pad, gpoin instance->pad_probe_cb_id_ = gst_pad_add_probe(pad, static_cast(GST_PAD_PROBE_TYPE_BUFFER | GST_PAD_PROBE_TYPE_EVENT_DOWNSTREAM | GST_PAD_PROBE_TYPE_EVENT_FLUSH), PadProbeCallback, instance, nullptr); instance->pipeline_connected_ = true; - if (instance->pending_seek_nanosec_ != -1 && instance->pipeline_active_) { - QMetaObject::invokeMethod(instance, "Seek", Qt::QueuedConnection, Q_ARG(qint64, instance->pending_seek_nanosec_)); + if (instance->pending_seek_nanosec_.value() != -1 && instance->pipeline_active_.value()) { + QMetaObject::invokeMethod(instance, "Seek", Qt::QueuedConnection, Q_ARG(qint64, instance->pending_seek_nanosec_.value())); instance->pending_seek_nanosec_ = -1; } @@ -1206,7 +1229,7 @@ GstPadProbeReturn GstEnginePipeline::BufferProbeCallback(GstPad *pad, GstPadProb GstBuffer *buf = gst_pad_probe_info_get_buffer(info); GstBuffer *buf16 = nullptr; - quint64 start_time = GST_BUFFER_TIMESTAMP(buf) - instance->segment_start_; + quint64 start_time = GST_BUFFER_TIMESTAMP(buf) - instance->segment_start_.value(); quint64 duration = GST_BUFFER_DURATION(buf); qint64 end_time = static_cast(start_time + duration); @@ -1324,19 +1347,23 @@ GstPadProbeReturn GstEnginePipeline::BufferProbeCallback(GstPad *pad, GstPadProb } // 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 && end_time > instance->end_offset_nanosec_) { - if (instance->has_next_valid_url() && instance->next_stream_url_ == instance->stream_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_media_url_.clear(); - instance->next_stream_url_.clear(); - instance->next_gst_url_.clear(); - instance->next_beginning_offset_nanosec_ = 0; - instance->next_end_offset_nanosec_ = 0; + if (instance->end_offset_nanosec_.value() > 0 && end_time > instance->end_offset_nanosec_.value()) { + if (instance->HasNextUrl()) { + QMutexLocker mutex_locker_url(&instance->mutex_url_); + QMutexLocker mutex_locker_next_url(&instance->mutex_next_url_); + if (instance->next_stream_url_ == instance->stream_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_media_url_.clear(); + instance->next_stream_url_.clear(); + instance->next_gst_url_.clear(); + 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; - Q_EMIT instance->EndOfStreamReached(instance->id(), true); + // 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; + Q_EMIT instance->EndOfStreamReached(instance->id(), true); + } } else { // There's no next song @@ -1354,11 +1381,14 @@ void GstEnginePipeline::AboutToFinishCallback(GstPlayBin *playbin, gpointer self GstEnginePipeline *instance = reinterpret_cast(self); - qLog(Debug) << "Stream from URL" << instance->gst_url_ << "about to finish."; + { + QMutexLocker l(&instance->mutex_url_); + qLog(Debug) << "Stream from URL" << instance->gst_url_ << "about to finish."; + } instance->about_to_finish_ = true; - if (instance->has_next_valid_url() && !instance->next_uri_set_) { + if (instance->HasNextUrl() && !instance->next_uri_set_.value()) { instance->SetNextUrl(); } @@ -1458,18 +1488,22 @@ void GstEnginePipeline::StreamStatusMessageReceived(GstMessage *msg) { void GstEnginePipeline::StreamStartMessageReceived() { - if (next_uri_set_) { - qLog(Debug) << "Stream changed from URL" << gst_url_ << "to" << next_gst_url_; + if (next_uri_set_.value()) { next_uri_set_ = false; next_uri_reset_ = false; about_to_finish_ = false; - media_url_ = next_media_url_; - stream_url_ = next_stream_url_; - gst_url_ = next_gst_url_; + { + QMutexLocker lock_url(&mutex_url_); + QMutexLocker lock_next_url(&mutex_next_url_); + qLog(Debug) << "Stream changed from URL" << gst_url_ << "to" << next_gst_url_; + media_url_ = next_media_url_; + stream_url_ = next_stream_url_; + gst_url_ = next_gst_url_; + next_stream_url_.clear(); + next_media_url_.clear(); + next_gst_url_.clear(); + } end_offset_nanosec_ = next_end_offset_nanosec_; - next_stream_url_.clear(); - next_media_url_.clear(); - next_gst_url_.clear(); next_beginning_offset_nanosec_ = 0; next_end_offset_nanosec_ = 0; @@ -1505,6 +1539,7 @@ void GstEnginePipeline::ElementMessageReceived(GstMessage *msg) { const char *uri = gst_structure_get_string(structure, "new-location"); // 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. + QMutexLocker l(&mutex_redirect_url_); redirect_url_ = uri; } @@ -1523,7 +1558,7 @@ void GstEnginePipeline::ErrorMessageReceived(GstMessage *msg) { g_error_free(error); g_free(debugs); - if (pipeline_active_ && next_uri_set_ && (domain == GST_CORE_ERROR || domain == GST_RESOURCE_ERROR || domain == GST_STREAM_ERROR)) { + if (pipeline_active_.value() && next_uri_set_.value() && (domain == GST_CORE_ERROR || 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(Info) << "Ignoring error" << domain << code << message << debugstr << "when loading next track"; @@ -1536,10 +1571,13 @@ void GstEnginePipeline::ErrorMessageReceived(GstMessage *msg) { qLog(Error) << __FUNCTION__ << "ID:" << id() << "Domain:" << domain << "Code:" << code << "Error:" << message; qLog(Error) << __FUNCTION__ << "ID:" << id() << "Domain:" << domain << "Code:" << code << "Debug:" << debugstr; - if (!redirect_url_.isEmpty() && debugstr.contains(QLatin1String("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. - // We handle the message, but now we have to ignore the error too. - return; + { + QMutexLocker l(&mutex_redirect_url_); + if (!redirect_url_.isEmpty() && debugstr.contains(QLatin1String("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. + // We handle the message, but now we have to ignore the error too. + return; + } } #ifdef Q_OS_WIN @@ -1555,15 +1593,18 @@ void GstEnginePipeline::ErrorMessageReceived(GstMessage *msg) { void GstEnginePipeline::TagMessageReceived(GstMessage *msg) { - if (ignore_tags_) return; + if (ignore_tags_.value()) return; GstTagList *taglist = nullptr; gst_message_parse_tag(msg, &taglist); EngineMetadata engine_metadata; engine_metadata.type = EngineMetadata::Type::Current; - engine_metadata.media_url = media_url_; - engine_metadata.stream_url = stream_url_; + { + QMutexLocker l(&mutex_url_); + engine_metadata.media_url = media_url_; + engine_metadata.stream_url = stream_url_; + } engine_metadata.title = ParseStrTag(taglist, GST_TAG_TITLE); engine_metadata.artist = ParseStrTag(taglist, GST_TAG_ARTIST); engine_metadata.comment = ParseStrTag(taglist, GST_TAG_COMMENT); @@ -1643,35 +1684,38 @@ void GstEnginePipeline::StateChangedMessageReceived(GstMessage *msg) { qLog(Debug) << "Pipeline state changed from" << GstStateText(old_state) << "to" << GstStateText(new_state); - if (!pipeline_active_ && (new_state == GST_STATE_PAUSED || new_state == GST_STATE_PLAYING)) { + if (!pipeline_active_.value() && (new_state == GST_STATE_PAUSED || new_state == GST_STATE_PLAYING)) { qLog(Debug) << "Pipeline is active"; pipeline_active_ = true; - if (pipeline_connected_) { - if (!volume_set_) { - SetVolume(volume_percent_); + if (pipeline_connected_.value()) { + if (!volume_set_.value()) { + SetVolume(volume_percent_.value()); } - if (pending_seek_nanosec_ != -1) { - if (next_uri_reset_ && new_state == GST_STATE_PAUSED) { + if (pending_seek_nanosec_.value() != -1) { + if (next_uri_reset_.value() && new_state == GST_STATE_PAUSED) { qLog(Debug) << "Reverting next uri and going to playing state."; next_uri_reset_ = false; pending_state_ = GST_STATE_PLAYING; - SeekDelayed(pending_seek_nanosec_); + SeekDelayed(pending_seek_nanosec_.value()); pending_seek_nanosec_ = -1; } else { - SeekAsync(pending_seek_nanosec_); + SeekAsync(pending_seek_nanosec_.value()); pending_seek_nanosec_ = -1; } } } } - else if (pipeline_active_ && new_state != GST_STATE_PAUSED && new_state != GST_STATE_PLAYING) { + else if (pipeline_active_.value() && new_state != GST_STATE_PAUSED && new_state != GST_STATE_PLAYING) { qLog(Debug) << "Pipeline is inactive"; pipeline_active_ = false; - if (next_uri_set_ && new_state == GST_STATE_READY) { + if (next_uri_set_.value() && new_state == GST_STATE_READY) { next_uri_set_ = false; - g_object_set(G_OBJECT(pipeline_), "uri", gst_url_.constData(), nullptr); + { + QMutexLocker l(&mutex_url_); + g_object_set(G_OBJECT(pipeline_), "uri", gst_url_.constData(), nullptr); + } if (pending_seek_nanosec_ == -1) { qLog(Debug) << "Reverting next uri and going to playing state."; SetStateAsync(GST_STATE_PLAYING); @@ -1684,13 +1728,13 @@ void GstEnginePipeline::StateChangedMessageReceived(GstMessage *msg) { } } - if (pipeline_active_ && !buffering_) { - if (pending_seek_nanosec_ != -1 && new_state == GST_STATE_PAUSED) { - SeekAsync(pending_seek_nanosec_); + if (pipeline_active_.value() && !buffering_.value()) { + if (pending_seek_nanosec_.value() != -1 && new_state == GST_STATE_PAUSED) { + SeekAsync(pending_seek_nanosec_.value()); pending_seek_nanosec_ = -1; } - else if (pending_state_ != GST_STATE_NULL) { - SetStateAsync(pending_state_); + else if (pending_state_.value() != GST_STATE_NULL) { + SetStateAsync(pending_state_.value()); pending_state_ = GST_STATE_NULL; } if (fader_ && fader_->state() != QTimeLine::State::Running && new_state == GST_STATE_PLAYING) { @@ -1699,6 +1743,11 @@ void GstEnginePipeline::StateChangedMessageReceived(GstMessage *msg) { } } + if (new_state == GST_STATE_NULL && !finished_.value() && finish_requested_.value()) { + finished_ = true; + Q_EMIT Finished(); + } + } void GstEnginePipeline::BufferingMessageReceived(GstMessage *msg) { @@ -1713,58 +1762,36 @@ void GstEnginePipeline::BufferingMessageReceived(GstMessage *msg) { const GstState current_state = state(); - if (percent < 100 && !buffering_) { + if (percent < 100 && !buffering_.value()) { qLog(Debug) << "Buffering started"; buffering_ = true; Q_EMIT BufferingStarted(); if (current_state == GST_STATE_PLAYING) { SetStateAsync(GST_STATE_PAUSED); - if (pending_state_ == GST_STATE_NULL) { + if (pending_state_.value() == GST_STATE_NULL) { pending_state_ = current_state; } } } - else if (percent == 100 && buffering_) { + else if (percent == 100 && buffering_.value()) { qLog(Debug) << "Buffering finished"; buffering_ = false; Q_EMIT BufferingFinished(); - if (pending_seek_nanosec_ != -1) { - SeekAsync(pending_seek_nanosec_); + if (pending_seek_nanosec_.value() != -1) { + SeekAsync(pending_seek_nanosec_.value()); pending_seek_nanosec_ = -1; } - else if (pending_state_ != GST_STATE_NULL) { - SetStateAsync(pending_state_); + else if (pending_state_.value() != GST_STATE_NULL) { + SetStateAsync(pending_state_.value()); pending_state_ = GST_STATE_NULL; } } - else if (buffering_) { + else if (buffering_.value()) { Q_EMIT BufferingProgress(percent); } } -qint64 GstEnginePipeline::position() const { - - if (pipeline_active_) { - gint64 current_position = 0; - if (gst_element_query_position(pipeline_, GST_FORMAT_TIME, ¤t_position)) { - last_known_position_ns_ = current_position; - } - } - - return last_known_position_ns_; - -} - -qint64 GstEnginePipeline::length() const { - - gint64 value = 0; - if (pipeline_) gst_element_query_duration(pipeline_, GST_FORMAT_TIME, &value); - - return value; - -} - GstState GstEnginePipeline::state() const { GstState s = GST_STATE_NULL, sp = GST_STATE_NULL; @@ -1776,15 +1803,37 @@ GstState GstEnginePipeline::state() const { } +qint64 GstEnginePipeline::length() const { + + gint64 value = 0; + if (pipeline_) gst_element_query_duration(pipeline_, GST_FORMAT_TIME, &value); + + return value; + +} + +qint64 GstEnginePipeline::position() const { + + if (pipeline_active_.value()) { + gint64 current_position = 0; + if (gst_element_query_position(pipeline_, GST_FORMAT_TIME, ¤t_position)) { + last_known_position_ns_ = current_position; + } + } + + return last_known_position_ns_; + +} + QFuture GstEnginePipeline::SetStateAsync(const GstState state) { - qLog(Debug) << "Setting pipeline" << id_ << "state to" << GstStateText(state); + qLog(Debug) << "Setting pipeline" << id() << "state to" << GstStateText(state); QFutureWatcher *watcher = new QFutureWatcher(); QObject::connect(watcher, &QFutureWatcher::finished, this, [this, watcher, state]() { - const GstStateChangeReturn state_change = watcher->result(); + const GstStateChangeReturn state_change_return = watcher->result(); watcher->deleteLater(); - SetStateAsyncFinished(state, state_change); + SetStateAsyncFinished(state, state_change_return); }); QFuture future = QtConcurrent::run(&set_state_threadpool_, &gst_element_set_state, pipeline_, state); watcher->setFuture(future); @@ -1793,15 +1842,15 @@ QFuture GstEnginePipeline::SetStateAsync(const GstState st } -void GstEnginePipeline::SetStateAsyncFinished(const GstState state, const GstStateChangeReturn state_change) { +void GstEnginePipeline::SetStateAsyncFinished(const GstState state, const GstStateChangeReturn state_change_return) { - switch (state_change) { + switch (state_change_return) { case GST_STATE_CHANGE_SUCCESS: case GST_STATE_CHANGE_ASYNC: case GST_STATE_CHANGE_NO_PREROLL: - qLog(Debug) << "Pipeline" << id_ << "state successfully set to" << GstStateText(state); - Q_EMIT SetStateFinished(state_change); - if (!finished_ && finish_requested_) { + qLog(Debug) << "Pipeline" << id() << "state successfully set to" << GstStateText(state); + Q_EMIT SetStateFinished(state_change_return); + if (!finished_.value() && finish_requested_.value()) { finished_ = true; Q_EMIT Finished(); } @@ -1829,17 +1878,17 @@ QFuture GstEnginePipeline::Play(const bool pause, const qu bool GstEnginePipeline::Seek(const qint64 nanosec) { - if (ignore_next_seek_) { + if (ignore_next_seek_.value()) { ignore_next_seek_ = false; return true; } - if (!pipeline_connected_ || !pipeline_active_) { + if (!pipeline_connected_.value() || !pipeline_active_.value()) { pending_seek_nanosec_ = nanosec; return true; } - if (next_uri_set_) { + if (next_uri_set_.value()) { pending_seek_nanosec_ = nanosec; SetStateAsync(GST_STATE_READY); return true; @@ -1854,9 +1903,9 @@ bool GstEnginePipeline::Seek(const qint64 nanosec) { if (success) { qLog(Debug) << "Seek succeeded"; - if (pending_state_ != GST_STATE_NULL) { - qLog(Debug) << "Setting state from pending state" << GstStateText(pending_state_); - SetStateAsync(pending_state_); + if (pending_state_.value() != GST_STATE_NULL) { + qLog(Debug) << "Setting state from pending state" << GstStateText(pending_state_.value()); + SetStateAsync(pending_state_.value()); pending_state_ = GST_STATE_NULL; } } @@ -1879,31 +1928,14 @@ void GstEnginePipeline::SeekDelayed(const qint64 nanosec) { } -void GstEnginePipeline::SetEBUR128LoudnessNormalizingGain_dB(const double ebur128_loudness_normalizing_gain_db) { - - ebur128_loudness_normalizing_gain_db_ = ebur128_loudness_normalizing_gain_db; - UpdateEBUR128LoudnessNormalizingGaindB(); - -} - -void GstEnginePipeline::UpdateEBUR128LoudnessNormalizingGaindB() { - - if (volume_ebur128_) { - auto dB_to_mult = [](const double gain_dB) { return std::pow(10., gain_dB / 20.); }; - - g_object_set(G_OBJECT(volume_ebur128_), "volume", dB_to_mult(ebur128_loudness_normalizing_gain_db_), nullptr); - } - -} - void GstEnginePipeline::SetVolume(const uint volume_percent) { if (volume_) { const double volume_internal = static_cast(volume_percent) * 0.01; - if (!volume_set_ || volume_internal != volume_internal_) { + if (!volume_set_.value() || volume_internal != volume_internal_.value()) { volume_internal_ = volume_internal; g_object_set(G_OBJECT(volume_), "volume", volume_internal, nullptr); - if (pipeline_active_) { + if (pipeline_active_.value()) { volume_set_ = true; } } @@ -1913,14 +1945,6 @@ void GstEnginePipeline::SetVolume(const uint volume_percent) { } -void GstEnginePipeline::SetFaderVolume(const qreal volume) { - - if (volume_fading_) { - g_object_set(G_OBJECT(volume_fading_), "volume", volume, nullptr); - } - -} - void GstEnginePipeline::SetStereoBalance(const float value) { stereo_balance_ = value; @@ -1973,8 +1997,27 @@ void GstEnginePipeline::UpdateEqualizer() { } +void GstEnginePipeline::SetEBUR128LoudnessNormalizingGain_dB(const double ebur128_loudness_normalizing_gain_db) { + + ebur128_loudness_normalizing_gain_db_ = ebur128_loudness_normalizing_gain_db; + UpdateEBUR128LoudnessNormalizingGaindB(); + +} + +void GstEnginePipeline::UpdateEBUR128LoudnessNormalizingGaindB() { + + if (volume_ebur128_) { + auto dB_to_mult = [](const double gain_dB) { return std::pow(10., gain_dB / 20.); }; + + g_object_set(G_OBJECT(volume_ebur128_), "volume", dB_to_mult(ebur128_loudness_normalizing_gain_db_), nullptr); + } + +} + void GstEnginePipeline::StartFader(const qint64 duration_nanosec, const QTimeLine::Direction direction, const QEasingCurve::Type shape, const bool use_fudge_timer) { + fader_active_ = true; + const qint64 duration_msec = duration_nanosec / kNsecPerMsec; // If there's already another fader running then start from the same time that one was already at. @@ -2007,17 +2050,25 @@ void GstEnginePipeline::StartFader(const qint64 duration_nanosec, const QTimeLin SetFaderVolume(fader_->currentValue()); - qLog(Debug) << "Pipeline" << id_ << "with state" << GstStateText(state()) << "set to fade from" << start_time; + qLog(Debug) << "Pipeline" << id() << "with state" << GstStateText(state()) << "set to fade from" << start_time; - if (pipeline_active_) { + if (pipeline_active_.value()) { fader_->resume(); } } +void GstEnginePipeline::SetFaderVolume(const qreal volume) { + + if (volume_fading_) { + g_object_set(G_OBJECT(volume_fading_), "volume", volume, nullptr); + } + +} + void GstEnginePipeline::ResumeFaderAsync() { - if (fader_) { + if (fader_active_.value()) { QMetaObject::invokeMethod(&*fader_, "resume", Qt::QueuedConnection); } @@ -2025,8 +2076,9 @@ void GstEnginePipeline::ResumeFaderAsync() { void GstEnginePipeline::FaderTimelineFinished() { - qLog(Debug) << "Pipeline" << id_ << "finished fading"; + qLog(Debug) << "Pipeline" << id() << "finished fading"; + fader_active_ = false; fader_.reset(); // Wait a little while longer before emitting the finished signal (and probably destroying the pipeline) to account for delays in the audio server/driver. @@ -2046,7 +2098,7 @@ void GstEnginePipeline::timerEvent(QTimerEvent *e) { if (e->timerId() == fader_fudge_timer_.timerId()) { fader_fudge_timer_.stop(); - Q_EMIT FaderFinished(id_); + Q_EMIT FaderFinished(id()); return; } @@ -2054,6 +2106,54 @@ void GstEnginePipeline::timerEvent(QTimerEvent *e) { } +bool GstEnginePipeline::HasNextUrl() const { + + QMutexLocker l(&mutex_next_url_); + return next_stream_url_.isValid(); + +} + +void GstEnginePipeline::PrepareNextUrl(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 beginning_nanosec, const qint64 end_nanosec) { + + { + QMutexLocker l(&mutex_next_url_); + next_media_url_ = media_url; + next_stream_url_ = stream_url; + next_gst_url_ = gst_url; + } + + next_beginning_offset_nanosec_ = beginning_nanosec; + next_end_offset_nanosec_ = end_nanosec; + + if (about_to_finish_.value()) { + SetNextUrl(); + } + +} + +void GstEnginePipeline::SetNextUrl() { + + if (about_to_finish_.value() && HasNextUrl() && !next_uri_set_.value()) { + // 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. + next_uri_set_ = true; + { + QMutexLocker l(&mutex_next_url_); + qLog(Debug) << "Setting next URL to" << next_gst_url_; + g_object_set(G_OBJECT(pipeline_), "uri", next_gst_url_.constData(), nullptr); + } + about_to_finish_ = false; + } + +} + +void GstEnginePipeline::SetSourceDevice(const QString &device) { + + QMutexLocker l(&mutex_source_device_); + source_device_ = device; + +} + void GstEnginePipeline::AddBufferConsumer(GstBufferConsumer *consumer) { QMutexLocker l(&mutex_buffer_consumers_); buffer_consumers_ << consumer; @@ -2069,29 +2169,3 @@ void GstEnginePipeline::RemoveAllBufferConsumers() { buffer_consumers_.clear(); } -void GstEnginePipeline::PrepareNextUrl(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 beginning_nanosec, const qint64 end_nanosec) { - - next_media_url_ = media_url; - next_stream_url_ = stream_url; - next_gst_url_ = gst_url; - next_beginning_offset_nanosec_ = beginning_nanosec; - next_end_offset_nanosec_ = end_nanosec; - - if (about_to_finish_) { - SetNextUrl(); - } - -} - -void GstEnginePipeline::SetNextUrl() { - - if (about_to_finish_ && has_next_valid_url() && !next_uri_set_) { - // 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. - next_uri_set_ = true; - qLog(Debug) << "Setting next URL to" << next_gst_url_; - g_object_set(G_OBJECT(pipeline_), "uri", next_gst_url_.constData(), nullptr); - about_to_finish_ = false; - } - -} diff --git a/src/engine/gstenginepipeline.h b/src/engine/gstenginepipeline.h index 376bcb7b8..afe1cf774 100644 --- a/src/engine/gstenginepipeline.h +++ b/src/engine/gstenginepipeline.h @@ -44,6 +44,7 @@ #include #include "core/shared_ptr.h" +#include "core/mutex_protected.h" #include "enginemetadata.h" class QTimer; @@ -59,7 +60,7 @@ class GstEnginePipeline : public QObject { ~GstEnginePipeline() override; // Globally unique across all pipelines. - int id() const { return id_; } + int id() const { return id_.value(); } // Call these setters before Init void set_output_device(const QString &output, const QVariant &device); @@ -97,69 +98,63 @@ class GstEnginePipeline : public QObject { Q_INVOKABLE bool Seek(const qint64 nanosec); void SeekAsync(const qint64 nanosec); void SeekDelayed(const qint64 nanosec); - void SetEBUR128LoudnessNormalizingGain_dB(const double ebur128_loudness_normalizing_gain_db); + void SetVolume(const uint volume_percent); void SetStereoBalance(const float value); void SetEqualizerParams(const int preamp, const QList &band_gains); - - void StartFader(const qint64 duration_nanosec, const QTimeLine::Direction direction = QTimeLine::Forward, const QEasingCurve::Type shape = QEasingCurve::Linear, const bool use_fudge_timer = true); + void SetEBUR128LoudnessNormalizingGain_dB(const double ebur128_loudness_normalizing_gain_db); // If this is set then it will be loaded automatically when playback finishes for gapless playback + bool HasNextUrl() const; void PrepareNextUrl(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 beginning_nanosec, const qint64 end_nanosec); void SetNextUrl(); - bool has_next_valid_url() const { return next_stream_url_.isValid(); } - void SetSourceDevice(const QString &device) { source_device_ = device; } + void SetSourceDevice(const QString &device); + + void StartFader(const qint64 duration_nanosec, const QTimeLine::Direction direction = QTimeLine::Forward, const QEasingCurve::Type shape = QEasingCurve::Linear, const bool use_fudge_timer = true); // Get information about the music playback QUrl media_url() const { return media_url_; } QUrl stream_url() const { return stream_url_; } - double ebur128_loudness_normalizing_gain_db() const { return ebur128_loudness_normalizing_gain_db_; } QByteArray gst_url() const { return gst_url_; } + QMutex *mutex_url() const { return &mutex_url_; } QUrl next_media_url() const { return next_media_url_; } QUrl next_stream_url() const { return next_stream_url_; } QByteArray next_gst_url() const { return next_gst_url_; } - bool is_valid() const { return valid_; } + QMutex *mutex_next_url() const { return &mutex_next_url_; } + double ebur128_loudness_normalizing_gain_db() const { return ebur128_loudness_normalizing_gain_db_; } - // Please note that this method (unlike GstEngine's.position()) is multiple-section media unaware. - qint64 position() const; - // Please note that this method (unlike GstEngine's.length()) is multiple-section media unaware. - qint64 length() const; // Returns this pipeline's state. May return GST_STATE_NULL if the state check timed out. The timeout value is a reasonable default. GstState state() const; - qint64 segment_start() const { return segment_start_; } + // Please note that this method (unlike GstEngine's.length()) is multiple-section media unaware. + qint64 length() const; + // Please note that this method (unlike GstEngine's.position()) is multiple-section media unaware. + qint64 position() const; + qint64 segment_start() const { return segment_start_.value(); } // Don't allow the user to change the playback state (playing/paused) while the pipeline is buffering. - bool is_buffering() const { return buffering_; } - - QByteArray redirect_url() const { return redirect_url_; } - - QString source_device() const { return source_device_; } + bool is_buffering() const { return buffering_.value(); } bool exclusive_mode() const { return exclusive_mode_; } - public Q_SLOTS: - void SetFaderVolume(const qreal volume); + QByteArray redirect_url() const { return redirect_url_; } + QMutex *mutex_redirect_url() { return &mutex_redirect_url_; } + + QString source_device() const { return source_device_; } Q_SIGNALS: + void SetStateFinished(const GstStateChangeReturn state_change_return); void Error(const int pipeline_id, const int domain, const int error_code, const QString &message, const QString &debug); - void EndOfStreamReached(const int pipeline_id, const bool has_next_track); void MetadataFound(const int pipeline_id, const EngineMetadata &bundle); - + void AboutToFinish(); + void Finished(); void VolumeChanged(const uint volume); void FaderFinished(const int pipeline_id); - void BufferingStarted(); void BufferingProgress(const int percent); void BufferingFinished(); - void AboutToFinish(); - - void Finished(); - - void SetStateFinished(const GstStateChangeReturn state); - protected: void timerEvent(QTimerEvent*) override; @@ -199,27 +194,28 @@ class GstEnginePipeline : public QObject { void UpdateEqualizer(); void Disconnect(); - void ResumeFaderAsync(); private Q_SLOTS: - void SetStateAsyncFinished(const GstState state, const GstStateChangeReturn state_change); + void SetStateAsyncFinished(const GstState state, const GstStateChangeReturn state_change_return); + void SetFaderVolume(const qreal volume); void FaderTimelineFinished(); private: // 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. // 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; - int id_; + mutex_protected id_; + + QThreadPool set_state_threadpool_; // General settings for the pipeline - bool valid_; QString output_; QVariant device_; bool exclusive_mode_; bool volume_enabled_; bool fading_enabled_; - bool strict_ssl_enabled_; + mutex_protected strict_ssl_enabled_; // Buffering quint64 buffer_duration_nanosec_; @@ -231,6 +227,7 @@ class GstEnginePipeline : public QObject { bool proxy_authentication_; QString proxy_user_; QString proxy_pass_; + QMutex mutex_proxy_; // Channels bool channels_enabled_; @@ -270,43 +267,52 @@ class GstEnginePipeline : public QObject { QUrl media_url_; QUrl stream_url_; QByteArray gst_url_; + mutable QMutex mutex_url_; + QUrl next_media_url_; QUrl next_stream_url_; QByteArray next_gst_url_; + mutable QMutex mutex_next_url_; + double ebur128_loudness_normalizing_gain_db_; // These get called when there is a new audio buffer available QList buffer_consumers_; QMutex mutex_buffer_consumers_; - qint64 segment_start_; - bool segment_start_received_; + + mutex_protected segment_start_; + mutex_protected segment_start_received_; + GstSegment last_playbin_segment_{}; // If this is > 0 then the pipeline will be forced to stop when playback goes past this position. - qint64 end_offset_nanosec_; + mutex_protected 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_; + mutex_protected next_beginning_offset_nanosec_; + mutex_protected next_end_offset_nanosec_; // Set temporarily when moving to the next contiguous section in a multipart file. - bool ignore_next_seek_; + mutex_protected 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_; + mutex_protected 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. + mutable QMutex mutex_redirect_url_; QByteArray redirect_url_; // When we need to specify the device to use as source (for CD device) QString source_device_; + QMutex mutex_source_device_; // Seeking while the pipeline is in the READY state doesn't work, so we have to wait until it goes to PAUSED or PLAYING. // Also, we have to wait for the playbin to be connected. - bool pipeline_connected_; - bool pipeline_active_; + mutex_protected pipeline_connected_; + mutex_protected pipeline_active_; + mutex_protected buffering_; - GstState pending_state_; - qint64 pending_seek_nanosec_; + mutex_protected pending_state_; + mutex_protected pending_seek_nanosec_; // 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 correct call to gst_element_query_position() or after a seek), we store @@ -314,15 +320,14 @@ class GstEnginePipeline : public QObject { mutable gint64 last_known_position_ns_; // Complete the transition to the next song when it starts playing - bool next_uri_set_; - bool next_uri_reset_; + mutex_protected next_uri_set_; + mutex_protected next_uri_reset_; - bool volume_set_; - gdouble volume_internal_; - uint volume_percent_; - - bool buffering_; + mutex_protected volume_set_; + mutex_protected volume_internal_; + mutex_protected volume_percent_; + mutex_protected fader_active_; SharedPtr fader_; QBasicTimer fader_fudge_timer_; bool use_fudge_timer_; @@ -351,16 +356,10 @@ class GstEnginePipeline : public QObject { glong about_to_finish_cb_id_; glong notify_volume_cb_id_; - QThreadPool set_state_threadpool_; - - GstSegment last_playbin_segment_{}; - bool logged_unsupported_analyzer_format_; - - bool about_to_finish_; - - bool finish_requested_; - bool finished_; + mutex_protected about_to_finish_; + mutex_protected finish_requested_; + mutex_protected finished_; }; using GstEnginePipelinePtr = SharedPtr;