diff --git a/AampUnderflowMonitor.cpp b/AampUnderflowMonitor.cpp index fe3599237..869f2c342 100644 --- a/AampUnderflowMonitor.cpp +++ b/AampUnderflowMonitor.cpp @@ -90,20 +90,19 @@ void AampUnderflowMonitor::Start() { void AampUnderflowMonitor::Stop() { + // Use unique_lock so we can release the mutex before joining + std::unique_lock lock(mMutex); // Signal thread to stop mRunning.store(false); - - // Wait for thread to terminate + + // If a thread is joinable, release the lock before joining to avoid deadlock if (mThread.joinable()) { + lock.unlock(); mThread.join(); AAMPLOG_INFO("AampUnderflowMonitor thread joined"); + lock.lock(); } - - // Nullify pointers under mutex to prevent any race with thread cleanup - std::lock_guard lock(mMutex); - mAamp = nullptr; - mStream = nullptr; } void AampUnderflowMonitor::Run() @@ -125,8 +124,6 @@ void AampUnderflowMonitor::Run() { std::lock_guard lock(mMutex); - if (!mAamp) return; // Stop() was called - state = mAamp->GetState(); if (state == eSTATE_STOPPED || state == eSTATE_RELEASED || state == eSTATE_ERROR) { mRunning.store(false); @@ -139,12 +136,7 @@ void AampUnderflowMonitor::Run() if (shouldBreak) { break; } - - { - std::lock_guard lock(mMutex); - if (!mAamp) return; - mAamp->interruptibleMsSleep(100); - } + mAamp->interruptibleMsSleep(100); } while (mRunning.load()) { @@ -156,7 +148,6 @@ void AampUnderflowMonitor::Run() { std::lock_guard lock(mMutex); - if (!mAamp || !mStream) return; // Stop() was called underflowActive = mAamp->GetBufUnderFlowStatus(); playerState = mAamp->GetState(); @@ -184,7 +175,6 @@ void AampUnderflowMonitor::Run() { std::lock_guard lock(mMutex); - if (!mAamp) return; trackDownloadsEnabled = mAamp->TrackDownloadsAreEnabled(eMEDIATYPE_VIDEO); sinkCacheEmpty = mAamp->IsSinkCacheEmpty(eMEDIATYPE_VIDEO); } @@ -198,7 +188,6 @@ void AampUnderflowMonitor::Run() AAMPLOG_INFO("[video] underflow detected. buffered=%.3f cacheEmpty=%d (rate=%.2f, trickplay=%d, seeking=%d)", bufferedTimeSec, (int)sinkCacheEmpty, currentRate, (int)isTrickplay, (int)isSeekingState); std::lock_guard lock(mMutex); - if (!mAamp) return; mAamp->SetBufferingState(true); PlaybackErrorType errorType = eGST_ERROR_UNDERFLOW; mAamp->SendAnomalyEvent(ANOMALY_WARNING, "%s %s", GetMediaTypeName(eMEDIATYPE_VIDEO), mAamp->getStringForPlaybackError(errorType)); @@ -209,7 +198,6 @@ void AampUnderflowMonitor::Run() { AAMPLOG_WARN("[video] downloads blocked with empty cache during underflow; resuming"); std::lock_guard lock(mMutex); - if (!mAamp) return; mAamp->ResumeTrackDownloads(eMEDIATYPE_VIDEO); } } @@ -236,7 +224,6 @@ void AampUnderflowMonitor::Run() { AAMPLOG_INFO("[video] underflow ended. buffered=%.3f cacheEmpty=%d", bufferedTimeSec, (int)sinkCacheEmpty); std::lock_guard lock(mMutex); - if (!mAamp) return; mAamp->SetBufferingState(false); } else @@ -247,8 +234,6 @@ void AampUnderflowMonitor::Run() else if (underflowActive && !trackDownloadsEnabled && sinkCacheEmpty) { AAMPLOG_WARN("[video] underflow ongoing, downloads blocked and cache empty; resuming track downloads"); - std::lock_guard lock(mMutex); - if (!mAamp) return; mAamp->ResumeTrackDownloads(eMEDIATYPE_VIDEO); } } @@ -259,12 +244,7 @@ void AampUnderflowMonitor::Run() const int sleepMs = (bufferedTimeSec < kLowBufferSec) ? kLowBufferPollMs : (bufferedTimeSec >= kHighBufferSec) ? kHighBufferPollMs : kMediumBufferPollMs; - - { - std::lock_guard lock(mMutex); - if (!mAamp) return; - mAamp->interruptibleMsSleep(sleepMs); - } + mAamp->interruptibleMsSleep(sleepMs); } mRunning.store(false); } diff --git a/AampUnderflowMonitor.h b/AampUnderflowMonitor.h index 59890e522..9d45fdf13 100644 --- a/AampUnderflowMonitor.h +++ b/AampUnderflowMonitor.h @@ -37,7 +37,6 @@ class PrivateInstanceAAMP; class AampUnderflowMonitor { public: /** - * @fn AampUnderflowMonitor * @brief Construct an `AampUnderflowMonitor`. * @param[in] stream Stream abstraction used to query buffered video duration * and playback state relevant to underflow detection. @@ -53,31 +52,23 @@ class AampUnderflowMonitor { AampUnderflowMonitor(StreamAbstractionAAMP* stream, PrivateInstanceAAMP* aamp); /** - * @fn ~AampUnderflowMonitor * @brief Destructor. Ensures monitoring has been stopped. */ ~AampUnderflowMonitor(); - /** - * @fn Start - * @brief Start the monitoring thread. If already running, returns immediately. - * @return void - */ + /** + * @brief Start the monitoring thread. If already running, returns immediately. + * @return void + */ void Start(); /** - * @fn Stop - * @brief Request the monitoring thread to stop and join it if joinable. - * Safe to call multiple times. Nullifies internal pointers after - * thread termination to prevent use-after-free. - * @return void - * @note After `Stop()` returns, the monitoring thread has fully terminated - * and will not access `StreamAbstractionAAMP` or `PrivateInstanceAAMP`. + * @brief Stop and join the monitoring thread. + * @return void */ void Stop(); /** - * @fn isRunning * @brief Check whether the monitoring thread is currently active. * @return true if running, false otherwise. */ @@ -85,7 +76,6 @@ class AampUnderflowMonitor { private: /** - * @fn run * @brief Thread entry routine that polls/awaits underflow conditions * and triggers coordinated handling. */ diff --git a/StreamAbstractionAAMP.h b/StreamAbstractionAAMP.h index 0bb769906..a91c06b0c 100644 --- a/StreamAbstractionAAMP.h +++ b/StreamAbstractionAAMP.h @@ -2013,13 +2013,6 @@ class StreamAbstractionAAMP : public AampLicenseFetcher void ReinitializeInjection(double rate); protected: - /** - * Mutex used to serialize UnderflowMonitor lifecycle in const methods. - * Declared mutable to allow locking within const functions such as - * IsUnderflowMonitorRunning(). - */ - mutable std::mutex mUnderflowMonitorMutex; - /** * Underflow monitor instance owned by Stream; manages detection and * handling of underflow conditions. diff --git a/fragmentcollector_hls.cpp b/fragmentcollector_hls.cpp index 8b01dab5f..01d2bb99c 100644 --- a/fragmentcollector_hls.cpp +++ b/fragmentcollector_hls.cpp @@ -4977,6 +4977,16 @@ void StreamAbstractionAAMP_HLS::Start(void) track->Start(); } } + + // Start underflow monitor after successful initialization and Start() + if (mUnderflowMonitor) + { + StartUnderflowMonitor(); + if (!IsUnderflowMonitorRunning()) + { + AAMPLOG_WARN("UnderflowMonitor did not start; continuing without AampUnderflowMonitor"); + } + } } diff --git a/fragmentcollector_mpd.cpp b/fragmentcollector_mpd.cpp index 937a34015..8b238ffbe 100644 --- a/fragmentcollector_mpd.cpp +++ b/fragmentcollector_mpd.cpp @@ -10352,6 +10352,16 @@ void StreamAbstractionAAMP_MPD::Start(void) { StartLatencyMonitorThread(); } + + // Start underflow monitor after successful initialization and Start() + if (mUnderflowMonitor) + { + StartUnderflowMonitor(); + if (!IsUnderflowMonitorRunning()) + { + AAMPLOG_WARN("UnderflowMonitor did not start; continuing without AampUnderflowMonitor"); + } + } } /** diff --git a/fragmentcollector_progressive.cpp b/fragmentcollector_progressive.cpp index 424137847..a0ed72f4c 100644 --- a/fragmentcollector_progressive.cpp +++ b/fragmentcollector_progressive.cpp @@ -238,6 +238,16 @@ void StreamAbstractionAAMP_PROGRESSIVE::Start(void) { AAMPLOG_ERR("Failed to create FragmentCollector thread : %s", e.what()); } + + // Start underflow monitor after successful initialization and Start() + if (mUnderflowMonitor) + { + StartUnderflowMonitor(); + if (!IsUnderflowMonitorRunning()) + { + AAMPLOG_WARN("UnderflowMonitor did not start; continuing without AampUnderflowMonitor"); + } + } } /** diff --git a/priv_aamp.cpp b/priv_aamp.cpp index 21825c338..2712b26a0 100644 --- a/priv_aamp.cpp +++ b/priv_aamp.cpp @@ -3209,19 +3209,19 @@ void PrivateInstanceAAMP::UpdateRefreshPlaylistInterval(float maxIntervalSecs) /** * @brief Sends UnderFlow Event messages */ -void PrivateInstanceAAMP::SendBufferChangeEvent(bool bufferingStopped) +void PrivateInstanceAAMP::SendBufferChangeEvent(bool bufferingStart) { // Buffer Change event indicate buffer availability - // Buffering stop notification need to be inverted to indicate if buffer available or not // BufferChangeEvent with False = Underflow / non-availability of buffer to play // BufferChangeEvent with True = Availability of buffer to play - BufferingChangedEventPtr e = std::make_shared(!bufferingStopped, GetSessionId()); + bool bufferAvailable = !bufferingStart; // Buffering stop notification need to be inverted to indicate if buffer available or not + BufferingChangedEventPtr e = std::make_shared(bufferAvailable, GetSessionId()); - SetBufUnderFlowStatus(bufferingStopped); + SetBufUnderFlowStatus(bufferingStart); AAMPLOG_INFO("PrivateInstanceAAMP: Sending Buffer Change event status (Buffering): %s", (e->buffering() ? "End": "Start")); #ifdef AAMP_TELEMETRY_SUPPORT AAMPTelemetry2 at2(mAppName); - std::string telemetryName = bufferingStopped?"VideoBufferingStart":"VideoBufferingEnd"; + std::string telemetryName = bufferingStart?"VideoBufferingStart":"VideoBufferingEnd"; at2.send(telemetryName,{/*int data*/},{/*string data*/},{/*float data*/}); #endif //AAMP_TELEMETRY_SUPPORT SendEvent(e,AAMP_EVENT_ASYNC_MODE); @@ -3232,25 +3232,28 @@ void PrivateInstanceAAMP::SendBufferChangeEvent(bool bufferingStopped) */ void PrivateInstanceAAMP::SetBufferingState(bool buffering) { - if (buffering) + if(ISCONFIGSET_PRIV(eAAMPConfig_ReportBufferEvent)) { - SendBufferChangeEvent(true); - if (!mSinkPaused.load()) + if (buffering) { - if (!PausePipeline(true, true)) + SendBufferChangeEvent(true); + if (!mSinkPaused.load()) { - AAMPLOG_ERR("Failed to pause the Pipeline"); + if (!PausePipeline(true, true)) + { + AAMPLOG_ERR("Failed to pause the Pipeline"); + } } } - } - else - { - if (mSinkPaused.load()) + else { - (void)PausePipeline(false, false); + if (mSinkPaused.load()) + { + (void)PausePipeline(false, false); + } + UpdateSubtitleTimestamp(); + SendBufferChangeEvent(false); } - UpdateSubtitleTimestamp(); - SendBufferChangeEvent(false); } } @@ -5516,7 +5519,6 @@ void PrivateInstanceAAMP::TeardownStream(bool newTune, bool disableDownloads) // Using StreamLock to make sure this is not interfering with GetFile() from PreCachePlaylistDownloadTask AcquireStreamLock(); AAMPLOG_INFO("TeardownStream: Stopping StreamAbstraction"); - mpStreamAbstractionAAMP->StopUnderflowMonitor(); mpStreamAbstractionAAMP->Stop(disableDownloads); if(mContentType == ContentType_HDMIIN) @@ -6300,15 +6302,6 @@ void PrivateInstanceAAMP::TuneHelper(TuneType tuneType, bool seekWhilePaused) mpStreamAbstractionAAMP->ReSetPipelineFlushStatus(); mpStreamAbstractionAAMP->Start(); - // Start underflow monitor after successful initialization and Start() - if (mpStreamAbstractionAAMP && ISCONFIGSET_PRIV(eAAMPConfig_EnableAampUnderflowMonitor)) - { - mpStreamAbstractionAAMP->StartUnderflowMonitor(); - if (!mpStreamAbstractionAAMP->IsUnderflowMonitorRunning()) - { - AAMPLOG_WARN("UnderflowMonitor did not start; continuing without AampUnderflowMonitor"); - } - } if (!mbUsingExternalPlayer) { if (mbPlayEnabled) @@ -8352,6 +8345,11 @@ void PrivateInstanceAAMP::Stop( bool sendStateChangeEvent ) mAutoResumeTaskPending = false; } DisableDownloads(); + if (mpStreamAbstractionAAMP) + { + mpStreamAbstractionAAMP->StopUnderflowMonitor(); + } + //Moved the tsb delete request from XRE to AAMP to avoid the HTTP-404 erros if(IsFogTSBSupported()) { diff --git a/priv_aamp.h b/priv_aamp.h index 438c21b26..5daac62bc 100644 --- a/priv_aamp.h +++ b/priv_aamp.h @@ -1538,10 +1538,10 @@ class PrivateInstanceAAMP : public DrmCallbacks, public std::enable_shared_from_ /** * @fn SendBufferChangeEvent * - * @param[in] bufferingStopped- Flag to indicate buffering stopped.Underflow = True + * @param[in] bufferingStart Flag indicating whether buffering has started; true when underflow begins, false when it ends * @return void */ - void SendBufferChangeEvent(bool bufferingStopped=false); + void SendBufferChangeEvent(bool bufferingStart=false); /** * @fn SendTuneMetricsEvent diff --git a/streamabstraction.cpp b/streamabstraction.cpp index 88ada2b3b..c57c831bb 100644 --- a/streamabstraction.cpp +++ b/streamabstraction.cpp @@ -2184,6 +2184,23 @@ StreamAbstractionAAMP::StreamAbstractionAAMP(PrivateInstanceAAMP* aamp, id3_call { mBitrateReason = (aamp->rate != AAMP_NORMAL_PLAY_RATE) ? eAAMP_BITRATE_CHANGE_BY_TRICKPLAY : eAAMP_BITRATE_CHANGE_BY_SEEK; } + + if(GETCONFIGVALUE(eAAMPConfig_EnableAampUnderflowMonitor)) + { + if(!mUnderflowMonitor) + { + try + { + mUnderflowMonitor = std::make_unique(this, aamp); + AAMPLOG_INFO("Initialized AampUnderflowMonitor"); + } + catch (const std::exception &e) + { + AAMPLOG_ERR("Failed to initialize AampUnderflowMonitor: %s", e.what()); + mUnderflowMonitor.reset(); + } + } + } } @@ -3008,63 +3025,39 @@ bool StreamAbstractionAAMP::UpdateProfileBasedOnFragmentCache() void StreamAbstractionAAMP::StartUnderflowMonitor() { - std::lock_guard lock(mUnderflowMonitorMutex); - // Run underflow monitor only when explicitly enabled via config - if (!GETCONFIGVALUE(eAAMPConfig_EnableAampUnderflowMonitor)) - { - AAMPLOG_TRACE("UnderflowMonitor gated off by config; skipping"); - return; - } if (!GetMediaTrack(eTRACK_VIDEO)) { - AAMPLOG_WARN("StartUnderflowMonitor: video track unavailable"); - return; - } - if (!mUnderflowMonitor) - { - try + if (mUnderflowMonitor) { - mUnderflowMonitor = std::make_unique(this, aamp); - mUnderflowMonitor->Start(); - AAMPLOG_INFO("Started AampUnderflowMonitor for video"); + // No video track: delete the monitor to avoid wasted resources + mUnderflowMonitor.reset(); + AAMPLOG_INFO("StartUnderflowMonitor: no video track; deleted AampUnderflowMonitor"); } - catch (const std::exception &e) + else { - AAMPLOG_ERR("Failed to create/start AampUnderflowMonitor: %s", e.what()); - // Ensure future calls can attempt creation again - mUnderflowMonitor.reset(); + AAMPLOG_WARN("StartUnderflowMonitor: video track unavailable"); } + return; } - else + if (mUnderflowMonitor) { - // Attempt to start existing monitor; Start() is idempotent - try - { - mUnderflowMonitor->Start(); - } - catch (const std::exception &e) - { - AAMPLOG_ERR("Failed to start existing AampUnderflowMonitor: %s", e.what()); - // Reset to allow recreation on next call - mUnderflowMonitor.reset(); - } + mUnderflowMonitor->Start(); + AAMPLOG_INFO("Started AampUnderflowMonitor for video"); } } void StreamAbstractionAAMP::StopUnderflowMonitor() { - std::lock_guard lock(mUnderflowMonitorMutex); if (mUnderflowMonitor) { mUnderflowMonitor->Stop(); mUnderflowMonitor.reset(); - AAMPLOG_INFO("Stopped AampUnderflowMonitor for video"); + AAMPLOG_INFO("Stopped AampUnderflowMonitor for video; resetting monitor instance"); } } bool StreamAbstractionAAMP::IsUnderflowMonitorRunning() const { - std::lock_guard lock(mUnderflowMonitorMutex); return (mUnderflowMonitor && mUnderflowMonitor->IsRunning()); } /** diff --git a/test/utests/drm/mocks/MockPrivateInstanceAAMP.h b/test/utests/drm/mocks/MockPrivateInstanceAAMP.h index 009264513..b94dbb850 100644 --- a/test/utests/drm/mocks/MockPrivateInstanceAAMP.h +++ b/test/utests/drm/mocks/MockPrivateInstanceAAMP.h @@ -30,6 +30,15 @@ class MockPrivateInstanceAAMP MOCK_METHOD(bool, isDecryptClearSamplesRequired, ()); MOCK_METHOD(void, SendDrmErrorEvent, (DrmMetaDataEventPtr event, bool isRetryEnabled)); MOCK_METHOD(void, SendDRMMetaData, (DrmMetaDataEventPtr e)); + + // Player state and timing + MOCK_METHOD(AAMPPlayerState, GetState, ()); + MOCK_METHOD(void, SetState, (AAMPPlayerState state, bool sendStateChangeEvent)); + + // Underflow monitor interactions + MOCK_METHOD(void, SetBufferingState, (bool buffering)); + MOCK_METHOD(bool, IsSinkCacheEmpty, (AampMediaType mediaType)); + MOCK_METHOD(bool, TrackDownloadsAreEnabled, (AampMediaType type)); }; extern MockPrivateInstanceAAMP *g_mockPrivateInstanceAAMP; diff --git a/test/utests/drm/mocks/aampMocks.cpp b/test/utests/drm/mocks/aampMocks.cpp index 661f011e5..3b2c5a52e 100644 --- a/test/utests/drm/mocks/aampMocks.cpp +++ b/test/utests/drm/mocks/aampMocks.cpp @@ -209,7 +209,11 @@ void DumpBlob(const unsigned char *ptr, size_t len) void PrivateInstanceAAMP::SetBufferingState(bool buffering) { - (void)buffering; + // Forward to Google Mock when available so tests can assert calls + if (g_mockPrivateInstanceAAMP != nullptr) + { + g_mockPrivateInstanceAAMP->SetBufferingState(buffering); + } } void PrivateInstanceAAMP::UpdateUseSinglePipeline(void) @@ -974,6 +978,11 @@ bool PrivateInstanceAAMP::IsDiscontinuityProcessPending() bool PrivateInstanceAAMP::IsSinkCacheEmpty(AampMediaType mediaType) { + // Forward to Google Mock when available + if (g_mockPrivateInstanceAAMP != nullptr) + { + return g_mockPrivateInstanceAAMP->IsSinkCacheEmpty(mediaType); + } return true; } @@ -1019,6 +1028,11 @@ void PrivateInstanceAAMP::StopBuffering(bool forceStop) bool PrivateInstanceAAMP::TrackDownloadsAreEnabled(AampMediaType type) { + // Forward to Google Mock when available + if (g_mockPrivateInstanceAAMP != nullptr) + { + return g_mockPrivateInstanceAAMP->TrackDownloadsAreEnabled(type); + } return true; } diff --git a/test/utests/fakes/FakeAampUnderflowMonitor.cpp b/test/utests/fakes/FakeAampUnderflowMonitor.cpp index 7fa265eaf..5da580a3f 100644 --- a/test/utests/fakes/FakeAampUnderflowMonitor.cpp +++ b/test/utests/fakes/FakeAampUnderflowMonitor.cpp @@ -1,31 +1,25 @@ /* - * If not stated otherwise in this file or this component's license file the - * following copyright and licenses apply: - * - * Copyright 2026 RDK Management - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @file FakeAampUnderflowMonitor.cpp - * @brief Fake implementation of AampUnderflowMonitor for unit testing. - */ +* If not stated otherwise in this file or this component's license file the +* following copyright and licenses apply: +* +* Copyright 2026 RDK Management +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ #include "AampUnderflowMonitor.h" -AampUnderflowMonitor::AampUnderflowMonitor(StreamAbstractionAAMP* stream, PrivateInstanceAAMP* aamp) - : mStream(stream), mAamp(aamp) +AampUnderflowMonitor::AampUnderflowMonitor(StreamAbstractionAAMP* stream, PrivateInstanceAAMP* aamp) : mStream(stream), mAamp(aamp) { } @@ -40,3 +34,7 @@ void AampUnderflowMonitor::Start() void AampUnderflowMonitor::Stop() { } + +void AampUnderflowMonitor::Run() +{ +} diff --git a/test/utests/fakes/FakePrivateInstanceAAMP.cpp b/test/utests/fakes/FakePrivateInstanceAAMP.cpp index aea6a0424..d22406bd7 100644 --- a/test/utests/fakes/FakePrivateInstanceAAMP.cpp +++ b/test/utests/fakes/FakePrivateInstanceAAMP.cpp @@ -177,6 +177,10 @@ int PrivateInstanceAAMP::HandleSSLProgressCallback ( void *clientp, double dltot void PrivateInstanceAAMP::SetBufferingState(bool buffering) { (void)buffering; + if (g_mockPrivateInstanceAAMP != nullptr) + { + g_mockPrivateInstanceAAMP->SetBufferingState(buffering); + } } void PrivateInstanceAAMP::UpdateUseSinglePipeline( void ) @@ -1111,7 +1115,11 @@ bool PrivateInstanceAAMP::IsDiscontinuityProcessPending() bool PrivateInstanceAAMP::IsSinkCacheEmpty(AampMediaType mediaType) { - return true; + if (g_mockPrivateInstanceAAMP != nullptr) + { + return g_mockPrivateInstanceAAMP->IsSinkCacheEmpty(mediaType); + } + return true; } void PrivateInstanceAAMP::NotifyBitRateChangeEvent( BitsPerSecond bitrate, BitrateChangeReason reason, int width, int height, double frameRate, double position, bool GetBWIndex, VideoScanType scantype, int aspectRatioWidth, int aspectRatioHeight) diff --git a/test/utests/mocks/MockPrivateInstanceAAMP.h b/test/utests/mocks/MockPrivateInstanceAAMP.h index 278412355..16e921ac2 100644 --- a/test/utests/mocks/MockPrivateInstanceAAMP.h +++ b/test/utests/mocks/MockPrivateInstanceAAMP.h @@ -95,6 +95,9 @@ class MockPrivateInstanceAAMP MOCK_METHOD(bool, IsLiveStream, ()); MOCK_METHOD(bool, TrackDownloadsAreEnabled, (AampMediaType type)); MOCK_METHOD(void, NotifyReservationComplete, (const std::string& reservationId)); + // Hooks needed by AampUnderflowMonitor tests + MOCK_METHOD(void, SetBufferingState, (bool buffering)); + MOCK_METHOD(bool, IsSinkCacheEmpty, (AampMediaType mediaType)); }; extern MockPrivateInstanceAAMP *g_mockPrivateInstanceAAMP; diff --git a/test/utests/tests/StreamAbstractionAAMP/AampUnderflowMonitorTests.cpp b/test/utests/tests/StreamAbstractionAAMP/AampUnderflowMonitorTests.cpp new file mode 100644 index 000000000..e44ad59b9 --- /dev/null +++ b/test/utests/tests/StreamAbstractionAAMP/AampUnderflowMonitorTests.cpp @@ -0,0 +1,271 @@ +/* +* If not stated otherwise in this file or this component's license file the +* following copyright and licenses apply: +* +* Copyright 2026 RDK Management +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#include +#include +#include "priv_aamp.h" +#include "AampConfig.h" +#include "AampUnderflowMonitor.h" +#include "StreamAbstractionAAMP.h" +#include "MockAampConfig.h" +#include "MockPrivateInstanceAAMP.h" +#include "MockMediaTrack.h" +#include +#include +#include + +using ::testing::Return; +using ::testing::NiceMock; +using ::testing::AnyNumber; +using ::testing::AtLeast; +using ::testing::InSequence; + +extern MockAampConfig *g_mockAampConfig; +extern MockPrivateInstanceAAMP *g_mockPrivateInstanceAAMP; + +// Local global config for tests in this TU +static AampConfig *gLocalConfig = nullptr; + +class AampUnderflowMonitorTests : public ::testing::Test +{ + protected: + class TestStreamAbstraction : public StreamAbstractionAAMP + { + public: + explicit TestStreamAbstraction(PrivateInstanceAAMP* aamp) + : StreamAbstractionAAMP(aamp) + , videoTrack(nullptr) + , audioTrack(nullptr) + { + } + ~TestStreamAbstraction() override = default; + std::unique_ptr videoTrack; + std::unique_ptr audioTrack; + // Minimal overrides not relevant to test focus + AAMPStatusType Init(TuneType) override { return eAAMPSTATUS_OK; } + void Start() override {} + void Stop(bool) override {} + void GetStreamFormat(StreamOutputFormat&, StreamOutputFormat&, StreamOutputFormat&) override {} + MediaTrack* GetMediaTrack(TrackType t) override + { + if (t == eTRACK_VIDEO) return videoTrack.get(); + if (t == eTRACK_AUDIO) return audioTrack.get(); + return nullptr; + } + }; + + PrivateInstanceAAMP* aamp = nullptr; + TestStreamAbstraction* stream = nullptr; + + void SetUp() override + { + if (!gLocalConfig) gLocalConfig = new AampConfig(); + // Hook the mock config so GETCONFIGVALUE/ISCONFIGSET macros route to mocks + g_mockAampConfig = new NiceMock(); + + // Create aamp using the real fake implementation used in utests + aamp = new PrivateInstanceAAMP(gLocalConfig); + // Provide a mock proxy used by various calls + if (!g_mockPrivateInstanceAAMP) g_mockPrivateInstanceAAMP = new NiceMock(); + EXPECT_CALL(*g_mockAampConfig, GetConfigValue(eAAMPConfig_EnableAampUnderflowMonitor)) + .Times(AnyNumber()).WillRepeatedly(Return(true)); + stream = new TestStreamAbstraction(aamp); + + // MediaTrack init expectations commonly present across tests + EXPECT_CALL(*g_mockAampConfig, GetConfigValue(eAAMPConfig_MaxFragmentCached)) + .Times(AnyNumber()).WillRepeatedly(Return(0)); + EXPECT_CALL(*g_mockAampConfig, GetConfigValue(eAAMPConfig_MaxFragmentChunkCached)) + .Times(AnyNumber()).WillRepeatedly(Return(0)); + + // Underflow monitor configuration expectations used by AampUnderflowMonitor::Run + EXPECT_CALL(*g_mockAampConfig, GetConfigValue(eAAMPConfig_UnderflowDetectThresholdSec)) + .Times(AnyNumber()).WillRepeatedly(Return(1.0)); + EXPECT_CALL(*g_mockAampConfig, GetConfigValue(eAAMPConfig_UnderflowResumeThresholdSec)) + .Times(AnyNumber()).WillRepeatedly(Return(2.0)); + EXPECT_CALL(*g_mockAampConfig, GetConfigValue(eAAMPConfig_UnderflowLowBufferSec)) + .Times(AnyNumber()).WillRepeatedly(Return(1.0)); + EXPECT_CALL(*g_mockAampConfig, GetConfigValue(eAAMPConfig_UnderflowHighBufferSec)) + .Times(AnyNumber()).WillRepeatedly(Return(10.0)); + EXPECT_CALL(*g_mockAampConfig, GetConfigValue(eAAMPConfig_UnderflowLowBufferPollMs)) + .Times(AnyNumber()).WillRepeatedly(Return(50)); + EXPECT_CALL(*g_mockAampConfig, GetConfigValue(eAAMPConfig_UnderflowMediumBufferPollMs)) + .Times(AnyNumber()).WillRepeatedly(Return(100)); + EXPECT_CALL(*g_mockAampConfig, GetConfigValue(eAAMPConfig_UnderflowHighBufferPollMs)) + .Times(AnyNumber()).WillRepeatedly(Return(150)); + } + + void TearDown() override + { + delete stream; stream = nullptr; + delete aamp; aamp = nullptr; + delete g_mockPrivateInstanceAAMP; g_mockPrivateInstanceAAMP = nullptr; + delete g_mockAampConfig; g_mockAampConfig = nullptr; + delete gLocalConfig; gLocalConfig = nullptr; + } +}; + +// 1) No video track -> early exit +TEST_F(AampUnderflowMonitorTests, NoVideoTrackSkipsStart) +{ + // No videoTrack set + stream->videoTrack.reset(); + + stream->StartUnderflowMonitor(); + EXPECT_FALSE(stream->IsUnderflowMonitorRunning()); +} + +// 2) Happy path: video track present, monitor starts +TEST_F(AampUnderflowMonitorTests, StartsWhenEnabledAndVideoPresent) +{ + // Provide video track + stream->videoTrack = std::make_unique>(eTRACK_VIDEO, aamp, "video"); + + // Drive thread state; allow any number of polls + EXPECT_CALL(*g_mockPrivateInstanceAAMP, GetState()) + .Times(AnyNumber()) + .WillRepeatedly(Return(eSTATE_PLAYING)); + + stream->StartUnderflowMonitor(); + EXPECT_TRUE(stream->IsUnderflowMonitorRunning()); + + // Cleanup + stream->StopUnderflowMonitor(); + EXPECT_FALSE(stream->IsUnderflowMonitorRunning()); +} + +// 3) Idempotent: starting again while already started should keep running without crash +TEST_F(AampUnderflowMonitorTests, StartTwiceKeepsRunning) +{ + stream->videoTrack = std::make_unique>(eTRACK_VIDEO, aamp, "video"); + + EXPECT_CALL(*g_mockPrivateInstanceAAMP, GetState()) + .Times(AnyNumber()) + .WillRepeatedly(Return(eSTATE_PLAYING)); + + stream->StartUnderflowMonitor(); + EXPECT_TRUE(stream->IsUnderflowMonitorRunning()); + // Thread likely exited; ensure no crash on double-start + + // Call StartUnderflowMonitor again; implementation is idempotent and should not throw + EXPECT_NO_THROW(stream->StartUnderflowMonitor()); + EXPECT_TRUE(stream->IsUnderflowMonitorRunning()); + + stream->StopUnderflowMonitor(); + EXPECT_FALSE(stream->IsUnderflowMonitorRunning()); +} + +// 4) Run() enters detection path when buffer <= threshold and downloads enabled +TEST_F(AampUnderflowMonitorTests, Run_DetectsUnderflowByBufferThreshold) +{ + stream->videoTrack = std::make_unique>(eTRACK_VIDEO, aamp, "video"); + + // Keep PLAYING; allow any number of polls + EXPECT_CALL(*g_mockPrivateInstanceAAMP, GetState()) + .Times(AnyNumber()) + .WillRepeatedly(Return(eSTATE_PLAYING)); + // Buffered duration at/below threshold to trigger detection + EXPECT_CALL(*stream->videoTrack, GetBufferedDuration()) + .Times(AnyNumber()) + .WillRepeatedly(Return(0.0)); + + // Allow buffer check: normal rate, not seeking + aamp->rate = AAMP_NORMAL_PLAY_RATE; + + // Downloads enabled and sink cache not empty + EXPECT_CALL(*g_mockPrivateInstanceAAMP, TrackDownloadsAreEnabled(eMEDIATYPE_VIDEO)) + .WillRepeatedly(Return(true)); + EXPECT_CALL(*g_mockPrivateInstanceAAMP, IsSinkCacheEmpty(eMEDIATYPE_VIDEO)) + .WillRepeatedly(Return(false)); + + // Expect buffering to start when underflow detected + std::mutex mtx; + std::condition_variable cv; + bool signaled = false; + EXPECT_CALL(*g_mockPrivateInstanceAAMP, SetBufferingState(true)) + .Times(AtLeast(1)) + .WillOnce(::testing::Invoke([&](bool){ + { + std::lock_guard lock(mtx); + signaled = true; + } + cv.notify_one(); + })); + + stream->StartUnderflowMonitor(); + // Wait until buffering start is observed or timeout + { + std::unique_lock lock(mtx); + cv.wait_for(lock, std::chrono::milliseconds(1000), [&]{ return signaled; }); + } + stream->StopUnderflowMonitor(); + EXPECT_TRUE(signaled); +} + +// 5) Run() suppresses buffer checks during trickplay and ends underflow when resume threshold met and cache not empty +TEST_F(AampUnderflowMonitorTests, Run_TrickplaySuppressesBufferCheckAndEndsUnderflow) +{ + stream->videoTrack = std::make_unique>(eTRACK_VIDEO, aamp, "video"); + + // Underflow active and pipeline paused + aamp->SetBufUnderFlowStatus(true); + aamp->mSinkPaused.store(true); + + // Make allowBufferCheck false via trickplay rate + aamp->rate = 2.0f; + + // Ensure detection condition is false: cache not empty + EXPECT_CALL(*g_mockPrivateInstanceAAMP, IsSinkCacheEmpty(eMEDIATYPE_VIDEO)) + .Times(AtLeast(1)) + .WillRepeatedly(Return(false)); + // Downloads enabled state doesn't matter here + EXPECT_CALL(*g_mockPrivateInstanceAAMP, TrackDownloadsAreEnabled(eMEDIATYPE_VIDEO)) + .Times(AnyNumber()).WillRepeatedly(Return(true)); + + // Configure resume threshold at 0 so buffered(=0) meets it + EXPECT_CALL(*g_mockAampConfig, GetConfigValue(eAAMPConfig_UnderflowResumeThresholdSec)) + .Times(AnyNumber()).WillRepeatedly(Return(0.0)); + + EXPECT_CALL(*g_mockPrivateInstanceAAMP, GetState()) + .Times(AnyNumber()) + .WillRepeatedly(Return(eSTATE_PLAYING)); + // Buffered duration via GetBufferedVideoDurationSec() uses fake impl returning 0.0 + + // Expect buffering to end + std::mutex mtx; + std::condition_variable cv; + bool signaled = false; + EXPECT_CALL(*g_mockPrivateInstanceAAMP, SetBufferingState(false)) + .Times(AtLeast(1)) + .WillOnce(::testing::Invoke([&](bool){ + { + std::lock_guard lock(mtx); + signaled = true; + } + cv.notify_one(); + })); + + stream->StartUnderflowMonitor(); + // Wait until buffering end is observed or timeout + { + std::unique_lock lock(mtx); + cv.wait_for(lock, std::chrono::milliseconds(1000), [&]{ return signaled; }); + } + stream->StopUnderflowMonitor(); + EXPECT_TRUE(signaled); +} \ No newline at end of file diff --git a/test/utests/tests/StreamAbstractionAAMP/CMakeLists.txt b/test/utests/tests/StreamAbstractionAAMP/CMakeLists.txt index f3370e6db..24e95d98c 100644 --- a/test/utests/tests/StreamAbstractionAAMP/CMakeLists.txt +++ b/test/utests/tests/StreamAbstractionAAMP/CMakeLists.txt @@ -35,10 +35,12 @@ set (DASH_PARSER_SOURCES ${AAMP_ROOT}/dash/xml/DomDocument.cpp ${AAMP_ROOT}/dash/utils/Path.cpp) set(TEST_SOURCES FunctionalTests.cpp - StreamAbstractionAAMP.cpp) - -set(AAMP_SOURCES ${AAMP_ROOT}/streamabstraction.cpp ${AAMP_ROOT}/CachedFragment.cpp ) + StreamAbstractionAAMP.cpp + AampUnderflowMonitorTests.cpp) +set(AAMP_SOURCES ${AAMP_ROOT}/streamabstraction.cpp + ${AAMP_ROOT}/CachedFragment.cpp + ${AAMP_ROOT}/AampUnderflowMonitor.cpp) add_executable(${EXEC_NAME} ${TEST_SOURCES} ${AAMP_SOURCES})