diff --git a/CHANGELOG.md b/CHANGELOG.md index 0901cad9..40b650d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,17 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [0.2.0](https://github.com/rdkcentral/middleware-player-interface/compare/0.1.4...0.2.0) + +- RDKEMW-19686 [Develop]Bringing RKDEMW-19683 , RDKEMW-19684, RDKEMW-19685 to middleware-player-interface [`#170`](https://github.com/rdkcentral/middleware-player-interface/pull/170) +- Revert "VPAAMP-457 middleware changes" [`da9157e`](https://github.com/rdkcentral/middleware-player-interface/commit/da9157e565ce0ff78a87c893a4246f08476b61ed) +- Merge tag '0.1.4' into develop [`7c07f2a`](https://github.com/rdkcentral/middleware-player-interface/commit/7c07f2a7f9e30f5699c86baa3d8f2709612f4dbc) +- dev_sprint_pli sync [`5537418`](https://github.com/rdkcentral/middleware-player-interface/commit/5537418a13f5ff3fecad2d89d79a9fdea76878d9) + #### [0.1.4](https://github.com/rdkcentral/middleware-player-interface/compare/0.1.3...0.1.4) +> 11 June 2026 + - RDKEMW-19919 : No WebVTT subtitles shown on some channels when PTS restamping enabled [`#174`](https://github.com/rdkcentral/middleware-player-interface/pull/174) - Revert "Revert "RDKEMW-18010 Middleware-player-interface feature development and inalign with dev_sprint"" [`#175`](https://github.com/rdkcentral/middleware-player-interface/pull/175) - Revert "RDKEMW-18010 Middleware-player-interface feature development and inalign with dev_sprint" [`#172`](https://github.com/rdkcentral/middleware-player-interface/pull/172) @@ -24,9 +33,9 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - Develop [`#98`](https://github.com/rdkcentral/middleware-player-interface/pull/98) - RDKEMW-14946 : Fix coverity workflow scan in middleware-player-interface repo [`#91`](https://github.com/rdkcentral/middleware-player-interface/pull/91) - main sync [`#86`](https://github.com/rdkcentral/middleware-player-interface/pull/86) -- Delete sync directory [`caff23b`](https://github.com/rdkcentral/middleware-player-interface/commit/caff23b7f84d96c5314d24e033030c0e4f81aa8d) -- Update GstHandlerControlTests.cpp [`2aa5167`](https://github.com/rdkcentral/middleware-player-interface/commit/2aa51679a712158a23e65e5d58d1cd5cd4fc1411) -- Update GstHandlerControlTests.cpp [`33a8050`](https://github.com/rdkcentral/middleware-player-interface/commit/33a80506fdbe144b75a084244fa2f7927bef8cc4) +- changelog update [`0e0120d`](https://github.com/rdkcentral/middleware-player-interface/commit/0e0120d2a7c68ec0b031ad34dc2536779ce67b83) +- Integerate LLM teamgiven test cases into dev_pli branch [`8695df1`](https://github.com/rdkcentral/middleware-player-interface/commit/8695df1648e8e7fb5d2f9be24be0fac5f77df6d8) +- init commit [`41b08c8`](https://github.com/rdkcentral/middleware-player-interface/commit/41b08c887a9feda03057a129ade8fa334568eb12) #### [0.1.3](https://github.com/rdkcentral/middleware-player-interface/compare/0.1.3-r2...0.1.3) diff --git a/InterfacePlayerPriv.h b/InterfacePlayerPriv.h index 296c5431..97798d78 100755 --- a/InterfacePlayerPriv.h +++ b/InterfacePlayerPriv.h @@ -206,6 +206,7 @@ struct GstPlayerPriv gboolean buffering_in_progress; /**< buffering is in progress */ guint buffering_timeout_cnt; /**< make sure buffering_timeout doesn't get stuck */ GstState buffering_target_state; /**< the target state after buffering */ + bool seekPausedState; /** < true when seek with keepPaused is active — guards buffering_timeout from setting PLAYING */ gint64 lastKnownPTS; /**< To store the PTS of last displayed video */ long long ptsUpdatedTimeMS; /**< Timestamp when PTS was last updated */ guint ptsCheckForEosOnUnderflowIdleTaskId; /**< ID of task to ensure video PTS is not moving before notifying EOS on underflow. */ diff --git a/InterfacePlayerRDK.cpp b/InterfacePlayerRDK.cpp index f648fa50..0b57fa7c 100644 --- a/InterfacePlayerRDK.cpp +++ b/InterfacePlayerRDK.cpp @@ -148,7 +148,7 @@ firstFrameCallbackIdleTaskId(GST_TASK_ID_INVALID), firstFrameCallbackIdleTaskPen using_westerossink(false), usingRialtoSink(false), usingClosedCaptionsControl(false), pauseOnStartPlayback(false), eosSignalled(false), buffering_enabled(FALSE), buffering_in_progress(FALSE), buffering_timeout_cnt(0), buffering_target_state(GST_STATE_NULL), -lastKnownPTS(0), ptsUpdatedTimeMS(0), ptsCheckForEosOnUnderflowIdleTaskId(GST_TASK_ID_INVALID), +seekPausedState(false),lastKnownPTS(0), ptsUpdatedTimeMS(0), ptsCheckForEosOnUnderflowIdleTaskId(GST_TASK_ID_INVALID), numberOfVideoBuffersSent(0), segmentStart(0), positionQuery(NULL), paused(false), pipelineState(GST_STATE_NULL), firstVideoFrameDisplayedCallbackTask("FirstVideoFrameDisplayedCallback"), @@ -470,6 +470,15 @@ void InterfacePlayerRDK::ConfigurePipeline(int format, int audioFormat, int subF gboolean videoOnly = (audioFormat == GST_FORMAT_INVALID); MW_LOG_INFO("Setting single-path-stream to %d", videoOnly); g_object_set(vidsink, "single-path-stream", videoOnly, NULL); + // RDKEMW-18286: Reinforce show-video-window before pipeline state change + if (interfacePlayerPriv->gstPrivateContext->videoMuted) + { + MW_LOG_MIL("InterfacePlayerRDK - ConfigurePipeline: reinforcing " + "show-video-window=FALSE on %s (videoMuted=%d)", + GST_ELEMENT_NAME(vidsink), + interfacePlayerPriv->gstPrivateContext->videoMuted); + g_object_set(vidsink, "show-video-window", FALSE, NULL); + } } else { @@ -512,6 +521,12 @@ void InterfacePlayerRDK::ConfigurePipeline(int format, int audioFormat, int subF interfacePlayerPriv->gstPrivateContext->buffering_in_progress = true; interfacePlayerPriv->gstPrivateContext->buffering_timeout_cnt = DEFAULT_BUFFERING_MAX_CNT; + // buffering_timeout will handle the PLAYING transition, so seekPausedState must not block it. + if (interfacePlayerPriv->gstPrivateContext->seekPausedState) + { + MW_LOG_WARN("ConfigurePipeline: clearing seekPausedState — buffering will drive PLAYING transition"); + interfacePlayerPriv->gstPrivateContext->seekPausedState = false; + } if (SetStateWithWarnings(interfacePlayerPriv->gstPrivateContext->pipeline, GST_STATE_PAUSED) == GST_STATE_CHANGE_FAILURE) { MW_LOG_ERR("InterfacePlayerRDK_Configure GST_STATE_PAUSED failed"); @@ -522,12 +537,31 @@ void InterfacePlayerRDK::ConfigurePipeline(int format, int audioFormat, int subF else { MW_LOG_INFO("Setting state to GST_STATE_PLAYING"); - if (SetStateWithWarnings(interfacePlayerPriv->gstPrivateContext->pipeline, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE) + /* If a seek-with-keepPaused is active we must not race into PLAYING. + * Defer the PLAYING transition and leave pipeline in PAUSED until + * an explicit resume (Pause(false)) clears `seekPausedState`. + */ + if (interfacePlayerPriv->gstPrivateContext->seekPausedState) { - MW_LOG_ERR("InterfacePlayerRDK: GST_STATE_PLAYING failed"); + MW_LOG_WARN("seekPausedState active - deferring transition to PLAYING, marking pendingPlayState"); + interfacePlayerPriv->gstPrivateContext->buffering_target_state = GST_STATE_PLAYING; + interfacePlayerPriv->gstPrivateContext->pendingPlayState = true; + /* Ensure pipeline is left/returned to PAUSED to avoid accidental play */ + if (SetStateWithWarnings(interfacePlayerPriv->gstPrivateContext->pipeline, GST_STATE_PAUSED) == GST_STATE_CHANGE_FAILURE) + { + MW_LOG_ERR("InterfacePlayerRDK: GST_STATE_PAUSED failed while deferring PLAYING"); + } + interfacePlayerPriv->gstPrivateContext->paused = true; + } + else + { + if (SetStateWithWarnings(interfacePlayerPriv->gstPrivateContext->pipeline, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE) + { + MW_LOG_ERR("InterfacePlayerRDK: GST_STATE_PLAYING failed"); + } + interfacePlayerPriv->gstPrivateContext->pendingPlayState = false; + interfacePlayerPriv->gstPrivateContext->paused = false; } - interfacePlayerPriv->gstPrivateContext->pendingPlayState = false; - interfacePlayerPriv->gstPrivateContext->paused = false; } interfacePlayerPriv->gstPrivateContext->eosSignalled = false; interfacePlayerPriv->gstPrivateContext->numberOfVideoBuffersSent = 0; @@ -1597,14 +1631,32 @@ bool InterfacePlayerRDK::Flush(double position, int rate, bool shouldTearDown, b interfacePlayerPriv->gstPrivateContext->ptsCheckForEosOnUnderflowIdleTaskId = PLAYER_TASK_ID_INVALID; } + + /* If pipeline is paused (seek with keepPaused), mark seekPausedState + * so that when ConfigurePipeline restarts buffering, the buffering_timeout callback + * won't race to set PLAYING before Pause(1) arrives */ + if (interfacePlayerPriv->gstPrivateContext->paused) + { + interfacePlayerPriv->gstPrivateContext->seekPausedState = true; + MW_LOG_MIL("InterfacePlayerRDK: Flush with paused state — setting seekPausedState"); + } if (interfacePlayerPriv->gstPrivateContext->bufferingTimeoutTimerId) { MW_LOG_MIL("InterfacePlayerRDK: Remove bufferingTimeoutTimerId %d", interfacePlayerPriv->gstPrivateContext->bufferingTimeoutTimerId); g_source_remove(interfacePlayerPriv->gstPrivateContext->bufferingTimeoutTimerId); interfacePlayerPriv->gstPrivateContext->bufferingTimeoutTimerId = PLAYER_TASK_ID_INVALID; + // Reset buffering state to prevent stale timeout_cnt from triggering error after seek + interfacePlayerPriv->gstPrivateContext->buffering_in_progress = false; + interfacePlayerPriv->gstPrivateContext->buffering_timeout_cnt = DEFAULT_BUFFERING_MAX_CNT; - } + } + // If rate indicates playback (not paused seek), clear seekPausedState + if (rate > 0 && !interfacePlayerPriv->gstPrivateContext->paused) + { + interfacePlayerPriv->gstPrivateContext->seekPausedState = false; + MW_LOG_MIL("InterfacePlayerRDK: rate indicates playback, clearing seekPausedState"); + } // If the pipeline is not setup, we will cache the value for later SetSeekPosition(position); @@ -1680,8 +1732,21 @@ bool InterfacePlayerRDK::Flush(double position, int rate, bool shouldTearDown, b { if ((interfacePlayerPriv->socInterface->IsSimulatorSink() || interfacePlayerPriv->gstPrivateContext->usingRialtoSink) && rate != GST_NORMAL_PLAY_RATE) { - MW_LOG_INFO("Resetting seek position to zero"); - position = 0; + const bool isTrickplay = (rate != GST_NORMAL_PLAY_RATE); + const bool isLiveMedia = (static_cast(m_gstConfigParam->media) == eGST_MEDIAFORMAT_OTA); + + if (isTrickplay) + { + if (isLiveMedia) + { + MW_LOG_WARN("Live trickplay active - preserving flush seek position %f", position); + } + else + { + MW_LOG_INFO("Resetting seek position to zero"); + position = 0; + } + } } } if (!gst_element_seek(interfacePlayerPriv->gstPrivateContext->pipeline, playRate, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH, GST_SEEK_TYPE_SET, @@ -2343,6 +2408,21 @@ int InterfacePlayerRDK::SetupStream(int streamId, void *playerInstance, std::st g_object_set(vidsink, "has-drm", FALSE, NULL); } interfacePlayerPriv->gstPrivateContext->video_sink = vidsink; + + // RDKEMW-18286: Set show-video-window=FALSE at sink creation time. + // This is the EARLIEST possible point. The Rialto delegate will queue + // this (m_videoMuteQueued=true) and apply it server-side when the + // source attaches — which happens BEFORE the pipeline goes to PLAYING + // and BEFORE any frame can be decoded. + if (interfacePlayerPriv->gstPrivateContext->videoMuted) + { + MW_LOG_MIL("InterfacePlayerRDK - %s: setting show-video-window=FALSE " + "at creation time (videoMuted=%d)", + GST_ELEMENT_NAME(vidsink), + interfacePlayerPriv->gstPrivateContext->videoMuted); + g_object_set(vidsink, "show-video-window", FALSE, NULL); + } + } else { @@ -3385,6 +3465,12 @@ bool InterfacePlayerRDK::Pause(bool pause , bool forceStopGstreamerPreBuffering) GstState nextState = pause ? GST_STATE_PAUSED : GST_STATE_PLAYING; interfacePlayerPriv->gstPrivateContext->buffering_target_state = nextState; + /* Clear seekPausedState when explicitly resuming playback */ + if (!pause) + { + interfacePlayerPriv->gstPrivateContext->seekPausedState = false; + } + if (GST_STATE_PAUSED == nextState && forceStopGstreamerPreBuffering) { /* maybe in a timing case during the playback start, @@ -3404,7 +3490,30 @@ bool InterfacePlayerRDK::Pause(bool pause , bool forceStopGstreamerPreBuffering) /* wait a bit longer for the state change to conclude */ if (nextState != validateStateWithMsTimeout(this,nextState, 100)) { - MW_LOG_ERR("InterfacePlayerRDK_Pause - validateStateWithMsTimeout - FAILED GstState %d", nextState); + GstState current, pending; + MW_LOG_INFO("InterfacePlayerRDK_Pause - validateStateWithMsTimeout - FAILED expected %s", gst_element_state_get_name(nextState)); + + /* Recovery: retry the state change once before reporting failure */ + MW_LOG_INFO("InterfacePlayerRDK_Pause - retrying state change to GstState %d", nextState); + + // Wait for any in-flight transition to settle + gst_element_get_state(interfacePlayerPriv->gstPrivateContext->pipeline, ¤t, &pending, 0); + + // Single retry — no destructive NULL reset + GstStateChangeReturn rcRetry = SetStateWithWarnings(interfacePlayerPriv->gstPrivateContext->pipeline, nextState); + if (GST_STATE_CHANGE_ASYNC == rcRetry) + { + if (nextState != validateStateWithMsTimeout(this, nextState, 100)) + { + MW_LOG_ERR("Retry also failed — reporting error"); + retValue = false; + } + } + else if (GST_STATE_CHANGE_SUCCESS != rcRetry) + { + MW_LOG_ERR("Retry failed immediately with rc %d — reporting error", rcRetry); + retValue = false; + } } } else if (GST_STATE_CHANGE_SUCCESS != rc) @@ -4500,7 +4609,17 @@ static gboolean bus_message(GstBus * bus, GstMessage * msg, InterfacePlayerRDK * if(eGST_MEDIAFORMAT_DASH != static_cast(pInterfacePlayerRDK->m_gstConfigParam->media)) { SetStateWithWarnings(privatePlayer->gstPrivateContext->pipeline, GST_STATE_PAUSED); - SetStateWithWarnings(privatePlayer->gstPrivateContext->pipeline, GST_STATE_PLAYING); + /* Avoid forcing PLAYING if a seek-with-keepPaused is active */ + if (!privatePlayer->gstPrivateContext->seekPausedState) + { + SetStateWithWarnings(privatePlayer->gstPrivateContext->pipeline, GST_STATE_PLAYING); + } + else + { + MW_LOG_WARN("GST_MESSAGE_CLOCK_LOST: seekPausedState active - skipping PLAYING"); + privatePlayer->gstPrivateContext->pendingPlayState = true; + privatePlayer->gstPrivateContext->buffering_target_state = GST_STATE_PLAYING; + } } break; @@ -4566,8 +4685,34 @@ bool InterfacePlayerRDK::SetPlayBackRate(double rate) sources.push_back(interfacePlayerPriv->gstPrivateContext->stream[iTrack].source); } } - ret = interfacePlayerPriv->socInterface->SetPlaybackRate(sources, interfacePlayerPriv->gstPrivateContext->pipeline, rate, interfacePlayerPriv->gstPrivateContext->video_dec,interfacePlayerPriv->gstPrivateContext->audio_dec); - return ret; + ret = interfacePlayerPriv->socInterface->SetPlaybackRate(sources, interfacePlayerPriv->gstPrivateContext->pipeline, rate, interfacePlayerPriv->gstPrivateContext->video_dec,interfacePlayerPriv->gstPrivateContext->audio_dec); + + /* If application requested resume via rate change but middleware's + * seek-paused protection left the pipeline in PAUSED, ensure we clear + * `seekPausedState` here at middleware level. This handles cases where + * higher-level callers may retry or skip setting rate — forcing an + * explicit resume in the middleware prevents the pipeline from being + * stuck in PAUSED. */ + if (rate != 0.0 && interfacePlayerPriv->gstPrivateContext->seekPausedState && interfacePlayerPriv->gstPrivateContext->paused) + { + MW_LOG_WARN("InterfacePlayerRDK: SetPlayBackRate detected resume while seekPausedState active — forcing resume"); + /* Pause(false) clears seekPausedState in Pause implementation. */ + bool pauseResult = Pause(false, false); + if (pauseResult) + { + interfacePlayerPriv->gstPrivateContext->seekPausedState = false; + interfacePlayerPriv->gstPrivateContext->pendingPlayState = false; + /* After explicit resume we consider operation successful */ + ret = true; + } + else + { + MW_LOG_ERR("SetPlayBackRate: Pause(false) failed — cannot resume"); + ret = false; + } + } + + return ret; } /** @@ -4686,6 +4831,20 @@ static gboolean buffering_timeout (gpointer data) } else if (frames == -1 || frames >= pInterfacePlayerRDK->m_gstConfigParam->framesToQueue || (privatePlayer->gstPrivateContext->buffering_timeout_cnt > 0 && --privatePlayer->gstPrivateContext->buffering_timeout_cnt == 0)) { + /* Do not set PLAYING if a seek-with-keepPaused is in progress. + * The buffering_timeout timer may fire after ConfigurePipeline restarts buffering + * but BEFORE the Pause(1) from keepPaused logic arrives — causing a race. */ + if (privatePlayer->gstPrivateContext->seekPausedState) + { + MW_LOG_WARN("buffering_timeout: skipping PLAYING — seekPausedState active (cnt %u, frames %d)", privatePlayer->gstPrivateContext->buffering_timeout_cnt, frames); + if (privatePlayer->gstPrivateContext->buffering_timeout_cnt == 0) + { + MW_LOG_ERR("buffering_timeout: seekPausedState still active after timeout exhausted — clearing to unblock"); + privatePlayer->gstPrivateContext->seekPausedState = false; + } + return privatePlayer->gstPrivateContext->buffering_in_progress; + } + uint32_t original_buffering_timeout_cnt = privatePlayer->gstPrivateContext->buffering_timeout_cnt; MW_LOG_MIL("Set pipeline state to %s - buffering_timeout_cnt %u frames %i", gst_element_state_get_name(privatePlayer->gstPrivateContext->buffering_target_state), original_buffering_timeout_cnt, frames); @@ -5097,6 +5256,16 @@ void InterfacePlayerRDK::NotifyFragmentCachingComplete() { if(interfacePlayerPriv->gstPrivateContext->pendingPlayState) { + /* If a seek-with-keepPaused is active, do not transition to PLAYING here. + * Leave pendingPlayState set so the explicit resume will perform the transition. + */ + if (interfacePlayerPriv->gstPrivateContext->seekPausedState) + { + MW_LOG_WARN("NotifyFragmentCachingComplete: seekPausedState active - deferring PLAYING"); + interfacePlayerPriv->gstPrivateContext->buffering_target_state = GST_STATE_PLAYING; + return; + } + MW_LOG_MIL("InterfacePlayer: Setting pipeline to PLAYING state "); interfacePlayerPriv->gstPrivateContext->buffering_target_state = GST_STATE_PLAYING; if (SetStateWithWarnings(interfacePlayerPriv->gstPrivateContext->pipeline, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE) @@ -5266,6 +5435,7 @@ void InterfacePlayerRDK::InitializePlayerGstreamerPlugins() if (!gst_init_check(nullptr, nullptr, nullptr)) { MW_LOG_ERR("gst_init_check() failed"); } + SocUtils::Init(); #define PLUGINS_TO_LOWER_RANK_MAX 2 static const char *plugins_to_lower_rank[PLUGINS_TO_LOWER_RANK_MAX] = { diff --git a/SocUtils.cpp b/SocUtils.cpp index e6967a80..5449cb86 100644 --- a/SocUtils.cpp +++ b/SocUtils.cpp @@ -27,7 +27,16 @@ namespace SocUtils { - static std::shared_ptr socInterface = SocInterface::CreateSocInterface(); + static std::shared_ptr GetSocInterface() + { + static std::shared_ptr socInterface = SocInterface::CreateSocInterface(); + return socInterface; + } + + void Init() + { + (void)GetSocInterface(); + } /** * @brief Checks if AppSrc should be used for progressive playback. * @@ -38,7 +47,7 @@ namespace SocUtils */ bool UseAppSrcForProgressivePlayback( void ) { - return socInterface->UseAppSrc(); + return GetSocInterface()->UseAppSrc(); } /** @@ -51,7 +60,7 @@ namespace SocUtils */ bool UseWesterosSink( void ) { - return socInterface->UseWesterosSink(); + return GetSocInterface()->UseWesterosSink(); } /** @@ -63,7 +72,7 @@ namespace SocUtils */ bool IsAudioFragmentSyncSupported( void ) { - return socInterface->IsAudioFragmentSyncSupported(); + return GetSocInterface()->IsAudioFragmentSyncSupported(); } /** @@ -76,7 +85,7 @@ namespace SocUtils */ bool EnableLiveLatencyCorrection( void ) { - return socInterface->EnableLiveLatencyCorrection(); + return GetSocInterface()->EnableLiveLatencyCorrection(); } /** @@ -89,7 +98,7 @@ namespace SocUtils */ int RequiredQueuedFrames( void ) { - return socInterface->RequiredQueuedFrames(); + return GetSocInterface()->RequiredQueuedFrames(); } /** @@ -102,7 +111,7 @@ namespace SocUtils */ bool EnablePTSRestamp(void) { - return socInterface->EnablePTSRestamp(); + return GetSocInterface()->EnablePTSRestamp(); } /** * @brief Resets segment event flags during trickplay transitions. @@ -111,7 +120,7 @@ namespace SocUtils */ bool ResetNewSegmentEvent() { - return socInterface->ResetNewSegmentEvent(); + return GetSocInterface()->ResetNewSegmentEvent(); } /** * @brief Check if GST Subtec is enabled diff --git a/SocUtils.h b/SocUtils.h index 892eb3d4..d1aed318 100644 --- a/SocUtils.h +++ b/SocUtils.h @@ -26,6 +26,13 @@ namespace SocUtils { + /** + * @brief Initializes access to SOC-specific runtime capabilities. + * + * This function can be used to perform eager initialization during startup. + * SocUtils accessors also perform lazy initialization when needed. + */ + void Init(); /** * @brief Checks if AppSrc should be used for progressive playback. * diff --git a/subtec/subtecparser/WebVttSubtecParser.cpp b/subtec/subtecparser/WebVttSubtecParser.cpp index 92e9d71d..b284e470 100644 --- a/subtec/subtecparser/WebVttSubtecParser.cpp +++ b/subtec/subtecparser/WebVttSubtecParser.cpp @@ -19,7 +19,6 @@ #include "WebVttSubtecParser.hpp" #include "TextStyleAttributes.h" -#include WebVTTSubtecParser::WebVTTSubtecParser(SubtitleMimeType type, int width, int height) : SubtitleParser(type, width, height), m_channel(nullptr) { @@ -71,20 +70,11 @@ bool WebVTTSubtecParser::processData(const char* buffer, size_t bufferLen, doubl std::string str(const_cast(buffer), bufferLen); std::vector data(str.begin(), str.end()); - m_channel->SendDataPacket(std::move(data), time_offset_ms_); + m_channel->SendDataPacket(std::move(data), 0); return true; } -void WebVTTSubtecParser::setPtsOffset(double ptsOffsetSec) -{ - // Subtec's display_time = media_PTS - time_offset_ms (subtraction - // convention shared with Rialto SetSubtitlePtsOffset). The HLS - // restamped video PTS is media_PTS + ptsOffsetSec, so we negate - // here to make subtec add the offset on the subtitle path. - time_offset_ms_ = -static_cast(std::llround(ptsOffsetSec * 1000.0)); -} - void WebVTTSubtecParser::mute(bool mute) { if (mute) diff --git a/subtec/subtecparser/WebVttSubtecParser.hpp b/subtec/subtecparser/WebVttSubtecParser.hpp index e9cbf040..5e4186dd 100644 --- a/subtec/subtecparser/WebVttSubtecParser.hpp +++ b/subtec/subtecparser/WebVttSubtecParser.hpp @@ -40,10 +40,9 @@ class WebVTTSubtecParser : public SubtitleParser void pause(bool pause) override; void mute(bool mute) override; void setTextStyle(const std::string &options) override; - void setPtsOffset(double ptsOffsetSec) override; protected: std::unique_ptr m_channel; private: - std::int64_t time_offset_ms_ = 0; + std::uint64_t time_offset_ms_ = 0; std::uint64_t start_ms_ = 0; }; diff --git a/subtitle/subtitleParser.h b/subtitle/subtitleParser.h index ca156bf1..fb1d88af 100644 --- a/subtitle/subtitleParser.h +++ b/subtitle/subtitleParser.h @@ -93,15 +93,6 @@ class SubtitleParser virtual void mute(bool mute) {} virtual void isLinear(bool isLinear) {} virtual void setTextStyle(const std::string &options){} - /** - * @brief Set a per-fragment PTS offset (seconds) that the parser - * applies when forwarding cue data to its subtitle sink. - * - * Used by the HLS PTS-restamp path to align subtitle display time - * with the restamped video PTS without rewriting MPEGTS in the - * VTT header. Default no-op; subtec-based parsers override. - */ - virtual void setPtsOffset(double ptsOffsetSec) {} void RegisterCallback(const PlayerCallbacks& playerCallBack) { playerResumeTrackDownloads_CB = playerCallBack.resumeTrackDownloads_CB; diff --git a/test/utests/fakes/FakeSocUtils.cpp b/test/utests/fakes/FakeSocUtils.cpp index c278cd65..13c8137e 100644 --- a/test/utests/fakes/FakeSocUtils.cpp +++ b/test/utests/fakes/FakeSocUtils.cpp @@ -21,6 +21,10 @@ namespace SocUtils { + void Init() + { + } + void InitializePlatformConfigs() { } diff --git a/test/utests/tests/InterfacePlayerTests/InterfacePlayerFunctionTests.cpp b/test/utests/tests/InterfacePlayerTests/InterfacePlayerFunctionTests.cpp index 95585061..5f93fa11 100644 --- a/test/utests/tests/InterfacePlayerTests/InterfacePlayerFunctionTests.cpp +++ b/test/utests/tests/InterfacePlayerTests/InterfacePlayerFunctionTests.cpp @@ -1985,7 +1985,10 @@ TEST_F(InterfacePlayerTests, Pause_Success) mPlayerContext->pipeline = &gst_element_pipeline; EXPECT_CALL(*g_mockGStreamer, gst_element_get_state(_, NotNull(), NotNull(), _)) - .WillRepeatedly(Return(GST_STATE_CHANGE_SUCCESS)); + .WillRepeatedly(DoAll( + SetArgPointee<1>(GST_STATE_PAUSED), + SetArgPointee<2>(GST_STATE_NULL), + Return(GST_STATE_CHANGE_SUCCESS))); EXPECT_CALL(*g_mockGStreamer, gst_element_set_state(_, GST_STATE_PAUSED)) .WillOnce(Return(GST_STATE_CHANGE_ASYNC)); @@ -3085,216 +3088,3 @@ TEST_F(InterfacePlayerTests, SetStreamCaps_EncryptedAudioCodecFormat) delete g_mockGstUtils; } -/** - * @test GetVideoPosition_PipelineNull - * @brief Verify GetVideoPosition returns 0 when pipeline pointer is NULL. - */ -TEST_F(InterfacePlayerTests, GetVideoPosition_PipelineNull) -{ - mPlayerContext->pipeline = nullptr; - mPlayerContext->stream[eGST_MEDIATYPE_VIDEO].sinkbin = &gst_element_pipeline; - - EXPECT_CALL(*g_mockGStreamer, gst_element_get_state(_, _, _, _)).Times(0); - EXPECT_CALL(*g_mockGStreamer, gst_element_query_position(_, _, _)).Times(0); - - EXPECT_EQ(mInterfaceGstPlayer->GetVideoPosition(), 0); -} - -/** - * @test GetVideoPosition_VideoSinkbinNull - * @brief Verify GetVideoPosition returns 0 when the video sinkbin is NULL. - */ -TEST_F(InterfacePlayerTests, GetVideoPosition_VideoSinkbinNull) -{ - mPlayerContext->pipeline = &gst_element_pipeline; - mPlayerContext->stream[eGST_MEDIATYPE_VIDEO].sinkbin = nullptr; - - EXPECT_CALL(*g_mockGStreamer, gst_element_get_state(_, _, _, _)).Times(0); - EXPECT_CALL(*g_mockGStreamer, gst_element_query_position(_, _, _)).Times(0); - - EXPECT_EQ(mInterfaceGstPlayer->GetVideoPosition(), 0); -} - -/** - * @test GetVideoPosition_StateNotPlayingOrPaused - * @brief When the pipeline state is neither PLAYING nor PAUSED, the position - * query must be skipped and 0 returned. - */ -TEST_F(InterfacePlayerTests, GetVideoPosition_StateNotPlayingOrPaused) -{ - GstElement video_sinkbin = {.object = {.name = (gchar *)"video_sinkbin"}}; - mPlayerContext->pipeline = &gst_element_pipeline; - mPlayerContext->stream[eGST_MEDIATYPE_VIDEO].sinkbin = &video_sinkbin; - - EXPECT_CALL(*g_mockGStreamer, gst_element_get_state(&gst_element_pipeline, _, _, _)) - .WillOnce(DoAll( - SetArgPointee<1>(GST_STATE_READY), - SetArgPointee<2>(GST_STATE_READY), - Return(GST_STATE_CHANGE_SUCCESS))); - EXPECT_CALL(*g_mockGStreamer, gst_element_query_position(_, _, _)).Times(0); - - EXPECT_EQ(mInterfaceGstPlayer->GetVideoPosition(), 0); -} - -/** - * @test GetVideoPosition_GetStateFails - * @brief When gst_element_get_state does not return SUCCESS the position - * query must be skipped and 0 returned. - */ -TEST_F(InterfacePlayerTests, GetVideoPosition_GetStateFails) -{ - GstElement video_sinkbin = {.object = {.name = (gchar *)"video_sinkbin"}}; - mPlayerContext->pipeline = &gst_element_pipeline; - mPlayerContext->stream[eGST_MEDIATYPE_VIDEO].sinkbin = &video_sinkbin; - - EXPECT_CALL(*g_mockGStreamer, gst_element_get_state(&gst_element_pipeline, _, _, _)) - .WillOnce(DoAll( - SetArgPointee<1>(GST_STATE_PLAYING), - SetArgPointee<2>(GST_STATE_PLAYING), - Return(GST_STATE_CHANGE_FAILURE))); - EXPECT_CALL(*g_mockGStreamer, gst_element_query_position(_, _, _)).Times(0); - - EXPECT_EQ(mInterfaceGstPlayer->GetVideoPosition(), 0); -} - -/** - * @test GetVideoPosition_QueryFails - * @brief When state is PLAYING but gst_element_query_position fails, return 0. - */ -TEST_F(InterfacePlayerTests, GetVideoPosition_QueryFails) -{ - GstElement video_sinkbin = {.object = {.name = (gchar *)"video_sinkbin"}}; - mPlayerContext->pipeline = &gst_element_pipeline; - mPlayerContext->stream[eGST_MEDIATYPE_VIDEO].sinkbin = &video_sinkbin; - - EXPECT_CALL(*g_mockGStreamer, gst_element_get_state(&gst_element_pipeline, _, _, _)) - .WillOnce(DoAll( - SetArgPointee<1>(GST_STATE_PLAYING), - SetArgPointee<2>(GST_STATE_PLAYING), - Return(GST_STATE_CHANGE_SUCCESS))); - EXPECT_CALL(*g_mockGStreamer, - gst_element_query_position(&video_sinkbin, GST_FORMAT_TIME, _)) - .WillOnce(Return(FALSE)); - - EXPECT_EQ(mInterfaceGstPlayer->GetVideoPosition(), 0); -} - -/** - * @test GetVideoPosition_QuerySucceedsPlaying - * @brief Position is returned in milliseconds (ns -> ms) when state is PLAYING - * and the position query succeeds. - */ -TEST_F(InterfacePlayerTests, GetVideoPosition_QuerySucceedsPlaying) -{ - GstElement video_sinkbin = {.object = {.name = (gchar *)"video_sinkbin"}}; - mPlayerContext->pipeline = &gst_element_pipeline; - mPlayerContext->stream[eGST_MEDIATYPE_VIDEO].sinkbin = &video_sinkbin; - - const gint64 positionNs = 7000000; /* 7 ms in nanoseconds */ - EXPECT_CALL(*g_mockGStreamer, gst_element_get_state(&gst_element_pipeline, _, _, _)) - .WillOnce(DoAll( - SetArgPointee<1>(GST_STATE_PLAYING), - SetArgPointee<2>(GST_STATE_PLAYING), - Return(GST_STATE_CHANGE_SUCCESS))); - EXPECT_CALL(*g_mockGStreamer, - gst_element_query_position(&video_sinkbin, GST_FORMAT_TIME, _)) - .WillOnce(DoAll(SetArgPointee<2>(positionNs), Return(TRUE))); - - EXPECT_EQ(mInterfaceGstPlayer->GetVideoPosition(), 7); -} - -/** - * @test GetVideoPosition_QuerySucceedsPaused - * @brief Position is also queried in PAUSED state. - */ -TEST_F(InterfacePlayerTests, GetVideoPosition_QuerySucceedsPaused) -{ - GstElement video_sinkbin = {.object = {.name = (gchar *)"video_sinkbin"}}; - mPlayerContext->pipeline = &gst_element_pipeline; - mPlayerContext->stream[eGST_MEDIATYPE_VIDEO].sinkbin = &video_sinkbin; - - const gint64 positionNs = 12000000; /* 12 ms in nanoseconds */ - EXPECT_CALL(*g_mockGStreamer, gst_element_get_state(&gst_element_pipeline, _, _, _)) - .WillOnce(DoAll( - SetArgPointee<1>(GST_STATE_PAUSED), - SetArgPointee<2>(GST_STATE_PAUSED), - Return(GST_STATE_CHANGE_SUCCESS))); - EXPECT_CALL(*g_mockGStreamer, - gst_element_query_position(&video_sinkbin, GST_FORMAT_TIME, _)) - .WillOnce(DoAll(SetArgPointee<2>(positionNs), Return(TRUE))); - - EXPECT_EQ(mInterfaceGstPlayer->GetVideoPosition(), 12); -} - -/** - * @test GetVideoPTS_PropertySupported - * @brief When SocInterface::GetVideoPts returns a valid PTS, GetVideoPTS() - * returns it directly without falling back to the position query. - */ -TEST_F(InterfacePlayerTests, GetVideoPTS_PropertySupported) -{ - auto mockSoc = std::make_shared(); - mInterfacePrivatePlayer->socInterface = mockSoc; - - const long long ptsValue = 4500; - EXPECT_CALL(*mockSoc, GetVideoPts(_, _, _)) - .WillOnce(Return(ptsValue)); - - /* Position fallback must NOT be used when a valid PTS is returned. */ - EXPECT_CALL(*g_mockGStreamer, gst_element_get_state(_, _, _, _)).Times(0); - EXPECT_CALL(*g_mockGStreamer, gst_element_query_position(_, _, _)).Times(0); - - EXPECT_EQ(mInterfaceGstPlayer->GetVideoPTS(), ptsValue); -} - -/** - * @test GetVideoPTS_PropertyNotSupportedFallsBackToPosition - * @brief When SocInterface::GetVideoPts returns -1 (property not supported), - * InterfacePlayerRDK::GetVideoPTS falls back to - * 90 * GetVideoPosition() (90 kHz ticks per millisecond). - */ -TEST_F(InterfacePlayerTests, GetVideoPTS_PropertyNotSupportedFallsBackToPosition) -{ - auto mockSoc = std::make_shared(); - mInterfacePrivatePlayer->socInterface = mockSoc; - - GstElement video_sinkbin = {.object = {.name = (gchar *)"video_sinkbin"}}; - mPlayerContext->pipeline = &gst_element_pipeline; - mPlayerContext->stream[eGST_MEDIATYPE_VIDEO].sinkbin = &video_sinkbin; - - /* SocInterface reports property not supported. */ - EXPECT_CALL(*mockSoc, GetVideoPts(_, _, _)) - .WillOnce(Return(-1LL)); - - const gint64 positionNs = 10000000; /* 10 ms */ - EXPECT_CALL(*g_mockGStreamer, gst_element_get_state(&gst_element_pipeline, _, _, _)) - .WillOnce(DoAll( - SetArgPointee<1>(GST_STATE_PLAYING), - SetArgPointee<2>(GST_STATE_PLAYING), - Return(GST_STATE_CHANGE_SUCCESS))); - EXPECT_CALL(*g_mockGStreamer, - gst_element_query_position(&video_sinkbin, GST_FORMAT_TIME, _)) - .WillOnce(DoAll(SetArgPointee<2>(positionNs), Return(TRUE))); - - /* Expected: 90 * 10 ms = 900 (90 kHz ticks per ms). */ - EXPECT_EQ(mInterfaceGstPlayer->GetVideoPTS(), 900); -} - -/** - * @test GetVideoPTS_PropertyProbeOnceAtCreation - * @brief GetVideoPTS() delegates to SocInterface::GetVideoPts on every call - * and returns the same value consistently across multiple calls. - */ -TEST_F(InterfacePlayerTests, GetVideoPTS_PropertyProbeOnceAtCreation) -{ - auto mockSoc = std::make_shared(); - mInterfacePrivatePlayer->socInterface = mockSoc; - - const long long ptsValue = 1234; - EXPECT_CALL(*mockSoc, GetVideoPts(_, _, _)) - .Times(2) - .WillRepeatedly(Return(ptsValue)); - - EXPECT_EQ(mInterfaceGstPlayer->GetVideoPTS(), ptsValue); - EXPECT_EQ(mInterfaceGstPlayer->GetVideoPTS(), ptsValue); -}