diff --git a/StreamSink.h b/StreamSink.h index 2951699021..41f5111470 100644 --- a/StreamSink.h +++ b/StreamSink.h @@ -142,6 +142,18 @@ class StreamSink */ virtual void Flush(double position = 0, int rate = AAMP_NORMAL_PLAY_RATE, bool shouldTearDown = true, bool keepPausedSeek = false){} + /** + * @brief RDKEMW-21923: query the underlying pipeline GstState. + * @param[out] currentState current pipeline state + * @param[out] pendingState pending pipeline state + * @return GstStateChangeReturn from gst_element_get_state (default: SUCCESS/no-op) + */ + virtual GstStateChangeReturn GetPipelineState(GstState *currentState, GstState *pendingState) { + // Default: assume pipeline is healthy (PLAYING). Concrete overrides query real state. + if (currentState) *currentState = GST_STATE_PLAYING; + if (pendingState) *pendingState = GST_STATE_VOID_PENDING; + return GST_STATE_CHANGE_SUCCESS; +} /** * @brief Flush the audio playbin * @param[in] position - playback position diff --git a/aampgstplayer.cpp b/aampgstplayer.cpp index 32a3f24a5e..23678ea7d9 100644 --- a/aampgstplayer.cpp +++ b/aampgstplayer.cpp @@ -1483,3 +1483,15 @@ bool AAMPGstPlayer::SendSample(AampMediaType mediaType, AampMediaSample&& sample std::move(sample.mDrmMetadata) }); } + +/** + * @brief Get the current and pending state of the GStreamer pipeline + * + * @param[out] currentState - Pointer to store the current state of the pipeline + * @param[out] pendingState - Pointer to store the pending state of the pipeline + * @return GstStateChangeReturn indicating the result of the state query + */ +GstStateChangeReturn AAMPGstPlayer::GetPipelineState(GstState *currentState, GstState *pendingState) +{ + return playerInstance->GetPipelineState(currentState, pendingState); +} \ No newline at end of file diff --git a/aampgstplayer.h b/aampgstplayer.h index 86d390b59e..4012fd3eda 100644 --- a/aampgstplayer.h +++ b/aampgstplayer.h @@ -441,6 +441,14 @@ class AAMPGstPlayer : public StreamSink * @param[in] codecInfo - Codec information */ void SetStreamCaps(AampMediaType type, MediaCodecInfo&& codecInfo) override; + + /** + * @brief Get the stream capabilities for a given media type + * + * @param[in] type - Media type + * @return const reference to MediaCodecInfo for the specified media type + */ + GstStateChangeReturn GetPipelineState(GstState *currentState, GstState *pendingState) override; private: std::mutex mBufferingLock; diff --git a/main_aamp.cpp b/main_aamp.cpp index fc6e19b654..24f91d165d 100755 --- a/main_aamp.cpp +++ b/main_aamp.cpp @@ -762,6 +762,87 @@ void PlayerInstanceAAMP::SetRateInternal(float rate,int overshootcorrection) bool isPipelinePaused = aamp->mSinkPaused.load(); if ((!isPipelinePaused && rate == aamp->rate && !aamp->GetPauseOnFirstVideoFrameDisp()) || (rate == 0 && isPipelinePaused)) { + // RDKEMW-21923: During rapid scrubbing (e.g. 1x->2x->1x->PAUSE within ~1s), + // TuneHelper may still be rebuilding the pipeline (NULL/READY state). + // If we skip here, downstream Pause()/Resume() will race with the rebuild + // and cause a PAUSED->PAUSED wedge. Detect and bail out early. + if (!isPipelinePaused && rate == aamp->rate && rate == AAMP_NORMAL_PLAY_RATE) + { + StreamSink *sink = AampStreamSinkManager::GetInstance().GetStreamSink(aamp); + if (sink) + { + GstState curState = GST_STATE_VOID_PENDING; + GstState pendState = GST_STATE_VOID_PENDING; + sink->GetPipelineState(&curState, &pendState); + if (curState == GST_STATE_NULL || curState == GST_STATE_VOID_PENDING || curState == GST_STATE_READY) + { + AAMPLOG_WARN("SetRateInternal: rate=%.1f but pipeline in state %d " + "(rebuild in progress) — skipping, TuneHelper will complete", rate, curState); + return; + } + } + } + + if (!isPipelinePaused && rate == aamp->rate && rate == AAMP_NORMAL_PLAY_RATE) + { + StreamSink *sink = AampStreamSinkManager::GetInstance().GetStreamSink(aamp); + if (sink) + { + GstState curState; + GstState pendState; + sink->GetPipelineState(&curState, &pendState); + + if (curState == GST_STATE_PLAYING) + { + // Pipeline is genuinely PLAYING — safe to skip + AAMPLOG_WARN("SetRateInternal: pipeline confirmed PLAYING (state=%d), safe to skip", curState); + } + else if (curState == GST_STATE_NULL || curState == GST_STATE_VOID_PENDING) + { + // Pipeline destroyed — teardown/rebuild in progress by another TuneHelper + // Do NOT re-seek; let the active TuneHelper complete naturally + AAMPLOG_WARN("SetRateInternal: pipeline is NULL/VOID (state=%d), " + "teardown in progress — skipping recovery", curState); + return; + } + else if (curState == GST_STATE_PAUSED) + { + // Pipeline stuck in PAUSED despite mSinkPaused=false (state drift) + if (!sink->Pause(false, false)) + { + AAMPLOG_WARN("SetRateInternal: pipeline stuck in PAUSED, " + "resume failed; recovering via re-seek"); + aamp->SetState(eSTATE_SEEKING); + aamp->seek_pos_seconds = aamp->GetPositionSeconds(); + aamp->rate = AAMP_NORMAL_PLAY_RATE; + aamp->mSinkPaused = false; + { + std::lock_guard lock(aamp->GetStreamLock()); + aamp->TuneHelper(eTUNETYPE_SEEK, false); + } + aamp->NotifySpeedChanged(aamp->rate, false); + aamp->ResumeDownloads(); + return; + } + else + { + // Un-pause succeeded — pipeline was just lagging + AAMPLOG_WARN("SetRateInternal: pipeline was PAUSED (drift), un-pause succeeded"); + aamp->NotifyFirstBufferProcessed(sink->GetVideoRectangle()); + return; + } + } + else if (curState == GST_STATE_READY) + { + // Pipeline in READY — being rebuilt by TuneHelper + AAMPLOG_WARN("SetRateInternal: pipeline in READY (state=%d), " + "rebuild in progress — skipping recovery", curState); + return; + } + // else: unknown state — fall through to original early-return + } + } + AAMPLOG_WARN("Already running at playback rate(%f) mSinkPaused(%d), hence skipping set rate for (%f)", aamp->rate, isPipelinePaused, rate); return; } @@ -888,6 +969,44 @@ void PlayerInstanceAAMP::SetRateInternal(float rate,int overshootcorrection) if (aamp->mSinkPaused.load() && rate != 0) { AAMPLOG_INFO("Resuming Playback at Position '%lld'.", aamp->GetPositionMilliseconds()); + + // RDKEMW-21923: If pipeline is NULL/READY (mid-rebuild from a prior + // TuneHelper), do NOT attempt resume — it will fail and wedge. + // Instead, clear mSinkPaused and let TuneHelper's rebuild naturally + // transition to PLAYING. + StreamSink *sinkGuard = AampStreamSinkManager::GetInstance().GetStreamSink(aamp); + if (sinkGuard) + { + GstState guardCur = GST_STATE_VOID_PENDING; + GstState guardPend = GST_STATE_VOID_PENDING; + sinkGuard->GetPipelineState(&guardCur, &guardPend); + + if (guardCur == GST_STATE_NULL || guardCur == GST_STATE_VOID_PENDING || guardCur == GST_STATE_READY) + { + AAMPLOG_WARN("SetRateInternal: resume requested but pipeline in state %d " + "(rebuild in progress) — deferring, TuneHelper will resume", guardCur); + aamp->mSinkPaused = false; + aamp->ResumeDownloads(); + return; + } + } + + if (aamp->IsPipelineWedged()) + { + AAMPLOG_WARN("SetRateInternal: pipeline wedged, recovering via re-seek instead of resume"); + aamp->SetState(eSTATE_SEEKING); + aamp->seek_pos_seconds = aamp->GetPositionSeconds(); + aamp->rate = AAMP_NORMAL_PLAY_RATE; + aamp->mSinkPaused = false; + { + std::lock_guard lock(aamp->GetStreamLock()); + aamp->TuneHelper(eTUNETYPE_SEEK, false); + } + aamp->NotifySpeedChanged(aamp->rate, false); + aamp->ResumeDownloads(); + return; + } + // Resuming payback from pause // If have local TSB, but playing from Live then seek into the TSB // Otherwise unpause the pipeline @@ -923,15 +1042,39 @@ void PlayerInstanceAAMP::SetRateInternal(float rate,int overshootcorrection) // rapid trickplay->seek->resume race - seen on both VOD and FOG-TSB linear). // Recover by re-seeking to the current position: the same mechanism the // local-TSB resume path uses. Works uniformly for VOD, FOG-TSB & local TSB. - AAMPLOG_WARN("SetRateInternal: pipeline resume failed (GstState timeout); recovering via re-seek"); + long long nowMs = NOW_STEADY_TS_MS; + if ((nowMs - aamp->mLastRecoveryTimeMs) > PrivateInstanceAAMP::RECOVERY_COOLDOWN_MS) + { + aamp->mRecoveryAttemptCount = 0; // healthy gap since last recovery -> reset + } + aamp->mLastRecoveryTimeMs = nowMs; + aamp->SetState(eSTATE_SEEKING); aamp->seek_pos_seconds = aamp->GetPositionSeconds(); aamp->rate = AAMP_NORMAL_PLAY_RATE; aamp->mSinkPaused = false; + + if (aamp->mRecoveryAttemptCount >= PrivateInstanceAAMP::MAX_RECOVERY_ATTEMPTS) + { + // Repeated resume-timeouts: stop re-seeking, force a full retune once. + AAMPLOG_ERR("SetRateInternal: resume recovery exceeded %d attempts; escalating to full retune", + PrivateInstanceAAMP::MAX_RECOVERY_ATTEMPTS); + aamp->mRecoveryAttemptCount = 0; + { + std::lock_guard lock(aamp->GetStreamLock()); + aamp->TuneHelper(eTUNETYPE_RETUNE, false); + } + } + else + { + aamp->mRecoveryAttemptCount++; + AAMPLOG_WARN("SetRateInternal: pipeline resume failed (GstState timeout); recovering via re-seek (attempt %d/%d) ",aamp->mRecoveryAttemptCount, PrivateInstanceAAMP::MAX_RECOVERY_ATTEMPTS); + { std::lock_guard lock(aamp->GetStreamLock()); aamp->TuneHelper(eTUNETYPE_SEEK, false); } + } // Skip common notification (like local-TSB path): state -> PLAYING // via NotifyFirstBufferProcessed once fragments arrive. @@ -940,8 +1083,36 @@ void PlayerInstanceAAMP::SetRateInternal(float rate,int overshootcorrection) } else { - // required since buffers are already cached in paused state - aamp->NotifyFirstBufferProcessed(sink ? sink->GetVideoRectangle() : std::string()); + if (!retValue && sink) + { + // RDKEMW-21923: Resume failed — check if pipeline is now wedged. + GstState cur = GST_STATE_VOID_PENDING; + GstState pend = GST_STATE_VOID_PENDING; + GstStateChangeReturn stRet = sink->GetPipelineState(&cur, &pend); + if (stRet == GST_STATE_CHANGE_FAILURE || + (stRet == GST_STATE_CHANGE_ASYNC && cur == pend)) + { + AAMPLOG_WARN("SetRateInternal: post-resume wedge detected " + "(state=%d pending=%d ret=%d), recovering via re-seek", + cur, pend, stRet); + aamp->SetState(eSTATE_SEEKING); + aamp->seek_pos_seconds = aamp->GetPositionSeconds(); + aamp->rate = AAMP_NORMAL_PLAY_RATE; + aamp->mSinkPaused = false; + { + std::lock_guard lock(aamp->GetStreamLock()); + aamp->TuneHelper(eTUNETYPE_SEEK, false); + } + aamp->NotifySpeedChanged(aamp->rate, false); + aamp->ResumeDownloads(); + return; + } + } + else + { + // required since buffers are already cached in paused state + aamp->NotifyFirstBufferProcessed(sink ? sink->GetVideoRectangle() : std::string()); + } } } } @@ -953,6 +1124,25 @@ void PlayerInstanceAAMP::SetRateInternal(float rate,int overshootcorrection) { if (!aamp->mSinkPaused.load()) { + + // RDKEMW-21923: Guard against pausing a pipeline that is mid-rebuild. + // Calling sink->Pause() on a NULL/READY pipeline triggers a PAUSED->PAUSED + // wedge once the rebuild completes. Defer: mark paused and return. + StreamSink *sinkCheck = AampStreamSinkManager::GetInstance().GetStreamSink(aamp); + if (sinkCheck) + { + GstState cur = GST_STATE_VOID_PENDING; + GstState pend = GST_STATE_VOID_PENDING; + sinkCheck->GetPipelineState(&cur, &pend); + if (cur == GST_STATE_NULL || cur == GST_STATE_VOID_PENDING || cur == GST_STATE_READY) + { + AAMPLOG_WARN("SetRateInternal: pause requested but pipeline in state %d " + "(rebuild in progress) — deferring pause", cur); + aamp->mSinkPaused = true; + return; + } + } + aamp->mpStreamAbstractionAAMP->NotifyPlaybackPaused(true); if (!aamp->IsLocalAAMPTsb()) { @@ -966,12 +1156,32 @@ void PlayerInstanceAAMP::SetRateInternal(float rate,int overshootcorrection) } } + if (aamp->IsPipelineWedged()) + { + AAMPLOG_WARN("SetRateInternal: pipeline wedged at pause-on-first-frame, skipping pause"); + // Don't attempt pause, just notify completion + return; + } + StreamSink *sink = AampStreamSinkManager::GetInstance().GetStreamSink(aamp); if (sink) { retValue = sink->Pause(true, false); } aamp->mSinkPaused = true; + + if (!retValue) + { + AAMPLOG_WARN("SetRateInternal: pause failed on first frame; recovering via re-seek"); + aamp->SetState(eSTATE_SEEKING); + aamp->seek_pos_seconds = aamp->GetPositionSeconds(); + aamp->mSinkPaused = true; + { + std::lock_guard lock(aamp->GetStreamLock()); + aamp->TuneHelper(eTUNETYPE_SEEK, false); + } + return; + } if(aamp->GetLLDashServiceData()->lowLatencyMode) { diff --git a/middleware/InterfacePlayerRDK.cpp b/middleware/InterfacePlayerRDK.cpp index bd90c9eab1..265e209149 100644 --- a/middleware/InterfacePlayerRDK.cpp +++ b/middleware/InterfacePlayerRDK.cpp @@ -5543,3 +5543,21 @@ static void DecorateGstBufferWithDrmMetadata(GstBuffer *buffer, const MediaDrmMe gst_buffer_add_protection_meta(buffer, metadata); } } +/** + * @brief Get the current state of the GStreamer pipeline. + * @param[out] currentState Pointer to store the current state of the pipeline. + * @param[out] pendingState Pointer to store the pending state of the pipeline. + * @return GstStateChangeReturn indicating the result of the state query. + */ +GstStateChangeReturn InterfacePlayerRDK::GetPipelineState(GstState *currentState, GstState *pendingState) +{ + if (interfacePlayerPriv->gstPrivateContext->pipeline) + { + return gst_element_get_state(interfacePlayerPriv->gstPrivateContext->pipeline, + currentState, pendingState, 100 * GST_MSECOND); + } + // No pipeline + if (currentState) *currentState = GST_STATE_NULL; + if (pendingState) *pendingState = GST_STATE_VOID_PENDING; + return GST_STATE_CHANGE_SUCCESS; +} diff --git a/middleware/InterfacePlayerRDK.h b/middleware/InterfacePlayerRDK.h index 515ccb7877..ccdfd0b547 100644 --- a/middleware/InterfacePlayerRDK.h +++ b/middleware/InterfacePlayerRDK.h @@ -782,6 +782,13 @@ class InterfacePlayerRDK */ void SetStreamCaps(GstMediaType type, MediaCodecInfo&& codecInfo); + /** + * @brief Gets the stream capabilities. + * @param[in] type The media type. + * @return The codec information for the specified media type. + */ + GstStateChangeReturn GetPipelineState(GstState *currentState, GstState *pendingState); + private: InterfacePlayerPriv *interfacePlayerPriv; }; diff --git a/priv_aamp.cpp b/priv_aamp.cpp index 4612a6eaa9..627c7b01b9 100644 --- a/priv_aamp.cpp +++ b/priv_aamp.cpp @@ -15155,3 +15155,57 @@ bool PrivateInstanceAAMP::IsAdPlaying() } return false; } + +// RDKEMW-21923: Detect pipeline wedge states, excluding NULL/destroyed pipelines +bool PrivateInstanceAAMP::IsPipelineWedged() +{ + StreamSink* sink = AampStreamSinkManager::GetInstance().GetStreamSink(this); + + if (sink) + { + GstState currentState; + GstState pendingState; + + GstStateChangeReturn ret = + sink->GetPipelineState(¤tState, &pendingState); + + // Pipeline NULL/VOID — not wedged, just destroyed/being rebuilt + if (currentState == GST_STATE_NULL || + currentState == GST_STATE_VOID_PENDING) + { + AAMPLOG_WARN("IsPipelineWedged: pipeline NULL/VOID (state=%d) — " + "not wedged, teardown/rebuild in progress", currentState); + return false; + } + + // Case 1: Wedged if async transition to the same state (PAUSED -> PAUSED) + if (ret == GST_STATE_CHANGE_ASYNC && + currentState == pendingState) + { + AAMPLOG_WARN("IsPipelineWedged: detected wedged pipeline " + "state=%d pending=%d", currentState, pendingState); + return true; + } + + // Case 2: State change failed entirely + if (ret == GST_STATE_CHANGE_FAILURE) + { + AAMPLOG_WARN("IsPipelineWedged: GST_STATE_CHANGE_FAILURE detected " + "state=%d pending=%d", currentState, pendingState); + return true; + } + + // Case 3: mSinkPaused=false but pipeline actually in PAUSED (state drift) + if (!mSinkPaused.load() && + ret == GST_STATE_CHANGE_SUCCESS && + currentState == GST_STATE_PAUSED) + { + AAMPLOG_WARN("IsPipelineWedged: state drift detected — " + "mSinkPaused=false but pipeline PAUSED " + "state=%d pending=%d", currentState, pendingState); + return true; + } + } + + return false; +} diff --git a/priv_aamp.h b/priv_aamp.h index f0393970bd..0cf18623f2 100644 --- a/priv_aamp.h +++ b/priv_aamp.h @@ -877,6 +877,11 @@ class PrivateInstanceAAMP : public DrmCallbacks, public std::enable_shared_from_ * This function is invoked continuously when ever there is an update in manifest */ void updateManifest(const char *manifestData); + + /** + * @brief This function is invoked by checking pipeline is getting into wedged + */ + bool IsPipelineWedged(); bool mDiscontinuityFound; int mTelemetryInterval; @@ -1038,6 +1043,10 @@ class PrivateInstanceAAMP : public DrmCallbacks, public std::enable_shared_from_ volatile std::atomic mPausePositionMilliseconds; /**< Requested pause position, can be 0 or more, or AAMP_PAUSE_POSITION_INVALID_POSITION */ MediaFormat mMediaFormat; double seek_pos_seconds; /**< indicates the playback position at which most recent playback activity began */ + int mRecoveryAttemptCount = 0; /**< RDKEMW-21923: consecutive SetRateInternal re-seek recovery attempts */ + long long mLastRecoveryTimeMs = 0; /**< RDKEMW-21923: steady-clock ms of the last recovery attempt */ + static constexpr int MAX_RECOVERY_ATTEMPTS = 3; /**< RDKEMW-21923: cap before escalating to full retune */ + static constexpr long long RECOVERY_COOLDOWN_MS = 5000; /**< RDKEMW-21923: window (ms) after which the recovery count resets */ float rate; /**< most recent (non-zero) play rate for non-paused content */ float playerrate; bool mSetPlayerRateAfterFirstframe;