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;