From 548fa3f6ee5281cb38a43a395674d9011fa2794c Mon Sep 17 00:00:00 2001 From: Jonas Kvinge Date: Sat, 3 Aug 2024 00:05:06 +0200 Subject: [PATCH] Wait for set state to finish before deleting pipeline Setting state to GST_STATE_NULL sometimes blocks, to fix this use the threadpool to set the state to NULL and wait with deleting the pipeline until the state is changed. This fixes blocking the main thread when switching Spotify songs. --- src/core/mainwindow.cpp | 51 +++-- src/core/player.cpp | 35 ++-- src/core/player.h | 7 +- src/engine/enginebase.cpp | 4 +- src/engine/enginebase.h | 4 +- src/engine/gstengine.cpp | 257 ++++++++++++++++++-------- src/engine/gstengine.h | 34 ++-- src/engine/gstenginepipeline.cpp | 308 ++++++++++++++++++++++--------- src/engine/gstenginepipeline.h | 28 ++- src/engine/vlcengine.cpp | 4 +- src/engine/vlcengine.h | 2 +- 11 files changed, 520 insertions(+), 214 deletions(-) diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index 5ce98a4a0..5b555333a 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -1371,8 +1371,12 @@ void MainWindow::MediaStopped() { ui_->button_love->setEnabled(false); tray_icon_->LoveStateChanged(false); - track_position_timer_->stop(); - track_slider_timer_->stop(); + if (track_position_timer_->isActive()) { + track_position_timer_->stop(); + } + if (track_slider_timer_->isActive()) { + track_slider_timer_->stop(); + } ui_->track_slider->SetStopped(); tray_icon_->SetProgress(0); tray_icon_->SetStopped(); @@ -1400,8 +1404,12 @@ void MainWindow::MediaPaused() { ui_->action_play_pause->setEnabled(true); - track_position_timer_->stop(); - track_slider_timer_->stop(); + if (!track_position_timer_->isActive()) { + track_position_timer_->start(); + } + if (!track_slider_timer_->isActive()) { + track_slider_timer_->start(); + } tray_icon_->SetPaused(); @@ -1426,8 +1434,13 @@ void MainWindow::MediaPlaying() { ui_->track_slider->SetCanSeek(can_seek); tray_icon_->SetPlaying(enable_play_pause); - track_position_timer_->start(); - track_slider_timer_->start(); + if (!track_position_timer_->isActive()) { + track_position_timer_->start(); + } + if (!track_slider_timer_->isActive()) { + track_slider_timer_->start(); + } + UpdateTrackPosition(); } @@ -1566,7 +1579,7 @@ void MainWindow::LoadPlaybackStatus() { const EngineBase::State playback_state = static_cast(s.value("playback_state", static_cast(EngineBase::State::Empty)).toInt()); s.endGroup(); - if (resume_playback && playback_state != EngineBase::State::Empty && playback_state != EngineBase::State::Idle) { + if (resume_playback && (playback_state == EngineBase::State::Playing || playback_state == EngineBase::State::Paused)) { SharedPtr connection = make_shared(); *connection = QObject::connect(&*app_->playlist_manager(), &PlaylistManager::AllPlaylistsLoaded, this, [this, connection]() { QObject::disconnect(*connection); @@ -1583,21 +1596,19 @@ void MainWindow::ResumePlayback() { Settings s; s.beginGroup(Player::kSettingsGroup); const EngineBase::State playback_state = static_cast(s.value("playback_state", static_cast(EngineBase::State::Empty)).toInt()); - int playback_playlist = s.value("playback_playlist", -1).toInt(); - int playback_position = s.value("playback_position", 0).toInt(); + const int playback_playlist = s.value("playback_playlist", -1).toInt(); + const int playback_position = s.value("playback_position", 0).toInt(); s.endGroup(); if (playback_playlist == app_->playlist_manager()->current()->id()) { // Set active to current to resume playback on correct playlist. app_->playlist_manager()->SetActiveToCurrent(); - if (playback_state == EngineBase::State::Paused) { - SharedPtr connection = make_shared(); - *connection = QObject::connect(&*app_->player(), &Player::Playing, &*app_->player(), [this, connection]() { - QObject::disconnect(*connection); - QTimer::singleShot(300, &*app_->player(), &Player::PlayPauseHelper); - }); + if (playback_state == EngineBase::State::Playing) { + app_->player()->Play(playback_position * kNsecPerSec); + } + else if (playback_state == EngineBase::State::Paused) { + app_->player()->PlayWithPause(playback_position * kNsecPerSec); } - app_->player()->Play(playback_position * kNsecPerSec); } // Reset saved playback status so we don't resume again from the same position. @@ -1620,7 +1631,7 @@ void MainWindow::PlayIndex(const QModelIndex &idx, Playlist::AutoScroll autoscro } app_->playlist_manager()->SetActiveToCurrent(); - app_->player()->PlayAt(row, 0, EngineBase::TrackChangeType::Manual, autoscroll, true); + app_->player()->PlayAt(row, false, 0, EngineBase::TrackChangeType::Manual, autoscroll, true); } @@ -1637,14 +1648,14 @@ void MainWindow::PlaylistDoubleClick(const QModelIndex &idx) { switch (doubleclick_playlist_addmode_) { case BehaviourSettingsPage::PlaylistAddBehaviour::Play: app_->playlist_manager()->SetActiveToCurrent(); - app_->player()->PlayAt(source_idx.row(), 0, EngineBase::TrackChangeType::Manual, Playlist::AutoScroll::Never, true, true); + app_->player()->PlayAt(source_idx.row(), false, 0, EngineBase::TrackChangeType::Manual, Playlist::AutoScroll::Never, true, true); break; case BehaviourSettingsPage::PlaylistAddBehaviour::Enqueue: app_->playlist_manager()->current()->queue()->ToggleTracks(QModelIndexList() << source_idx); if (app_->player()->GetState() != EngineBase::State::Playing) { app_->playlist_manager()->SetActiveToCurrent(); - app_->player()->PlayAt(app_->playlist_manager()->current()->queue()->TakeNext(), 0, EngineBase::TrackChangeType::Manual, Playlist::AutoScroll::Never, true); + app_->player()->PlayAt(app_->playlist_manager()->current()->queue()->TakeNext(), false, 0, EngineBase::TrackChangeType::Manual, Playlist::AutoScroll::Never, true); } break; } @@ -2585,7 +2596,7 @@ void MainWindow::CommandlineOptionsReceived(const CommandlineOptions &options) { app_->player()->SeekTo(app_->player()->engine()->position_nanosec() / kNsecPerSec + options.seek_by()); } - if (options.play_track_at() != -1) app_->player()->PlayAt(options.play_track_at(), 0, EngineBase::TrackChangeType::Manual, Playlist::AutoScroll::Maybe, true); + if (options.play_track_at() != -1) app_->player()->PlayAt(options.play_track_at(), false, 0, EngineBase::TrackChangeType::Manual, Playlist::AutoScroll::Maybe, true); if (options.show_osd()) app_->player()->ShowOSD(); diff --git a/src/core/player.cpp b/src/core/player.cpp index d9dce8fd5..b8da794d5 100644 --- a/src/core/player.cpp +++ b/src/core/player.cpp @@ -354,7 +354,7 @@ void Player::HandleLoadResult(const UrlHandler::LoadResult &result) { if (is_current) { qLog(Debug) << "Playing song" << current_item->Metadata().title() << result.stream_url_ << "position" << play_offset_nanosec_; - engine_->Play(result.media_url_, result.stream_url_, stream_change_type_, song.has_cue(), song.beginning_nanosec(), song.end_nanosec(), play_offset_nanosec_, song.ebur128_integrated_loudness_lufs()); + engine_->Play(result.media_url_, result.stream_url_, pause_, stream_change_type_, song.has_cue(), song.beginning_nanosec(), song.end_nanosec(), play_offset_nanosec_, song.ebur128_integrated_loudness_lufs()); current_item_ = current_item; play_offset_nanosec_ = 0; } @@ -426,7 +426,7 @@ void Player::NextItem(const EngineBase::TrackChangeFlags change, const Playlist: return; } - PlayAt(i, 0, change, autoscroll, false, true); + PlayAt(i, false, 0, change, autoscroll, false, true); } @@ -461,11 +461,10 @@ void Player::PlayPlaylistInternal(const EngineBase::TrackChangeFlags change, con if (i == -1) i = app_->playlist_manager()->active()->last_played_row(); if (i == -1) i = 0; - PlayAt(i, 0, change, autoscroll, true); + PlayAt(i, false, 0, change, autoscroll, true); } - bool Player::HandleStopAfter(const Playlist::AutoScroll autoscroll) { if (app_->playlist_manager()->active()->stop_after_current()) { @@ -527,7 +526,7 @@ void Player::PlayPause(const quint64 offset_nanosec, const Playlist::AutoScroll int i = app_->playlist_manager()->active()->current_row(); if (i == -1) i = app_->playlist_manager()->active()->last_played_row(); if (i == -1) i = 0; - PlayAt(i, offset_nanosec, EngineBase::TrackChangeType::First, autoscroll, true); + PlayAt(i, false, offset_nanosec, EngineBase::TrackChangeType::First, autoscroll, true); break; } } @@ -606,7 +605,7 @@ void Player::PreviousItem(const EngineBase::TrackChangeFlags change) { QDateTime now = QDateTime::currentDateTime(); if (last_pressed_previous_.isValid() && last_pressed_previous_.secsTo(now) >= 2) { last_pressed_previous_ = now; - PlayAt(app_->playlist_manager()->active()->current_row(), 0, change, Playlist::AutoScroll::Always, false, true); + PlayAt(app_->playlist_manager()->active()->current_row(), false, 0, change, Playlist::AutoScroll::Always, false, true); return; } last_pressed_previous_ = now; @@ -616,11 +615,11 @@ void Player::PreviousItem(const EngineBase::TrackChangeFlags change) { app_->playlist_manager()->active()->set_current_row(i, Playlist::AutoScroll::Always, false); if (i == -1) { Stop(); - PlayAt(i, 0, change, Playlist::AutoScroll::Always, true); + PlayAt(i, false, 0, change, Playlist::AutoScroll::Always, true); return; } - PlayAt(i, 0, change, Playlist::AutoScroll::Always, false); + PlayAt(i, false, 0, change, Playlist::AutoScroll::Always, false); } @@ -718,9 +717,9 @@ void Player::VolumeDown() { } -void Player::PlayAt(const int index, const quint64 offset_nanosec, EngineBase::TrackChangeFlags change, const Playlist::AutoScroll autoscroll, const bool reshuffle, const bool force_inform) { +void Player::PlayAt(const int index, const bool pause, const quint64 offset_nanosec, EngineBase::TrackChangeFlags change, const Playlist::AutoScroll autoscroll, const bool reshuffle, const bool force_inform) { - pause_time_ = QDateTime(); + pause_time_ = pause ? QDateTime::currentDateTime() : QDateTime(); play_offset_nanosec_ = offset_nanosec; if (current_item_ && change & EngineBase::TrackChangeType::Manual && engine_->position_nanosec() != engine_->length_nanosec()) { @@ -748,13 +747,14 @@ void Player::PlayAt(const int index, const quint64 offset_nanosec, EngineBase::T return; } + pause_ = pause; stream_change_type_ = change; autoscroll_ = autoscroll; HandleLoadResult(url_handlers_[url.scheme()]->StartLoading(url)); } else { qLog(Debug) << "Playing song" << current_item_->Metadata().title() << url << "position" << offset_nanosec; - engine_->Play(current_item_->Url(), url, change, current_item_->Metadata().has_cue(), current_item_->effective_beginning_nanosec(), current_item_->effective_end_nanosec(), offset_nanosec, current_item_->effective_ebur128_integrated_loudness_lufs()); + engine_->Play(current_item_->Url(), url, pause, change, current_item_->Metadata().has_cue(), current_item_->effective_beginning_nanosec(), current_item_->effective_end_nanosec(), offset_nanosec, current_item_->effective_ebur128_integrated_loudness_lufs()); } } @@ -866,6 +866,19 @@ void Player::Play(const quint64 offset_nanosec) { } +void Player::PlayWithPause(const quint64 offset_nanosec) { + + pause_time_ = QDateTime(); + play_offset_nanosec_ = offset_nanosec; + app_->playlist_manager()->SetActivePlaylist(app_->playlist_manager()->current_id()); + if (app_->playlist_manager()->active()->rowCount() == 0) return; + int i = app_->playlist_manager()->active()->current_row(); + if (i == -1) i = app_->playlist_manager()->active()->last_played_row(); + if (i == -1) i = 0; + PlayAt(i, true, offset_nanosec, EngineBase::TrackChangeType::First, Playlist::AutoScroll::Always, true); + +} + void Player::ShowOSD() { if (current_item_) emit ForceShowOSD(current_item_->Metadata(), false); } diff --git a/src/core/player.h b/src/core/player.h index 95e67b903..1c7692bfa 100644 --- a/src/core/player.h +++ b/src/core/player.h @@ -70,7 +70,7 @@ class PlayerInterface : public QObject { virtual void SaveVolume() = 0; // Manual track change to the specified track - virtual void PlayAt(const int index, const quint64 offset_nanosec, EngineBase::TrackChangeFlags change, const Playlist::AutoScroll autoscroll, const bool reshuffle, const bool force_inform = false) = 0; + virtual void PlayAt(const int index, const bool pause, const quint64 offset_nanosec, EngineBase::TrackChangeFlags change, const Playlist::AutoScroll autoscroll, const bool reshuffle, const bool force_inform = false) = 0; // If there's currently a song playing, pause it, otherwise play the track that was playing last, or the first one on the playlist virtual void PlayPause(const quint64 offset_nanosec = 0, const Playlist::AutoScroll autoscroll = Playlist::AutoScroll::Always) = 0; @@ -98,6 +98,7 @@ class PlayerInterface : public QObject { virtual void Pause() = 0; virtual void Stop(const bool stop_after = false) = 0; virtual void Play(const quint64 offset_nanosec = 0) = 0; + virtual void PlayWithPause(const quint64 offset_nanosec) = 0; virtual void PlayHelper() = 0; virtual void ShowOSD() = 0; @@ -159,7 +160,7 @@ class Player : public PlayerInterface { void LoadVolume() override; void SaveVolume() override; - void PlayAt(const int index, const quint64 offset_nanosec, EngineBase::TrackChangeFlags change, const Playlist::AutoScroll autoscroll, const bool reshuffle, const bool force_inform = false) override; + void PlayAt(const int index, const bool pause, const quint64 offset_nanosec, EngineBase::TrackChangeFlags change, const Playlist::AutoScroll autoscroll, const bool reshuffle, const bool force_inform = false) override; void PlayPause(const quint64 offset_nanosec = 0, const Playlist::AutoScroll autoscroll = Playlist::AutoScroll::Always) override; void PlayPauseHelper() override { PlayPause(play_offset_nanosec_); } void RestartOrPrevious() override; @@ -182,6 +183,7 @@ class Player : public PlayerInterface { void Stop(const bool stop_after = false) override; void StopAfterCurrent(); void Play(const quint64 offset_nanosec = 0) override; + void PlayWithPause(const quint64 offset_nanosec) override; void PlayHelper() override { Play(); } void ShowOSD() override; void TogglePrettyOSD(); @@ -228,6 +230,7 @@ class Player : public PlayerInterface { PlaylistItemPtr current_item_; + bool pause_; EngineBase::TrackChangeFlags stream_change_type_; Playlist::AutoScroll autoscroll_; EngineBase::State last_state_; diff --git a/src/engine/enginebase.cpp b/src/engine/enginebase.cpp index d0a873f93..0076f0bce 100644 --- a/src/engine/enginebase.cpp +++ b/src/engine/enginebase.cpp @@ -139,13 +139,13 @@ bool EngineBase::Load(const QUrl &media_url, const QUrl &stream_url, const Track } -bool EngineBase::Play(const QUrl &media_url, const QUrl &stream_url, const TrackChangeFlags flags, const bool force_stop_at_end, const quint64 beginning_nanosec, const qint64 end_nanosec, const quint64 offset_nanosec, const std::optional ebur128_integrated_loudness_lufs) { +bool EngineBase::Play(const QUrl &media_url, const QUrl &stream_url, const bool pause, const TrackChangeFlags flags, const bool force_stop_at_end, const quint64 beginning_nanosec, const qint64 end_nanosec, const quint64 offset_nanosec, const std::optional ebur128_integrated_loudness_lufs) { if (!Load(media_url, stream_url, flags, force_stop_at_end, beginning_nanosec, end_nanosec, ebur128_integrated_loudness_lufs)) { return false; } - return Play(offset_nanosec); + return Play(pause, offset_nanosec); } diff --git a/src/engine/enginebase.h b/src/engine/enginebase.h index e7eb5768a..8a6eed093 100644 --- a/src/engine/enginebase.h +++ b/src/engine/enginebase.h @@ -105,7 +105,7 @@ class EngineBase : public QObject { virtual State state() const = 0; virtual void StartPreloading(const QUrl&, const QUrl&, const bool, const qint64, const qint64) {} virtual bool Load(const QUrl &media_url, const QUrl &stream_url, const TrackChangeFlags change, const bool force_stop_at_end, const quint64 beginning_nanosec, const qint64 end_nanosec, const std::optional ebur128_integrated_loudness_lufs); - virtual bool Play(const quint64 offset_nanosec) = 0; + virtual bool Play(const bool pause, const quint64 offset_nanosec) = 0; virtual void Stop(const bool stop_after = false) = 0; virtual void Pause() = 0; virtual void Unpause() = 0; @@ -133,7 +133,7 @@ class EngineBase : public QObject { // Plays a media stream represented with the URL 'u' from the given 'beginning' to the given 'end' (usually from 0 to a song's length). // Both markers should be passed in nanoseconds. 'end' can be negative, indicating that the real length of 'u' stream is unknown. - bool Play(const QUrl &media_url, const QUrl &stream_url, const TrackChangeFlags flags, const bool force_stop_at_end, const quint64 beginning_nanosec, const qint64 end_nanosec, const quint64 offset_nanosec, const std::optional ebur128_integrated_loudness_lufs); + bool Play(const QUrl &media_url, const QUrl &stream_url, const bool pause, const TrackChangeFlags flags, const bool force_stop_at_end, const quint64 beginning_nanosec, const qint64 end_nanosec, const quint64 offset_nanosec, const std::optional ebur128_integrated_loudness_lufs); void SetVolume(const uint volume); public slots: diff --git a/src/engine/gstengine.cpp b/src/engine/gstengine.cpp index 7c4b38add..e3cd20c37 100644 --- a/src/engine/gstengine.cpp +++ b/src/engine/gstengine.cpp @@ -98,13 +98,15 @@ GstEngine::GstEngine(SharedPtr task_manager, QObject *parent) waiting_to_seek_(false), seek_pos_(0), timer_id_(-1), - is_fading_out_to_pause_(false), - has_faded_out_(false), + has_faded_out_to_pause_(false), scope_chunk_(0), have_new_buffer_(false), scope_chunks_(0), discovery_finished_cb_id_(-1), - discovery_discovered_cb_id_(-1) { + discovery_discovered_cb_id_(-1), + delayed_state_(State::Empty), + delayed_state_pause_(false), + delayed_state_offset_nanosec_(0) { seek_timer_->setSingleShot(true); seek_timer_->setInterval(kSeekDelayNanosec / kNsecPerMsec); @@ -205,20 +207,27 @@ bool GstEngine::Load(const QUrl &media_url, const QUrl &stream_url, const Engine return true; } - SharedPtr pipeline = CreatePipeline(media_url, stream_url, gst_url, force_stop_at_end ? end_nanosec : 0, ebur128_loudness_normalizing_gain_db_); + GstEnginePipelinePtr pipeline = CreatePipeline(media_url, stream_url, gst_url, force_stop_at_end ? end_nanosec : 0, ebur128_loudness_normalizing_gain_db_); if (!pipeline) return false; - if (crossfade) StartFadeout(); + if (crossfade && current_pipeline_ && !ExclusivePipelineActive() && !fadeout_pipelines_.contains(current_pipeline_->id())) { + StartFadeout(); + } BufferingFinished(); + + GstEnginePipelinePtr old_pipeline = current_pipeline_; current_pipeline_ = pipeline; + if (old_pipeline && !fadeout_pipelines_.contains(old_pipeline->id())) { + FinishPipeline(old_pipeline); + } SetVolume(volume_); SetStereoBalance(stereo_balance_); SetEqualizerParameters(equalizer_preamp_, equalizer_gains_); // Maybe fade in this track - if (crossfade) { + if (crossfade && !ExclusivePipelineActive()) { current_pipeline_->StartFader(fadeout_duration_nanosec_, QTimeLine::Forward); } @@ -243,26 +252,38 @@ bool GstEngine::Load(const QUrl &media_url, const QUrl &stream_url, const Engine } -bool GstEngine::Play(const quint64 offset_nanosec) { +bool GstEngine::Play(const bool pause, const quint64 offset_nanosec) { EnsureInitialized(); if (!current_pipeline_ || current_pipeline_->is_buffering()) return false; + if (ExclusivePipelineActive()) { + qLog(Debug) << "Delaying play because a exclusive pipeline is already active..."; + delayed_state_ = pause ? State::Paused : State::Playing; + delayed_state_pause_ = pause; + delayed_state_offset_nanosec_ = offset_nanosec; + return true; + } + + if (fadeout_pause_pipeline_) { + StopFadeoutPause(); + } + + delayed_state_ = State::Empty; + delayed_state_pause_ = false; + delayed_state_offset_nanosec_ = 0; + QFutureWatcher *watcher = new QFutureWatcher(); const int pipeline_id = current_pipeline_->id(); - QObject::connect(watcher, &QFutureWatcher::finished, this, [this, watcher, pipeline_id, offset_nanosec]() { + QObject::connect(watcher, &QFutureWatcher::finished, this, [this, watcher, pipeline_id, pause, offset_nanosec]() { const GstStateChangeReturn ret = watcher->result(); watcher->deleteLater(); - PlayDone(ret, offset_nanosec, pipeline_id); + PlayDone(ret, pause, offset_nanosec, pipeline_id); }); - QFuture future = current_pipeline_->SetState(GST_STATE_PLAYING); + QFuture future = current_pipeline_->Play(pause, offset_nanosec); watcher->setFuture(future); - if (is_fading_out_to_pause_) { - current_pipeline_->SetState(GST_STATE_PAUSED); - } - return true; } @@ -271,24 +292,33 @@ void GstEngine::Stop(const bool stop_after) { StopTimers(); + delayed_state_ = State::Empty; + delayed_state_pause_ = false; + delayed_state_offset_nanosec_ = 0; + media_url_.clear(); stream_url_.clear(); // To ensure we return Empty from state() beginning_nanosec_ = end_nanosec_ = 0; // 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. - if (is_fading_out_to_pause_) { - QObject::disconnect(&*current_pipeline_, &GstEnginePipeline::FaderFinished, nullptr, nullptr); - is_fading_out_to_pause_ = false; - has_faded_out_ = true; - - fadeout_pause_pipeline_.reset(); - fadeout_pipeline_.reset(); + if (fadeout_pause_pipeline_) { + StopFadeoutPause(); } - if (fadeout_enabled_ && current_pipeline_ && !stop_after) StartFadeout(); + if (current_pipeline_) { + if (fadeout_enabled_ && !stop_after && !ExclusivePipelineActive()) { + StartFadeout(); + current_pipeline_ = GstEnginePipelinePtr(); + } + else { + GstEnginePipelinePtr old_pipeline = current_pipeline_; + current_pipeline_ = GstEnginePipelinePtr(); + FinishPipeline(old_pipeline); + } + } - current_pipeline_.reset(); BufferingFinished(); + emit StateChanged(State::Empty); } @@ -297,22 +327,20 @@ void GstEngine::Pause() { if (!current_pipeline_ || current_pipeline_->is_buffering()) return; - // 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. - if (is_fading_out_to_pause_) { - QObject::disconnect(&*current_pipeline_, &GstEnginePipeline::FaderFinished, nullptr, nullptr); - current_pipeline_->StartFader(fadeout_pause_duration_nanosec_, QTimeLine::Forward, QEasingCurve::InOutQuad, false); - is_fading_out_to_pause_ = false; - has_faded_out_ = false; - emit StateChanged(State::Playing); + delayed_state_ = State::Empty; + delayed_state_pause_ = false; + delayed_state_offset_nanosec_ = 0; + + if (fadeout_pause_pipeline_) { return; } if (current_pipeline_->state() == GST_STATE_PLAYING) { - if (fadeout_pause_enabled_) { + if (fadeout_pause_enabled_ && !ExclusivePipelineActive()) { StartFadeoutPause(); } else { - current_pipeline_->SetState(GST_STATE_PAUSED); + current_pipeline_->SetStateAsync(GST_STATE_PAUSED); emit StateChanged(State::Paused); StopTimers(); } @@ -325,20 +353,22 @@ void GstEngine::Unpause() { if (!current_pipeline_ || current_pipeline_->is_buffering()) return; if (current_pipeline_->state() == GST_STATE_PAUSED) { - current_pipeline_->SetState(GST_STATE_PLAYING); // Check if we faded out last time. If yes, fade in no matter what the settings say. // If we pause with fadeout, deactivate fadeout and resume playback, the player would be muted if not faded in. - if (has_faded_out_) { + if (has_faded_out_to_pause_ && !ExclusivePipelineActive()) { QObject::disconnect(&*current_pipeline_, &GstEnginePipeline::FaderFinished, nullptr, nullptr); - current_pipeline_->StartFader(fadeout_pause_duration_nanosec_, QTimeLine::Forward, QEasingCurve::InOutQuad, false); - has_faded_out_ = false; + current_pipeline_->StartFader(fadeout_pause_duration_nanosec_, QTimeLine::Forward, QEasingCurve::Linear, false); + has_faded_out_to_pause_ = false; } + current_pipeline_->SetStateAsync(GST_STATE_PLAYING); + emit StateChanged(State::Playing); StartTimers(); } + } void GstEngine::Seek(const quint64 offset_nanosec) { @@ -352,6 +382,7 @@ void GstEngine::Seek(const quint64 offset_nanosec) { SeekNow(); seek_timer_->start(); // Stop us from seeking again for a little while } + } void GstEngine::SetVolumeSW(const uint volume) { @@ -621,25 +652,27 @@ void GstEngine::AddBufferToScope(GstBuffer *buf, const int pipeline_id, const QS } -void GstEngine::FadeoutFinished() { +void GstEngine::FadeoutFinished(const int pipeline_id) { + + if (!fadeout_pipelines_.contains(pipeline_id)) { + return; + } + + GstEnginePipelinePtr pipeline = fadeout_pipelines_.value(pipeline_id); + fadeout_pipelines_.remove(pipeline_id); + FinishPipeline(pipeline); - fadeout_pipeline_.reset(); emit FadeoutFinishedSignal(); } void GstEngine::FadeoutPauseFinished() { - fadeout_pause_pipeline_->SetState(GST_STATE_PAUSED); - current_pipeline_->SetState(GST_STATE_PAUSED); + fadeout_pause_pipeline_->SetStateAsync(GST_STATE_PAUSED); emit StateChanged(State::Paused); StopTimers(); - - is_fading_out_to_pause_ = false; - has_faded_out_ = true; - fadeout_pause_pipeline_.reset(); - fadeout_pipeline_.reset(); - + has_faded_out_to_pause_ = true; + fadeout_pause_pipeline_ = GstEnginePipelinePtr(); emit FadeoutFinishedSignal(); } @@ -657,7 +690,7 @@ void GstEngine::SeekNow() { } -void GstEngine::PlayDone(const GstStateChangeReturn ret, const quint64 offset_nanosec, const int pipeline_id) { +void GstEngine::PlayDone(const GstStateChangeReturn ret, const bool pause, const quint64 offset_nanosec, const int pipeline_id) { if (!current_pipeline_ || pipeline_id != current_pipeline_->id()) { return; @@ -668,26 +701,33 @@ void GstEngine::PlayDone(const GstStateChangeReturn ret, const quint64 offset_na const QByteArray redirect_url = current_pipeline_->redirect_url(); if (!redirect_url.isEmpty() && redirect_url != current_pipeline_->gst_url()) { qLog(Info) << "Redirecting to" << redirect_url; + GstEnginePipelinePtr old_pipeline = current_pipeline_; + current_pipeline_ = GstEnginePipelinePtr(); + if (old_pipeline) { + FinishPipeline(old_pipeline); + } current_pipeline_ = CreatePipeline(current_pipeline_->media_url(), current_pipeline_->stream_url(), redirect_url, end_nanosec_, current_pipeline_->ebur128_loudness_normalizing_gain_db()); - Play(offset_nanosec); + Play(pause, offset_nanosec); return; } // Failure - give up qLog(Warning) << "Could not set thread to PLAYING."; - current_pipeline_.reset(); + GstEnginePipelinePtr old_pipeline = current_pipeline_; + current_pipeline_ = GstEnginePipelinePtr(); + if (old_pipeline) { + FinishPipeline(old_pipeline); + } BufferingFinished(); return; } - StartTimers(); - - // Initial offset - if (offset_nanosec != 0 || beginning_nanosec_ != 0) { - Seek(offset_nanosec); + if (!pause) { + StartTimers(); } - emit StateChanged(State::Playing); + emit StateChanged(pause ? State::Paused : State::Playing); + // We've successfully started playing a media stream with this url emit ValidSongRequested(stream_url_); @@ -757,28 +797,37 @@ QByteArray GstEngine::FixupUrl(const QUrl &url) { void GstEngine::StartFadeout() { - if (is_fading_out_to_pause_) return; + GstEnginePipelinePtr pipeline = current_pipeline_; - fadeout_pipeline_ = current_pipeline_; - QObject::disconnect(&*fadeout_pipeline_, nullptr, nullptr, nullptr); - fadeout_pipeline_->RemoveAllBufferConsumers(); + if (fadeout_pipelines_.contains(pipeline->id())) { + return; + } - fadeout_pipeline_->StartFader(fadeout_duration_nanosec_, QTimeLine::Backward); - QObject::connect(&*fadeout_pipeline_, &GstEnginePipeline::FaderFinished, this, &GstEngine::FadeoutFinished); + fadeout_pipelines_.insert(pipeline->id(), pipeline); + pipeline->RemoveAllBufferConsumers(); + + pipeline->StartFader(fadeout_duration_nanosec_, QTimeLine::Backward); + QObject::connect(&*pipeline, &GstEnginePipeline::FaderFinished, this, &GstEngine::FadeoutFinished); } void GstEngine::StartFadeoutPause() { - fadeout_pause_pipeline_ = current_pipeline_; - QObject::disconnect(&*fadeout_pause_pipeline_, &GstEnginePipeline::FaderFinished, nullptr, nullptr); + if (!current_pipeline_ || fadeout_pipelines_.contains(current_pipeline_->id())) return; - fadeout_pause_pipeline_->StartFader(fadeout_pause_duration_nanosec_, QTimeLine::Backward, QEasingCurve::InOutQuad, false); - if (fadeout_pipeline_ && fadeout_pipeline_->state() == GST_STATE_PLAYING) { - fadeout_pipeline_->StartFader(fadeout_pause_duration_nanosec_, QTimeLine::Backward, QEasingCurve::Linear, false); - } + fadeout_pause_pipeline_ = current_pipeline_; QObject::connect(&*fadeout_pause_pipeline_, &GstEnginePipeline::FaderFinished, this, &GstEngine::FadeoutPauseFinished); - is_fading_out_to_pause_ = true; + fadeout_pause_pipeline_->StartFader(fadeout_pause_duration_nanosec_, QTimeLine::Backward, QEasingCurve::Linear, false); + +} + +void GstEngine::StopFadeoutPause() { + + if (!fadeout_pause_pipeline_) return; + + QObject::disconnect(&*fadeout_pause_pipeline_, &GstEnginePipeline::FaderFinished, this, &GstEngine::FadeoutPauseFinished); + has_faded_out_to_pause_ = true; + fadeout_pause_pipeline_ = GstEnginePipelinePtr(); } @@ -798,11 +847,11 @@ void GstEngine::StopTimers() { } -SharedPtr GstEngine::CreatePipeline() { +GstEnginePipelinePtr GstEngine::CreatePipeline() { EnsureInitialized(); - SharedPtr ret = make_shared(); + GstEnginePipelinePtr ret = make_shared(); ret->set_output_device(output_, device_); ret->set_exclusive_mode(exclusive_mode_); ret->set_volume_enabled(volume_control_); @@ -841,9 +890,9 @@ SharedPtr GstEngine::CreatePipeline() { } -SharedPtr GstEngine::CreatePipeline(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 end_nanosec, const double ebur128_loudness_normalizing_gain_db) { +GstEnginePipelinePtr GstEngine::CreatePipeline(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 end_nanosec, const double ebur128_loudness_normalizing_gain_db) { - SharedPtr ret = CreatePipeline(); + GstEnginePipelinePtr ret = CreatePipeline(); QString error; if (!ret->InitFromUrl(media_url, stream_url, gst_url, end_nanosec, ebur128_loudness_normalizing_gain_db, error)) { ret.reset(); @@ -856,6 +905,50 @@ SharedPtr GstEngine::CreatePipeline(const QUrl &media_url, co } +void GstEngine::FinishPipeline(GstEnginePipelinePtr pipeline) { + + const int pipeline_id = pipeline->id(); + + if (!pipeline->Finish() && !old_pipelines_.contains(pipeline->id())) { + old_pipelines_.insert(pipeline_id, pipeline); + QObject::connect(&*pipeline, &GstEnginePipeline::Finished, this, [this, pipeline_id]() { + PipelineFinished(pipeline_id); + }); + } + +} + +void GstEngine::PipelineFinished(const int pipeline_id) { + + qLog(Debug) << "Pipeline" << pipeline_id << "finished"; + + GstEnginePipelinePtr pipeline = old_pipelines_[pipeline_id]; + old_pipelines_.remove(pipeline_id); + if (pipeline == fadeout_pause_pipeline_) { + StopFadeoutPause(); + } + pipeline = GstEnginePipelinePtr(); + + if (current_pipeline_ && old_pipelines_.isEmpty() && delayed_state_ != State::Empty) { + switch (delayed_state_) { + case State::Playing: + Play(delayed_state_pause_, delayed_state_offset_nanosec_); + break; + case State::Paused: + Pause(); + break; + default: + break; + } + delayed_state_ = State::Empty; + delayed_state_pause_ = false; + delayed_state_offset_nanosec_ = 0; + } + + qLog(Debug) << (current_pipeline_ ? 1 : 0) + old_pipelines_.count() << "pipelines are active"; + +} + void GstEngine::UpdateScope(const int chunk_length) { using sample_type = EngineBase::Scope::value_type; @@ -1007,3 +1100,23 @@ QString GstEngine::GSTdiscovererErrorMessage(GstDiscovererResult result) { } } + +bool GstEngine::ExclusivePipelineActive() const { + + if (old_pipelines_.isEmpty()) { + return false; + } + + if (current_pipeline_ && current_pipeline_->exclusive_mode()) { + return true; + } + + for (const GstEnginePipelinePtr &pipeline : std::as_const(old_pipelines_)) { + if (pipeline->exclusive_mode()) { + return true; + } + } + + return false; + +} diff --git a/src/engine/gstengine.h b/src/engine/gstengine.h index 8ca2ad4ac..d6513056d 100644 --- a/src/engine/gstengine.h +++ b/src/engine/gstengine.h @@ -35,18 +35,19 @@ #include #include #include +#include #include #include #include "core/shared_ptr.h" #include "enginebase.h" #include "gststartup.h" +#include "gstenginepipeline.h" #include "gstbufferconsumer.h" class QTimer; class QTimerEvent; class TaskManager; -class GstEnginePipeline; class GstEngine : public EngineBase, public GstBufferConsumer { Q_OBJECT @@ -63,7 +64,7 @@ class GstEngine : public EngineBase, public GstBufferConsumer { State state() const override; void StartPreloading(const QUrl &media_url, const QUrl &stream_url, const bool force_stop_at_end, const qint64 beginning_nanosec, const qint64 end_nanosec) override; bool Load(const QUrl &media_url, const QUrl &stream_url, const EngineBase::TrackChangeFlags change, const bool force_stop_at_end, const quint64 beginning_nanosec, const qint64 end_nanosec, const std::optional ebur128_integrated_loudness_lufs) override; - bool Play(const quint64 offset_nanosec) override; + bool Play(const bool pause, const quint64 offset_nanosec) override; void Stop(const bool stop_after = false) override; void Pause() override; void Unpause() override; @@ -115,26 +116,31 @@ class GstEngine : public EngineBase, public GstBufferConsumer { void HandlePipelineError(const int pipeline_id, const int domain, const int error_code, const QString &message, const QString &debugstr); void NewMetaData(const int pipeline_id, const EngineMetadata &engine_metadata); void AddBufferToScope(GstBuffer *buf, const int pipeline_id, const QString &format); - void FadeoutFinished(); + void FadeoutFinished(const int pipeline_id); void FadeoutPauseFinished(); void SeekNow(); - void PlayDone(const GstStateChangeReturn ret, const quint64, const int); + void PlayDone(const GstStateChangeReturn ret, const bool pause, const quint64 offset_nanosec, const int pipeline_id); void BufferingStarted(); void BufferingProgress(int percent); void BufferingFinished(); + void PipelineFinished(const int pipeline_id); + private: QByteArray FixupUrl(const QUrl &url); void StartFadeout(); void StartFadeoutPause(); + void StopFadeoutPause(); void StartTimers(); void StopTimers(); - SharedPtr CreatePipeline(); - SharedPtr CreatePipeline(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 end_nanosec, const double ebur128_loudness_normalizing_gain_db); + GstEnginePipelinePtr CreatePipeline(); + GstEnginePipelinePtr CreatePipeline(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 end_nanosec, const double ebur128_loudness_normalizing_gain_db); + + void FinishPipeline(GstEnginePipelinePtr pipeline); void UpdateScope(int chunk_length); @@ -142,6 +148,8 @@ class GstEngine : public EngineBase, public GstBufferConsumer { static void StreamDiscoveryFinished(GstDiscoverer*, gpointer); static QString GSTdiscovererErrorMessage(GstDiscovererResult result); + bool ExclusivePipelineActive() const; + private: SharedPtr task_manager_; GstStartup *gst_startup_; @@ -149,9 +157,10 @@ class GstEngine : public EngineBase, public GstBufferConsumer { int buffering_task_id_; - SharedPtr current_pipeline_; - SharedPtr fadeout_pipeline_; - SharedPtr fadeout_pause_pipeline_; + GstEnginePipelinePtr current_pipeline_; + QMap fadeout_pipelines_; + GstEnginePipelinePtr fadeout_pause_pipeline_; + QMap old_pipelines_; QList buffer_consumers_; @@ -171,8 +180,7 @@ class GstEngine : public EngineBase, public GstBufferConsumer { int timer_id_; - bool is_fading_out_to_pause_; - bool has_faded_out_; + bool has_faded_out_to_pause_; int scope_chunk_; bool have_new_buffer_; @@ -181,6 +189,10 @@ class GstEngine : public EngineBase, public GstBufferConsumer { int discovery_finished_cb_id_; int discovery_discovered_cb_id_; + + State delayed_state_; + bool delayed_state_pause_; + quint64 delayed_state_offset_nanosec_; }; #endif // GSTENGINE_H diff --git a/src/engine/gstenginepipeline.cpp b/src/engine/gstenginepipeline.cpp index f714d4c4d..e17cf10f0 100644 --- a/src/engine/gstenginepipeline.cpp +++ b/src/engine/gstenginepipeline.cpp @@ -49,6 +49,7 @@ #include #include #include +#include #include #include #include @@ -64,6 +65,12 @@ namespace { +constexpr int GST_PLAY_FLAG_VIDEO = 0x00000001; +constexpr int GST_PLAY_FLAG_AUDIO = 0x00000002; +constexpr int GST_PLAY_FLAG_DOWNLOAD = 0x00000080; +constexpr int GST_PLAY_FLAG_BUFFERING = 0x00000100; +constexpr int GST_PLAY_FLAG_SOFT_VOLUME = 0x00000010; + constexpr int kGstStateTimeoutNanosecs = 10000000; constexpr int kFaderFudgeMsec = 2000; @@ -109,6 +116,7 @@ GstEnginePipeline::GstEnginePipeline(QObject *parent) ignore_tags_(false), pipeline_connected_(false), pipeline_active_(false), + pending_state_(GST_STATE_NULL), pending_seek_nanosec_(-1), last_known_position_ns_(0), next_uri_set_(false), @@ -141,7 +149,9 @@ GstEnginePipeline::GstEnginePipeline(QObject *parent) about_to_finish_cb_id_(-1), notify_volume_cb_id_(-1), logged_unsupported_analyzer_format_(false), - about_to_finish_(false) { + about_to_finish_(false), + finish_requested_(false), + finished_(false) { eq_band_gains_.reserve(kEqBandCount); for (int i = 0; i < kEqBandCount; ++i) eq_band_gains_ << 0; @@ -150,78 +160,22 @@ GstEnginePipeline::GstEnginePipeline(QObject *parent) GstEnginePipeline::~GstEnginePipeline() { + Disconnect(); + if (pipeline_) { - - if (fader_) { - if (fader_->state() != QTimeLine::NotRunning) { - fader_->stop(); - } - fader_.reset(); + if (state() != GST_STATE_NULL) { + gst_element_set_state(pipeline_, GST_STATE_NULL); } - - if (element_added_cb_id_ != -1) { - g_signal_handler_disconnect(G_OBJECT(audiobin_), element_added_cb_id_); - } - - if (element_removed_cb_id_ != -1) { - g_signal_handler_disconnect(G_OBJECT(audiobin_), element_removed_cb_id_); - } - - if (pad_added_cb_id_ != -1) { - g_signal_handler_disconnect(G_OBJECT(pipeline_), pad_added_cb_id_); - } - - if (notify_source_cb_id_ != -1) { - g_signal_handler_disconnect(G_OBJECT(pipeline_), notify_source_cb_id_); - } - - if (about_to_finish_cb_id_ != -1) { - g_signal_handler_disconnect(G_OBJECT(pipeline_), about_to_finish_cb_id_); - } - - if (notify_volume_cb_id_ != -1) { - g_signal_handler_disconnect(G_OBJECT(volume_), notify_volume_cb_id_); - } - - if (upstream_events_probe_cb_id_ != 0) { - GstPad *pad = gst_element_get_static_pad(eventprobe_, "src"); - if (pad) { - gst_pad_remove_probe(pad, upstream_events_probe_cb_id_); - gst_object_unref(pad); - } - } - - if (buffer_probe_cb_id_ != 0) { - GstPad *pad = gst_element_get_static_pad(audioqueueconverter_, "src"); - if (pad) { - gst_pad_remove_probe(pad, buffer_probe_cb_id_); - gst_object_unref(pad); - } - } - - { - GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline_)); - if (bus) { - gst_bus_remove_watch(bus); - gst_bus_set_sync_handler(bus, nullptr, nullptr, nullptr); - gst_object_unref(bus); - } - } - - gst_element_set_state(pipeline_, GST_STATE_NULL); - gst_object_unref(GST_OBJECT(pipeline_)); - pipeline_ = nullptr; - if (audiobin_ && !pipeline_connected_) { gst_object_unref(GST_OBJECT(audiobin_)); } - audiobin_ = nullptr; - } + qLog(Debug) << "Pipeline" << id_ << "deleted"; + } void GstEnginePipeline::set_output_device(const QString &output, const QVariant &device) { @@ -348,6 +302,97 @@ GstElement *GstEnginePipeline::CreateElement(const QString &factory_name, const } +void GstEnginePipeline::Disconnect() { + + if (pipeline_) { + + if (fader_) { + if (fader_->state() != QTimeLine::NotRunning) { + fader_->stop(); + } + fader_.reset(); + } + + if (element_added_cb_id_ != -1) { + g_signal_handler_disconnect(G_OBJECT(audiobin_), element_added_cb_id_); + element_added_cb_id_ = -1; + } + + if (element_removed_cb_id_ != -1) { + g_signal_handler_disconnect(G_OBJECT(audiobin_), element_removed_cb_id_); + element_removed_cb_id_ = -1; + } + + if (pad_added_cb_id_ != -1) { + g_signal_handler_disconnect(G_OBJECT(pipeline_), pad_added_cb_id_); + pad_added_cb_id_ = -1; + } + + if (notify_source_cb_id_ != -1) { + g_signal_handler_disconnect(G_OBJECT(pipeline_), notify_source_cb_id_); + notify_source_cb_id_ = -1; + } + + if (about_to_finish_cb_id_ != -1) { + g_signal_handler_disconnect(G_OBJECT(pipeline_), about_to_finish_cb_id_); + about_to_finish_cb_id_ = -1; + } + + if (notify_volume_cb_id_ != -1) { + g_signal_handler_disconnect(G_OBJECT(volume_), notify_volume_cb_id_); + notify_volume_cb_id_ = -1; + } + + if (upstream_events_probe_cb_id_ != 0) { + GstPad *pad = gst_element_get_static_pad(eventprobe_, "src"); + if (pad) { + gst_pad_remove_probe(pad, upstream_events_probe_cb_id_); + gst_object_unref(pad); + } + upstream_events_probe_cb_id_ = 0; + } + + if (buffer_probe_cb_id_ != 0) { + GstPad *pad = gst_element_get_static_pad(audioqueueconverter_, "src"); + if (pad) { + gst_pad_remove_probe(pad, buffer_probe_cb_id_); + gst_object_unref(pad); + } + buffer_probe_cb_id_ = 0; + } + + { + GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline_)); + if (bus) { + gst_bus_remove_watch(bus); + gst_bus_set_sync_handler(bus, nullptr, nullptr, nullptr); + gst_object_unref(bus); + } + } + + } + +} + +bool GstEnginePipeline::Finish() { + + qLog(Debug) << "Finishing pipeline" << id_; + + finish_requested_ = true; + + Disconnect(); + + if (state() == GST_STATE_NULL) { + finished_ = true; + } + else { + SetStateAsync(GST_STATE_NULL); + } + + return finished_; + +} + 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; @@ -395,9 +440,9 @@ bool GstEnginePipeline::InitFromUrl(const QUrl &media_url, const QUrl &stream_ur gint flags = 0; g_object_get(G_OBJECT(pipeline_), "flags", &flags, nullptr); - flags |= 0x00000002; - flags &= ~0x00000001; - flags &= ~0x00000010; + flags |= GST_PLAY_FLAG_AUDIO; + flags &= ~GST_PLAY_FLAG_VIDEO; + 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); @@ -434,10 +479,13 @@ bool GstEnginePipeline::InitAudioBin(QString &error) { #else case QVariant::String:{ #endif - QString device = device_.toString(); + const QString device = device_.toString(); if (!device.isEmpty()) { qLog(Debug) << "Setting device" << device << "for" << output_; g_object_set(G_OBJECT(audiosink_), "device", device.toUtf8().constData(), nullptr); + if (output_ == QLatin1String(GstEngine::kALSASink) && (device.startsWith(QLatin1String("hw:")) || device.startsWith(QLatin1String("plughw:")))) { + exclusive_mode_ = true; + } } break; } @@ -607,6 +655,9 @@ bool GstEnginePipeline::InitAudioBin(QString &error) { if (!volume_fading_) { return false; } + if (fader_) { + SetFaderVolume(fader_->currentValue()); + } } // Create the stereo balancer elements if it's enabled. @@ -1026,9 +1077,10 @@ void GstEnginePipeline::SourceSetupCallback(GstElement *playbin, GstElement *sou // If the pipeline was buffering we stop that now. if (instance->buffering_) { + qLog(Debug) << "Buffering finished"; instance->buffering_ = false; emit instance->BufferingFinished(); - instance->SetState(GST_STATE_PLAYING); + instance->SetStateAsync(GST_STATE_PLAYING); } } @@ -1254,7 +1306,7 @@ GstPadProbeReturn GstEnginePipeline::BufferProbeCallback(GstPad *pad, GstPadProb consumers = instance->buffer_consumers_; } - for (GstBufferConsumer *consumer : consumers) { + for (GstBufferConsumer *consumer : std::as_const(consumers)) { gst_buffer_ref(buf); consumer->ConsumeBuffer(buf, instance->id(), format); } @@ -1521,7 +1573,7 @@ void GstEnginePipeline::TagMessageReceived(GstMessage *msg) { } if (!title_splitted.isEmpty() && title_splitted.count() >= 2) { int i = 0; - for (const QString &title_part : title_splitted) { + for (const QString &title_part : std::as_const(title_splitted)) { ++i; switch (i) { case 1: @@ -1594,11 +1646,13 @@ void GstEnginePipeline::StateChangedMessageReceived(GstMessage *msg) { if (next_uri_reset_ && 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_); - SetStateDelayed(GST_STATE_PLAYING); + pending_seek_nanosec_ = -1; } else { - SeekQueued(pending_seek_nanosec_); + SeekAsync(pending_seek_nanosec_); + pending_seek_nanosec_ = -1; } } } @@ -1612,16 +1666,30 @@ void GstEnginePipeline::StateChangedMessageReceived(GstMessage *msg) { 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."; - SetState(GST_STATE_PLAYING); + SetStateAsync(GST_STATE_PLAYING); } else { qLog(Debug) << "Reverting next uri and going to paused state."; next_uri_reset_ = true; - SetState(GST_STATE_PAUSED); + SetStateAsync(GST_STATE_PAUSED); } } } + if (pipeline_active_) { + if (pending_seek_nanosec_ != -1 && new_state == GST_STATE_PAUSED) { + SeekAsync(pending_seek_nanosec_); + } + else if (pending_state_ != GST_STATE_NULL) { + SetStateAsync(pending_state_); + pending_state_ = GST_STATE_NULL; + } + if (fader_ && fader_->state() != QTimeLine::State::Running && new_state == GST_STATE_PLAYING) { + qLog(Debug) << "Resuming fader"; + ResumeFaderAsync(); + } + } + } void GstEnginePipeline::BufferingMessageReceived(GstMessage *msg) { @@ -1637,16 +1705,18 @@ void GstEnginePipeline::BufferingMessageReceived(GstMessage *msg) { const GstState current_state = state(); if (percent == 0 && current_state == GST_STATE_PLAYING && !buffering_) { + qLog(Debug) << "Buffering started"; buffering_ = true; emit BufferingStarted(); - SetState(GST_STATE_PAUSED); + SetStateAsync(GST_STATE_PAUSED); } else if (percent == 100 && buffering_) { + qLog(Debug) << "Buffering finished"; buffering_ = false; emit BufferingFinished(); - SetState(GST_STATE_PLAYING); + SetStateAsync(GST_STATE_PLAYING); } else if (buffering_) { emit BufferingProgress(percent); @@ -1687,18 +1757,54 @@ GstState GstEnginePipeline::state() const { } -QFuture GstEnginePipeline::SetState(const GstState state) { +QFuture GstEnginePipeline::SetStateAsync(const GstState state) { - qLog(Debug) << "Setting pipeline state to" << GstStateText(state); - return QtConcurrent::run(&set_state_threadpool_, &gst_element_set_state, pipeline_, 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(); + watcher->deleteLater(); + SetStateAsyncFinished(state, state_change); + }); + QFuture future = QtConcurrent::run(&set_state_threadpool_, &gst_element_set_state, pipeline_, state); + watcher->setFuture(future); + + return future; } -void GstEnginePipeline::SetStateDelayed(const GstState state) { +void GstEnginePipeline::SetStateAsyncFinished(const GstState state, const GstStateChangeReturn state_change) { - QMetaObject::invokeMethod(this, [this, state]() { - QTimer::singleShot(300, this, [this, state]() { SetState(state); }); - }, Qt::QueuedConnection); + switch (state_change) { + 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); + emit SetStateFinished(state_change); + if (!finished_ && finish_requested_) { + finished_ = true; + emit Finished(); + } + break; + case GST_STATE_CHANGE_FAILURE: + qLog(Error) << "Failed to set pipeline to state" << GstStateText(state); + break; + } + +} + +QFuture GstEnginePipeline::Play(const bool pause, const quint64 offset_nanosec) { + + if (offset_nanosec != 0) { + pending_seek_nanosec_ = static_cast(offset_nanosec); + } + + if (!pause) { + pending_state_ = GST_STATE_PLAYING; + } + + return SetStateAsync(GST_STATE_PAUSED); } @@ -1716,7 +1822,7 @@ bool GstEnginePipeline::Seek(const qint64 nanosec) { if (next_uri_set_) { pending_seek_nanosec_ = nanosec; - SetState(GST_STATE_READY); + SetStateAsync(GST_STATE_READY); return true; } @@ -1725,11 +1831,22 @@ bool GstEnginePipeline::Seek(const qint64 nanosec) { qLog(Debug) << "Seeking to" << nanosec; - return gst_element_seek_simple(pipeline_, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH, nanosec); + const bool success = gst_element_seek_simple(pipeline_, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH, 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_); + pending_state_ = GST_STATE_NULL; + } + } + + return success; } -void GstEnginePipeline::SeekQueued(const qint64 nanosec) { +void GstEnginePipeline::SeekAsync(const qint64 nanosec) { QMetaObject::invokeMethod(this, "Seek", Qt::QueuedConnection, Q_ARG(qint64, nanosec)); @@ -1865,17 +1982,32 @@ void GstEnginePipeline::StartFader(const qint64 duration_nanosec, const QTimeLin fader_->setDirection(direction); fader_->setEasingCurve(shape); fader_->setCurrentTime(static_cast(start_time)); - fader_->resume(); fader_fudge_timer_.stop(); use_fudge_timer_ = use_fudge_timer; SetFaderVolume(fader_->currentValue()); + qLog(Debug) << "Pipeline" << id_ << "with state" << GstStateText(state()) << "set to fade from" << start_time; + + if (pipeline_active_) { + fader_->resume(); + } + +} + +void GstEnginePipeline::ResumeFaderAsync() { + + if (fader_) { + QMetaObject::invokeMethod(&*fader_, "resume", Qt::QueuedConnection); + } + } void GstEnginePipeline::FaderTimelineFinished() { + qLog(Debug) << "Pipeline" << id_ << "finished fading"; + 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. @@ -1895,7 +2027,7 @@ void GstEnginePipeline::timerEvent(QTimerEvent *e) { if (e->timerId() == fader_fudge_timer_.timerId()) { fader_fudge_timer_.stop(); - emit FaderFinished(); + emit FaderFinished(id_); return; } diff --git a/src/engine/gstenginepipeline.h b/src/engine/gstenginepipeline.h index 18a4c7317..d501cf27b 100644 --- a/src/engine/gstenginepipeline.h +++ b/src/engine/gstenginepipeline.h @@ -46,6 +46,7 @@ #include "core/shared_ptr.h" #include "enginemetadata.h" +class QTimer; class QTimerEvent; class GstBufferConsumer; struct GstPlayBin; @@ -80,6 +81,8 @@ class GstEnginePipeline : public QObject { void set_spotify_login(const QString &spotify_username, const QString &spotify_password); #endif + bool Finish(); + // Creates the pipeline, returns false on error bool 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); @@ -89,10 +92,10 @@ class GstEnginePipeline : public QObject { void RemoveAllBufferConsumers(); // Control the music playback - Q_INVOKABLE QFuture SetState(const GstState state); - void SetStateDelayed(const GstState state); + Q_INVOKABLE QFuture SetStateAsync(const GstState state); + Q_INVOKABLE QFuture Play(const bool pause, const quint64 offset_nanosec); Q_INVOKABLE bool Seek(const qint64 nanosec); - void SeekQueued(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); @@ -133,6 +136,8 @@ class GstEnginePipeline : public QObject { QString source_device() const { return source_device_; } + bool exclusive_mode() const { return exclusive_mode_; } + public slots: void SetFaderVolume(const qreal volume); @@ -143,7 +148,7 @@ class GstEnginePipeline : public QObject { void MetadataFound(const int pipeline_id, const EngineMetadata &bundle); void VolumeChanged(const uint volume); - void FaderFinished(); + void FaderFinished(const int pipeline_id); void BufferingStarted(); void BufferingProgress(const int percent); @@ -151,6 +156,10 @@ class GstEnginePipeline : public QObject { void AboutToFinish(); + void Finished(); + + void SetStateFinished(const GstStateChangeReturn state); + protected: void timerEvent(QTimerEvent*) override; @@ -189,7 +198,12 @@ class GstEnginePipeline : public QObject { void UpdateStereoBalance(); void UpdateEqualizer(); + void Disconnect(); + + void ResumeFaderAsync(); + private slots: + void SetStateAsyncFinished(const GstState state, const GstStateChangeReturn state_change); void FaderTimelineFinished(); private: @@ -290,6 +304,8 @@ class GstEnginePipeline : public QObject { // Also, we have to wait for the playbin to be connected. bool pipeline_connected_; bool pipeline_active_; + + GstState pending_state_; qint64 pending_seek_nanosec_; // We can only use gst_element_query_position() when the pipeline is in @@ -342,6 +358,10 @@ class GstEnginePipeline : public QObject { bool about_to_finish_; + bool finish_requested_; + bool finished_; }; +using GstEnginePipelinePtr = SharedPtr; + #endif // GSTENGINEPIPELINE_H diff --git a/src/engine/vlcengine.cpp b/src/engine/vlcengine.cpp index b652e70ae..ceab1fcbc 100644 --- a/src/engine/vlcengine.cpp +++ b/src/engine/vlcengine.cpp @@ -124,7 +124,9 @@ bool VLCEngine::Load(const QUrl &media_url, const QUrl &stream_url, const Engine } -bool VLCEngine::Play(const quint64 offset_nanosec) { +bool VLCEngine::Play(const bool pause, const quint64 offset_nanosec) { + + Q_UNUSED(pause); if (!Initialized()) return false; diff --git a/src/engine/vlcengine.h b/src/engine/vlcengine.h index 04e3712fb..8ed09b85e 100644 --- a/src/engine/vlcengine.h +++ b/src/engine/vlcengine.h @@ -52,7 +52,7 @@ class VLCEngine : public EngineBase { bool Init() override; EngineBase::State state() const override { return state_; } bool Load(const QUrl &media_url, const QUrl &stream_url, const EngineBase::TrackChangeFlags change, const bool force_stop_at_end, const quint64 beginning_nanosec, const qint64 end_nanosec, const std::optional ebur128_integrated_loudness_lufs) override; - bool Play(const quint64 offset_nanosec) override; + bool Play(const bool pause, const quint64 offset_nanosec) override; void Stop(const bool stop_after = false) override; void Pause() override; void Unpause() override;