diff --git a/CMakeLists.txt b/CMakeLists.txt index c99baabf..ac168b75 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -274,6 +274,7 @@ install(FILES closedcaptions/CCTrackInfo.h subtec/libsubtec/SubtecPacket.hpp playerisobmff/playerisobmffbuffer.h playerisobmff/playerisobmffbox.h + closedcaptions/direct-rialto/IDirectRialtoCC.h DESTINATION include) # Install the generated version header @@ -476,7 +477,7 @@ if (CMAKE_SUBTITLE_SUPPORT) ) - set(LIBPLAYERGSTINTERFACE_SOURCES ${LIBPLAYERGSTINTERFACE_SOURCES} closedcaptions/subtec/PlayerSubtecCCManager.cpp closedcaptions/rialto/PlayerRialtoCCManager.cpp) + set(LIBPLAYERGSTINTERFACE_SOURCES ${LIBPLAYERGSTINTERFACE_SOURCES} closedcaptions/subtec/PlayerSubtecCCManager.cpp closedcaptions/rialto/PlayerRialtoCCManager.cpp closedcaptions/direct-rialto/PlayerDirectRialtoCCManager.cpp) endif() add_library(playergstinterface SHARED ${SOURCES} ${LIBPLAYERGSTINTERFACE_HEADERS} ${LIBPLAYERGSTINTERFACE_SOURCES} ${LIBPLAYERGSTINTERFACE_DRM_SOURCES} ${LIBPLAYERGSTINTERFACE_HELP_SOURCES}) @@ -490,6 +491,7 @@ target_include_directories(playergstinterface PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/playerjsonobject ${CMAKE_CURRENT_SOURCE_DIR}/closedcaptions ${CMAKE_CURRENT_SOURCE_DIR}/closedcaptions/subtec + ${CMAKE_CURRENT_SOURCE_DIR}/closedcaptions/direct-rialto ${CMAKE_CURRENT_SOURCE_DIR}/vendor) if (CMAKE_SUBTITLE_SUPPORT) diff --git a/DemuxDataTypes.h b/DemuxDataTypes.h index bba1f12c..d0b613f3 100644 --- a/DemuxDataTypes.h +++ b/DemuxDataTypes.h @@ -94,6 +94,11 @@ struct MediaCodecInfo GstStreamOutputFormat mCodecFormat; // GST_FORMAT_VIDEO_ES_H264, etc std::vector mCodecData; // codec private data, e.g. avcC box bool mIsEncrypted; + // True when NAL units are length-prefixed (AVCC/HVCC, e.g. avcC/hvcC + // sample entries per ISO/IEC 14496-15); false for Annex-B (start-code + // delimited) bitstreams such as HLS-TS ES output. Meaningless for + // non-NAL-unit codecs (audio/subtitle), where it stays false. + bool mNaluLengthPrefixed; union { struct @@ -116,7 +121,7 @@ struct MediaCodecInfo * Uniform initialization is preferred for C++ types as it's type-safe, clearer in intent, * and works correctly with all C++ types including those with constructors. */ - MediaCodecInfo() : mCodecFormat(GST_FORMAT_INVALID), mIsEncrypted(false), mCodecData(), mInfo{0} + MediaCodecInfo() : mCodecFormat(GST_FORMAT_INVALID), mIsEncrypted(false), mNaluLengthPrefixed(false), mCodecData(), mInfo{0} { } @@ -128,7 +133,7 @@ struct MediaCodecInfo * Uniform initialization is preferred for C++ types as it's type-safe, clearer in intent, * and works correctly with all C++ types including those with constructors. */ - MediaCodecInfo(GstStreamOutputFormat format) : mCodecFormat(format), mIsEncrypted(false), mCodecData(), mInfo{0} + MediaCodecInfo(GstStreamOutputFormat format) : mCodecFormat(format), mIsEncrypted(false), mNaluLengthPrefixed(false), mCodecData(), mInfo{0} { } @@ -144,6 +149,7 @@ struct MediaCodecInfo : mCodecFormat(exchange(other.mCodecFormat, GST_FORMAT_INVALID)) , mCodecData(exchange(other.mCodecData, {})) , mIsEncrypted(exchange(other.mIsEncrypted, false)) + , mNaluLengthPrefixed(exchange(other.mNaluLengthPrefixed, false)) , mInfo(exchange(other.mInfo, {})) // POD union - exchange with zero-initialized union { } @@ -160,12 +166,24 @@ struct MediaCodecInfo mCodecFormat = exchange(other.mCodecFormat, GST_FORMAT_INVALID); mCodecData = exchange(other.mCodecData, {}); mIsEncrypted = exchange(other.mIsEncrypted, false); + mNaluLengthPrefixed = exchange(other.mNaluLengthPrefixed, false); mInfo = exchange(other.mInfo, {}); // POD union - exchange with zero-initialized union } return *this; } }; +/* + * @struct StreamCodecInfo + * @brief Codec information for all tracks in a stream + */ +struct StreamCodecInfo +{ + MediaCodecInfo video; + MediaCodecInfo audio; + MediaCodecInfo subtitle; +}; + /* * @struct MediaDrmMetadata * @brief DRM metadata for encrypted samples diff --git a/InterfacePlayerPriv.h b/InterfacePlayerPriv.h index 19270489..b2fc8f43 100755 --- a/InterfacePlayerPriv.h +++ b/InterfacePlayerPriv.h @@ -125,6 +125,7 @@ struct gst_media_stream GstElement *sinkbin; /**< Sink element to consume data */ GstElement *source; /**< to provide data to the pipeline */ GstStreamOutputFormat format; /**< Stream output format for this stream */ + MediaCodecInfo codecInfo; /**< Codec and encryption information for this stream */ bool pendingSeek; /**< Flag denotes if a seek event has to be sent to the source */ bool resetPosition; /**< To indicate that the position of the stream is reset */ bool bufferUnderrun; @@ -137,7 +138,7 @@ struct gst_media_stream GstPad *demuxPad; /**< Demux src pad >*/ gulong demuxProbeId; /**< Demux pad probe ID >*/ - gst_media_stream() : sinkbin(NULL), source(NULL), format(GST_FORMAT_INVALID), + gst_media_stream() : sinkbin(NULL), source(NULL), format(GST_FORMAT_INVALID), codecInfo(), pendingSeek(false), resetPosition(false), bufferUnderrun(false), eosReached(false), sourceConfigured(false), sourceLock(PTHREAD_MUTEX_INITIALIZER), timeScale(1), trackId(-1), firstBufferProcessed(false), demuxPad(NULL), demuxProbeId(0) { diff --git a/InterfacePlayerRDK.cpp b/InterfacePlayerRDK.cpp index d6018c30..919a8073 100644 --- a/InterfacePlayerRDK.cpp +++ b/InterfacePlayerRDK.cpp @@ -86,6 +86,49 @@ const char * CipherTypeToString(CipherType type) return "unknown"; } +static bool IsElementaryStreamFormat(GstStreamOutputFormat format) +{ + return format >= GST_FORMAT_AUDIO_ES_MP3 && format <= GST_FORMAT_VIDEO_ES_MPEG2; +} + +/** + * @brief Apply encrypted content signaling fields to stream caps. + * @param[in,out] caps Caps to update in-place. + * @param[in] drmSystem DRM system UUID string for protection metadata. + */ +static void TransformToEncryptedCaps(GstCaps *caps, const char *drmSystem) +{ + if (caps == NULL) + { + MW_LOG_WARN("Skipping encrypted caps conversion: caps is NULL"); + return; + } + if (drmSystem == NULL) + { + MW_LOG_WARN("Skipping encrypted caps conversion: DRM system ID is NULL"); + return; + } + GstStructure *structure = gst_caps_get_structure(caps, 0); + if (structure) + { + gst_structure_set(structure, + "original-media-type", G_TYPE_STRING, gst_structure_get_name(structure), + NULL); + if (drmSystem != NULL) + { + gst_structure_set(structure, + GST_PROTECTION_SYSTEM_ID_CAPS_FIELD, G_TYPE_STRING, drmSystem, + NULL); + } + // Same for both cenc and cbcs + gst_structure_set_name(structure, "application/x-cenc"); + } + else + { + MW_LOG_WARN("Skipping encrypted caps conversion: failed to get caps structure"); + } +} + /*InterfacePlayerRDK constructor*/ InterfacePlayerRDK::InterfacePlayerRDK(bool isRialto) : mProtectionLock(), mPauseInjector(false), mSourceSetupMutex(), stopCallback(NULL), tearDownCb(NULL), notifyFirstFrameCallback(NULL), @@ -292,27 +335,56 @@ static void DecorateGstBufferWithDrmMetadata(GstBuffer *buffer, const MediaDrmMe void InterfacePlayerRDK::ConfigurePipeline(int format, int audioFormat, int subFormat, bool bESChangeStatus, bool setReadyAfterPipelineCreation, bool isSubEnable, int32_t trackId, gint rate, const char *pipelineName, int PipelinePriority, bool FirstFrameFlag, std::string manifestUrl, bool enableLiveLatency) +{ + StreamCodecInfo streamInfo; + streamInfo.video.mCodecFormat = static_cast(format); + streamInfo.audio.mCodecFormat = static_cast(audioFormat); + streamInfo.subtitle.mCodecFormat = static_cast(subFormat); + ConfigurePipeline(std::move(streamInfo), bESChangeStatus, setReadyAfterPipelineCreation, + isSubEnable, trackId, rate, pipelineName, PipelinePriority, FirstFrameFlag, std::move(manifestUrl), enableLiveLatency); +} + +/** + * @brief Configures the GStreamer pipeline. + * @param codecInfo Codec information for the stream. + * @param bESChangeStatus Whether ES change status is enabled. + * @param setReadyAfterPipelineCreation Whether to set the player as ready after pipeline creation. + * @param isSubEnable Whether subtitles are enabled. + * @param trackId Track ID. + * @param rate Bitrate. + * @param pipelineName Pipeline name. + * @param PipelinePriority Pipeline priority. + * @param FirstFrameFlag Whether the first-frame callback is required. + * @param manifestUrl URL of the manifest used to configure stream setup. + * @param enableLiveLatency Whether to enable live-latency mode in the + * RialtoSink streams-info context (passed as enable-live-latency). + */ +void InterfacePlayerRDK::ConfigurePipeline(StreamCodecInfo&& codecInfo, + bool bESChangeStatus, bool setReadyAfterPipelineCreation, + bool isSubEnable, int32_t trackId, gint rate, const char *pipelineName, int PipelinePriority, bool FirstFrameFlag, std::string manifestUrl, bool enableLiveLatency) { mFirstFrameRequired = FirstFrameFlag; - GstStreamOutputFormat gstFormat = static_cast(format); - GstStreamOutputFormat gstAudioFormat = static_cast(audioFormat); - GstStreamOutputFormat gstSubFormat = static_cast(subFormat); + MediaCodecInfo* codecInfoByTrack[GST_TRACK_COUNT] = { + &codecInfo.video, + &codecInfo.audio, + &codecInfo.subtitle + }; GstStreamOutputFormat newFormat[GST_TRACK_COUNT]; - newFormat[eGST_MEDIATYPE_VIDEO] = gstFormat; - newFormat[eGST_MEDIATYPE_AUDIO] = gstAudioFormat; + newFormat[eGST_MEDIATYPE_VIDEO] = static_cast(codecInfo.video.mCodecFormat);; + newFormat[eGST_MEDIATYPE_AUDIO] = static_cast(codecInfo.audio.mCodecFormat); bool newClosedCaptionsControl = false; if(isSubEnable) { MW_LOG_MIL("Gstreamer subs enabled"); - newFormat[eGST_MEDIATYPE_SUBTITLE] = gstSubFormat; + newFormat[eGST_MEDIATYPE_SUBTITLE] = static_cast(codecInfo.subtitle.mCodecFormat); } else { MW_LOG_MIL("Gstreamer subs disabled"); - newFormat[eGST_MEDIATYPE_SUBTITLE]=GST_FORMAT_INVALID; + newFormat[eGST_MEDIATYPE_SUBTITLE] = GST_FORMAT_INVALID; } if(!(m_gstConfigParam->useWesterosSink)) @@ -320,7 +392,6 @@ void InterfacePlayerRDK::ConfigurePipeline(int format, int audioFormat, int subF interfacePlayerPriv->gstPrivateContext->using_westerossink = false; interfacePlayerPriv->gstPrivateContext->firstTuneWithWesterosSinkOff = interfacePlayerPriv->socInterface->IsFirstTuneWithWesteros(); } - else { interfacePlayerPriv->gstPrivateContext->using_westerossink = true; @@ -337,7 +408,7 @@ void InterfacePlayerRDK::ConfigurePipeline(int format, int audioFormat, int subF interfacePlayerPriv->gstPrivateContext->usingRialtoSink = true; // If no subtitles defined, then create a closed caption control stream - newClosedCaptionsControl = (gstSubFormat == GST_FORMAT_INVALID); + newClosedCaptionsControl = (static_cast(codecInfo.subtitle.mCodecFormat) == GST_FORMAT_INVALID); // To avoid out of band subtitles being removed during trickplay, // check if they were previously configured, and don't enable Closed Caption Control. @@ -384,20 +455,24 @@ void InterfacePlayerRDK::ConfigurePipeline(int format, int audioFormat, int subF for (int i = 0; i < GST_TRACK_COUNT; i++) { gst_media_stream *stream = &interfacePlayerPriv->gstPrivateContext->stream[i]; - if(stream->format != newFormat[i]) + bool isInitialSetup = (stream->format == GST_FORMAT_INVALID && newFormat[i] == GST_FORMAT_UNKNOWN); + bool isValidNewFormat = (newFormat[i] != GST_FORMAT_INVALID && newFormat[i] != GST_FORMAT_UNKNOWN); + bool isEncryptionChanged = (stream->codecInfo.mIsEncrypted != codecInfoByTrack[i]->mIsEncrypted); + // The initial-setup case must be honoured even for an unknown format (e.g. muxed HLS/TS, or audio + // remapped to the video slot) because the real format is only discovered from the demuxer later - + // without a source element the injector never unblocks. An encryption-only change is only + // actionable once the format is usable. + bool shouldReconfigure = (stream->format != newFormat[i] && (isValidNewFormat || isInitialSetup)) + || (isEncryptionChanged && isValidNewFormat); + if(shouldReconfigure) + { + MW_LOG_MIL("Closing stream %d old format = %d, new format = %d",i, stream->format, newFormat[i]); + configureStream[i] = true; + interfacePlayerPriv->gstPrivateContext->NumberOfTracks++; + } + else { - bool isInitialSetup = (stream->format == GST_FORMAT_INVALID || stream->format == GST_FORMAT_UNKNOWN); - bool isValidNewFormat = (newFormat[i] != GST_FORMAT_INVALID && newFormat[i] != GST_FORMAT_UNKNOWN); - if (isValidNewFormat || isInitialSetup) - { - MW_LOG_MIL("Closing stream %d old format = %d, new format = %d",i, stream->format, newFormat[i]); - configureStream[i] = true; - interfacePlayerPriv->gstPrivateContext->NumberOfTracks++; - } - else - { - MW_LOG_MIL("Skipping reconfiguration for stream %d - both format invalid/unknown",i); - } + MW_LOG_MIL("Skipping reconfiguration for stream %d for [%d]->[%d], isEncryptionChanged = %d",i, stream->format, newFormat[i], isEncryptionChanged); } if(interfacePlayerPriv->socInterface->ShouldTearDownForTrickplay()) { @@ -434,6 +509,7 @@ void InterfacePlayerRDK::ConfigurePipeline(int format, int audioFormat, int subF trickTeardown = false; TearDownStream((int)i); stream->format = newFormat[i]; + stream->codecInfo = std::move(*codecInfoByTrack[i]); stream->trackId = trackId; /* Sets up the stream for the given MediaType */ @@ -468,7 +544,7 @@ void InterfacePlayerRDK::ConfigurePipeline(int format, int audioFormat, int subF g_object_get(interfacePlayerPriv->gstPrivateContext->stream[eGST_MEDIATYPE_VIDEO].sinkbin, "video-sink", &vidsink, NULL); if(vidsink) { - gboolean videoOnly = (audioFormat == GST_FORMAT_INVALID); + gboolean videoOnly = (newFormat[eGST_MEDIATYPE_AUDIO] == 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 @@ -515,7 +591,7 @@ void InterfacePlayerRDK::ConfigurePipeline(int format, int audioFormat, int subF } } /* If buffering is enabled, set the pipeline in Paused state, once sufficient content has been buffered the pipeline will be set to GST_STATE_PLAYING */ - else if (interfacePlayerPriv->gstPrivateContext->buffering_enabled && format != GST_FORMAT_INVALID && GST_NORMAL_PLAY_RATE == interfacePlayerPriv->gstPrivateContext->rate) + else if (interfacePlayerPriv->gstPrivateContext->buffering_enabled && newFormat[eGST_MEDIATYPE_VIDEO] != GST_FORMAT_INVALID && GST_NORMAL_PLAY_RATE == interfacePlayerPriv->gstPrivateContext->rate) { MW_LOG_INFO("Setting state to GST_STATE_PAUSED, target state to GST_STATE_PLAYING"); interfacePlayerPriv->gstPrivateContext->buffering_target_state = GST_STATE_PLAYING; @@ -1382,6 +1458,7 @@ void InterfacePlayerRDK::TearDownStream(int type) interfacePlayerPriv->gstPrivateContext->decoderHandleNotified = false; } stream->format = GST_FORMAT_INVALID; + stream->codecInfo = MediaCodecInfo{}; if (mediaType == eGST_MEDIATYPE_SUBTITLE) { interfacePlayerPriv->gstPrivateContext->usingClosedCaptionsControl = false; @@ -1869,6 +1946,7 @@ void InterfacePlayerRDK::InitializeSourceForPlayer(void *PlayerInstance, void * GstCaps * caps = NULL; GstMediaType mediaType = static_cast(type); gst_media_stream *stream = &privatePlayer->gstPrivateContext->stream[mediaType]; + MW_LOG_MIL("Entry type[%d] format[%d] encrypted[%d]", mediaType, stream->format, stream->codecInfo.mIsEncrypted); privatePlayer->SignalConnect(source, "need-data", G_CALLBACK(gst_need_data), _this); privatePlayer->SignalConnect(source, "enough-data", G_CALLBACK(gst_enough_data), _this); /* Sets up the call back function for enough data event */ privatePlayer->SignalConnect(source, "seek-data", G_CALLBACK(gstappsrc_seek), _this); /* Sets up the call back function for seek data event */ @@ -1905,11 +1983,19 @@ void InterfacePlayerRDK::InitializeSourceForPlayer(void *PlayerInstance, void * } else { + // Initial source caps intentionally use only the manifest-derived stream format and encryption flag. + // Codec private data, dimensions, channels, and sample rate are parsed from the MP4 init segment and + // applied through SetStreamCaps() before elementary media samples are injected. caps = GetCaps(static_cast(stream->format)); } if (caps != NULL) { + if (stream->codecInfo.mIsEncrypted && IsElementaryStreamFormat(static_cast(stream->format))) + { + MW_LOG_DEBUG("Applying encrypted caps during source configuration for type[%d] format[%d]", mediaType, stream->format); + TransformToEncryptedCaps(caps, mDrmSystem); + } gchar *capsStr = gst_caps_to_string(caps); MW_LOG_MIL("Setting caps for source[type:%d]: %s", mediaType, capsStr); g_free(capsStr); @@ -3700,10 +3786,46 @@ bool InterfacePlayerRDK::CheckDiscontinuity(int mediaType, int streamFormat , bo { MW_LOG_WARN("PTS-RESTAMP ENABLED, but we have codec change, so Signal EOS (%s).",gstGetMediaTypeName(type)); } - GstPlayer_SignalEOS(stream); - // We are in buffering, but we received discontinuity, un-pause pipeline - shouldHaltBuffering = true; - ret = true; + + // For a muxed audio/video discontinuity, only trigger EOS once BOTH tracks have + // resumed data flow (firstBufferProcessed) after the reset. Signalling EOS for + // only one side leaves its sibling's appsrc out of sync - causing audio loss / + // video freeze. Codec-change always proceeds immediately since the decoder swap + // can't wait on the sibling track's buffer flow. + bool proceedWithEos = true; + GstMediaType siblingType = (type == eGST_MEDIATYPE_AUDIO) ? eGST_MEDIATYPE_VIDEO : + (type == eGST_MEDIATYPE_VIDEO) ? eGST_MEDIATYPE_AUDIO : eGST_MEDIATYPE_DEFAULT; + if (!codecChange && siblingType != eGST_MEDIATYPE_DEFAULT) + { + gst_media_stream *siblingStream = &interfacePlayerPriv->gstPrivateContext->stream[siblingType]; + if (siblingStream->format != GST_FORMAT_INVALID && !siblingStream->firstBufferProcessed) + { + proceedWithEos = false; + } + } + + if (proceedWithEos) + { + GstPlayer_SignalEOS(stream); + // We are in buffering, but we received discontinuity, un-pause pipeline + shouldHaltBuffering = true; + ret = true; + + //If we have an audio discontinuity, signal subtec as well + if ((type == eGST_MEDIATYPE_AUDIO) && (interfacePlayerPriv->gstPrivateContext->stream[eGST_MEDIATYPE_SUBTITLE].source)) + { + GstPlayer_SignalEOS(interfacePlayerPriv->gstPrivateContext->stream[eGST_MEDIATYPE_SUBTITLE]); + } + else + { + MW_LOG_WARN("Skipped EOS %s", gstGetMediaTypeName(type)); + } + } + else + { + MW_LOG_WARN("Skipping appsrc EOS for %s discontinuity - sibling track not yet ready (firstBufferProcessed=0)", gstGetMediaTypeName(type)); + } + //If we have an audio discontinuity, signal subtec as well if ((type == eGST_MEDIATYPE_AUDIO) && (interfacePlayerPriv->gstPrivateContext->stream[eGST_MEDIATYPE_SUBTITLE].source)) @@ -5561,21 +5683,7 @@ void InterfacePlayerRDK::SetStreamCaps(GstMediaType type, MediaCodecInfo&& codec } if (codecInfo.mIsEncrypted) { - GstStructure *s = gst_caps_get_structure (caps, 0); - if (s) - { - gst_structure_set (s, - "original-media-type", G_TYPE_STRING, gst_structure_get_name (s), - NULL); - if (mDrmSystem != NULL) - { - gst_structure_set (s, - GST_PROTECTION_SYSTEM_ID_CAPS_FIELD, G_TYPE_STRING, mDrmSystem, - NULL); - } - // Same for both cenc and cbcs - gst_structure_set_name (s, "application/x-cenc"); - } + TransformToEncryptedCaps(caps, mDrmSystem); } gchar* capsStr = gst_caps_to_string(caps); MW_LOG_MIL("Setting stream caps for type[%d] format[%d]: %s", type, codecInfo.mCodecFormat, capsStr); @@ -5587,6 +5695,7 @@ void InterfacePlayerRDK::SetStreamCaps(GstMediaType type, MediaCodecInfo&& codec { MW_LOG_ERR("Failed to get caps for type[%d] format[%d]", type, codecInfo.mCodecFormat); } + stream->codecInfo = std::move(codecInfo); } /** diff --git a/InterfacePlayerRDK.h b/InterfacePlayerRDK.h index ba5a0908..2b125fa0 100644 --- a/InterfacePlayerRDK.h +++ b/InterfacePlayerRDK.h @@ -335,7 +335,7 @@ class InterfacePlayerRDK *@param[in] drmID preferred drm */ void SetPreferredDRM(const char *drmID); - /** + /** * @brief Configures the GStreamer pipeline. * @param format Video format. * @param audioFormat Audio format. @@ -353,6 +353,24 @@ class InterfacePlayerRDK * RialtoSink streams-info context. Defaults to false. */ void ConfigurePipeline(int, int, int, bool, bool, bool, int32_t, gint, const char *, int, bool, std::string url, bool enableLiveLatency = false); + + /** + * @brief Configures the GStreamer pipeline. + * @param codecInfo Codec information for the stream. + * @param bESChangeStatus Whether ES change status is enabled. + * @param setReadyAfterPipelineCreation Whether to set the player as ready after pipeline creation. + * @param isSubEnable Whether subtitles are enabled. + * @param trackId Track ID. + * @param rate Bitrate. + * @param pipelineName Pipeline name. + * @param PipelinePriority Pipeline priority. + * @param FirstFrameFlag Whether the first-frame callback is required. + * @param url URL of the manifest used to configure stream setup. + * @param enableLiveLatency Whether to enable live-latency mode in the + * RialtoSink streams-info context. Defaults to false. + */ + void ConfigurePipeline(StreamCodecInfo&& codecInfo, bool bESChangeStatus, bool setReadyAfterPipelineCreation, bool isSubEnable, int32_t trackId, gint rate, const char *pipelineName, int PipelinePriority, bool FirstFrameFlag, std::string url, bool enableLiveLatency = false); + /** * @brief Enables or disables pausing on playback start. * @param enable True to enable pausing, false to disable. diff --git a/closedcaptions/PlayerCCManager.cpp b/closedcaptions/PlayerCCManager.cpp index d61b379e..f29c882b 100644 --- a/closedcaptions/PlayerCCManager.cpp +++ b/closedcaptions/PlayerCCManager.cpp @@ -34,6 +34,7 @@ #include "PlayerCCManager.h" #include "PlayerSubtecCCManager.h" #include "PlayerRialtoCCManager.h" +#include "PlayerDirectRialtoCCManager.h" #define CHAR_CODE_1 49 @@ -823,9 +824,10 @@ bool PlayerCCManagerBase::IsOOBCCRenderingSupported() PlayerCCManagerBase *PlayerCCManager::mInstance = NULL; /** - * @brief Indicates whether mInstance should be a Rialto or a Subtec class. + * @brief Determines which CC manager subclass to instantiate. */ -bool PlayerCCManager::mIsRialto = false; +PlayerCCManager::CCManagerType PlayerCCManager::mCCManagerType = + PlayerCCManager::CCManagerType::SubtecCCManager; /** * @brief Get the singleton instance @@ -835,7 +837,12 @@ PlayerCCManagerBase *PlayerCCManager::GetInstance() if (mInstance == NULL) { #if defined(SUBTITLE_SUPPORTED) - if (mIsRialto) + if (mCCManagerType == CCManagerType::DirectRialtoCCManager) + { + MW_LOG_INFO("PlayerCCManager::Creating DirectRialto CC manager"); + mInstance = new PlayerDirectRialtoCCManager(); + } + else if (mCCManagerType == CCManagerType::RialtoCCManager) { MW_LOG_INFO("PlayerCCManager::Creating Rialto CC manager"); mInstance = new PlayerRialtoCCManager(); @@ -853,6 +860,14 @@ PlayerCCManagerBase *PlayerCCManager::GetInstance() return mInstance; } +/** + * @brief Check whether the singleton has already been created + */ +bool PlayerCCManager::HasInstance() +{ + return mInstance != NULL; +} + /** * @brief Reset the state. */ @@ -860,7 +875,14 @@ void PlayerCCManagerBase::ResetState() { MW_LOG_INFO("PlayerCCManagerBase::Resetting"); Stop(); + ResetTrackState(); +} +/** + * @brief Reset the cached track/rendering fields without stopping. + */ +void PlayerCCManagerBase::ResetTrackState() +{ mOptions = ""; mTrack = ""; mTrackFormat = eCLOSEDCAPTION_FORMAT_DEFAULT; @@ -871,18 +893,30 @@ void PlayerCCManagerBase::ResetState() } /** - * @brief Set the variant required + * @brief Set the CC manager variant */ -void PlayerCCManager::SetRialto(bool bIsRialto) +void PlayerCCManager::SetRialto(bool bIsRialto, bool bIsDirectRialto) { + CCManagerType newType = CCManagerType::SubtecCCManager; + + if (bIsDirectRialto) + { + newType = CCManagerType::DirectRialtoCCManager; + } + else if (bIsRialto) + { + newType = CCManagerType::RialtoCCManager; + } + if (mInstance == NULL) { - MW_LOG_INFO("PlayerCCManager::IsRialto:%d", bIsRialto); - mIsRialto = bIsRialto; + MW_LOG_INFO("PlayerCCManager::CCManagerType:%d", static_cast(newType)); + mCCManagerType = newType; } - else if (mIsRialto != bIsRialto) + else if (mCCManagerType != newType) { - MW_LOG_ERR("PlayerCCManager::IsRialto:%d while incompatible singleton instance exists", bIsRialto); + MW_LOG_ERR("PlayerCCManager::CCManagerType:%d while incompatible singleton instance exists", + static_cast(newType)); } } diff --git a/closedcaptions/PlayerCCManager.h b/closedcaptions/PlayerCCManager.h index 6990f53c..41642049 100644 --- a/closedcaptions/PlayerCCManager.h +++ b/closedcaptions/PlayerCCManager.h @@ -247,6 +247,17 @@ class PlayerCCManagerBase */ virtual void ResetState(); + /** + * @brief Reset mOptions/mTrack/mTrackFormat/mLastTextTracks/mEnabled/ + * mTrickplayStarted/mParentalCtrlLocked to their initial values. + * Does NOT call Stop(). Subclasses whose SetTrack()/Initialize() + * access these fields under a control mutex must call this while + * holding that same mutex (after calling Stop() separately, + * outside the lock) instead of calling ResetState() directly, to + * avoid racing with a concurrent SetTrack()/Initialize() call. + */ + void ResetTrackState(); + /* NOTE WELL: The ResetState() method resets these member variables back to ** their initial state. It should be updated if any of the following change ** or are added to. */ @@ -257,6 +268,23 @@ class PlayerCCManagerBase bool mEnabled{false}; /**< true if CC rendering enabled, false otherwise */ bool mTrickplayStarted{false}; /**< If a trickplay is going on or not */ bool mParentalCtrlLocked{false}; /**< If Parental Control lock enabled on not */ + +public: + /** + * @brief Clear the stored control handle if it currently equals handle. + * Called by the handle owner's destructor so a handle can never be + * used after the object it points to is freed, independent of + * whether the GetId()/Release() usage count has reached zero (it + * may not have, if another session is still registered - see + * multi-pipeline mode). + * Declared last (appended after the pre-existing virtual + * interface, not inserted between Release() and SetStatus()) so it + * only extends the vtable instead of shifting every later slot, + * which would silently mis-dispatch calls made through a binary + * built against an older header. + * @param[in] handle - the handle being invalidated + */ + virtual void InvalidateHandle(void *) {} }; /** @@ -274,12 +302,26 @@ class PlayerCCManager */ static PlayerCCManagerBase * GetInstance(); + /** + * @fn HasInstance + * @brief Check whether GetInstance() has already created the singleton, + * without creating it as a side effect. + * + * @return bool - true if an instance exists + */ + static bool HasInstance(); + /** * @fn SetRialto + * @brief Configure which CC manager subclass GetInstance() will create. * + * @param[in] bIsRialto true when using the Rialto GStreamer sink + * (PlayerRialtoCCManager). + * @param[in] bIsDirectRialto true when using the direct-Rialto path + * (PlayerDirectRialtoCCManager). * @return void */ - static void SetRialto(bool bIsRialto); + static void SetRialto(bool bIsRialto, bool bIsDirectRialto = false); /** * @fn DestroyInstance @@ -289,8 +331,19 @@ class PlayerCCManager static void DestroyInstance(); private: - static PlayerCCManagerBase *mInstance; /**< Singleton instance */ - static bool mIsRialto; /**< Determines which class to instantiate */ + /** + * @enum CCManagerType + * @brief Identifies which PlayerCCManagerBase subclass to instantiate. + */ + enum class CCManagerType + { + SubtecCCManager, ///< Use PlayerSubtecCCManager (default) + RialtoCCManager, ///< Use PlayerRialtoCCManager + DirectRialtoCCManager ///< Use PlayerDirectRialtoCCManager + }; + + static PlayerCCManagerBase *mInstance; /**< Singleton instance */ + static CCManagerType mCCManagerType; /**< Determines which class to instantiate */ }; class PlayerFakeCCManager : public PlayerCCManagerBase diff --git a/closedcaptions/direct-rialto/IDirectRialtoCC.h b/closedcaptions/direct-rialto/IDirectRialtoCC.h new file mode 100755 index 00000000..46b202a4 --- /dev/null +++ b/closedcaptions/direct-rialto/IDirectRialtoCC.h @@ -0,0 +1,64 @@ +/* + * 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 IDirectRialtoCC.h + * @brief Narrow control interface for closed-caption operations in the + * direct-rialto path. + * + * AampRialtoPlayer implements this interface and passes itself (cast to + * IDirectRialtoCC*) to PlayerDirectRialtoCCManager::Initialize() so that + * the CC manager can drive track selection and muting through the Rialto + * IMediaPipeline API without depending on GStreamer. + */ + +#ifndef IDIRECT_RIALTO_CC_H +#define IDIRECT_RIALTO_CC_H + +#include + +/** + * @interface IDirectRialtoCC + * @brief Minimal CC-control interface implemented by AampRialtoPlayer. + * + * Decouples PlayerDirectRialtoCCManager (which needs no Rialto headers) from + * AampRialtoPlayer (which owns the IMediaPipeline) so that neither class + * needs to include the other's full header. + */ +class IDirectRialtoCC +{ +public: + virtual ~IDirectRialtoCC() = default; + + /** + * @brief Set the active CC text-track identifier on the pipeline. + * @param id Track identifier string (e.g. "CC1", "SERVICE1"). + * @return true on success. + */ + virtual bool setTextTrackIdentifier(const std::string &id) = 0; + + /** + * @brief Mute or un-mute CC rendering via the pipeline. + * @param muted true to mute; false to un-mute. + * @return true on success. + */ + virtual bool setCCMute(bool muted) = 0; +}; + +#endif // IDIRECT_RIALTO_CC_H diff --git a/closedcaptions/direct-rialto/PlayerDirectRialtoCCManager.cpp b/closedcaptions/direct-rialto/PlayerDirectRialtoCCManager.cpp new file mode 100755 index 00000000..86c5eeb4 --- /dev/null +++ b/closedcaptions/direct-rialto/PlayerDirectRialtoCCManager.cpp @@ -0,0 +1,229 @@ +/* + * 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 PlayerDirectRialtoCCManager.cpp + * @brief Implementation of PlayerDirectRialtoCCManager. + */ + +#include "PlayerDirectRialtoCCManager.h" +#include "PlayerLogManager.h" + +#include +#include + +// --------------------------------------------------------------------------- +// Private helpers +// --------------------------------------------------------------------------- + +/*static*/ +std::string PlayerDirectRialtoCCManager::mapTrackIdentifier( + const std::string &track, CCFormat format) +{ + // If the track string already has an alphabetic prefix (e.g. "CC1", + // "SERVICE3"), use it as-is. If it starts with a digit, prepend the + // appropriate prefix so the Rialto server can identify the CC service. + // This mirrors the logic in PlayerRialtoCCManager::SetTrack(). + if (!track.empty() && std::isdigit(static_cast(track[0]))) + { + std::string prefix; + if (format == eCLOSEDCAPTION_FORMAT_608) + { + prefix = "CC"; + } + else if (format == eCLOSEDCAPTION_FORMAT_708) + { + prefix = "SERVICE"; + } + return prefix + track; + } + return track; +} + +// --------------------------------------------------------------------------- +// PlayerCCManagerBase overrides +// --------------------------------------------------------------------------- + +int PlayerDirectRialtoCCManager::Initialize(void *handle) +{ + MW_LOG_INFO("ENTRY handle=%p", handle); + + auto *newControl = static_cast(handle); + // Held for the whole handle-assignment + configuration sequence so a + // concurrent Initialize()/SetTrack() call can't interleave and configure + // the wrong handle (or leave this handle unconfigured). + std::lock_guard lock(m_controlMutex); + const bool changedHandle = (newControl != m_control); + m_control = newControl; + + if (newControl == nullptr) + { + MW_LOG_WARN("Initialize called with null handle"); + MW_LOG_INFO("EXIT"); + return 0; + } + + if (mTrack.empty()) + { + // Apps expect CC1 as the default; apply it so the first frame + // renders without an explicit SetTextTrack() call. + MW_LOG_INFO("Setting default track CC1"); + if (SetTrackLocked("CC1", eCLOSEDCAPTION_FORMAT_DEFAULT) != 0) + { + MW_LOG_WARN("Failed to set default track CC1"); + m_control = nullptr; + return -1; + } + } + else if (changedHandle) + { + // Re-apply the cached track on the new handle (e.g. re-tune). + if (SetTrackLocked(mTrack, mTrackFormat) != 0) + { + MW_LOG_WARN("Failed to reapply cached track on new handle"); + m_control = nullptr; + return -1; + } + } + + MW_LOG_INFO("EXIT"); + return 0; +} + +int PlayerDirectRialtoCCManager::GetId() +{ + std::lock_guard lock(m_idLock); + ++m_id; + m_idSet.insert(m_id); + MW_LOG_INFO("id=%d users=%zu", m_id, m_idSet.size()); + return m_id; +} + +void PlayerDirectRialtoCCManager::Release(int id) +{ + std::lock_guard lock(m_idLock); + if (m_idSet.erase(id) > 0) + { + MW_LOG_INFO("id=%d users=%zu", id, m_idSet.size()); + if (m_idSet.empty()) + { + ResetState(); + } + } + else + { + MW_LOG_WARN("id=%d not found", id); + } +} + +void PlayerDirectRialtoCCManager::InvalidateHandle(void *handle) +{ + // Holding m_controlMutex blocks until any SetTrack() / StartRendering() / + // StopRendering() call already in progress on the old handle has + // returned, so the caller can safely destroy the handle owner once this + // call returns. + std::lock_guard lock(m_controlMutex); + if (handle != nullptr && m_control == handle) + { + m_control = nullptr; + MW_LOG_WARN("handle=%p invalidated ahead of Release()", handle); + } +} + + +int PlayerDirectRialtoCCManager::SetTrack( + const std::string &track, CCFormat format) +{ + // mTrack/mTrackFormat are shared with Initialize() and ResetState(), so + // they must be updated under the same lock as m_control. + std::lock_guard lock(m_controlMutex); + return SetTrackLocked(track, format); +} + +int PlayerDirectRialtoCCManager::SetTrackLocked( + const std::string &track, CCFormat format) +{ + MW_LOG_INFO("track=\"%s\" format=%d", track.c_str(), static_cast(format)); + + if (m_control == nullptr) + { + // No backend to apply against yet; cache unconditionally so + // Initialize() can apply it once a control becomes available. + mTrack = track; + mTrackFormat = format; + MW_LOG_INFO("No control handle — track cached"); + return 0; + } + + const std::string identifier = mapTrackIdentifier(track, format); + MW_LOG_INFO("setTextTrackIdentifier=\"%s\"", identifier.c_str()); + const bool ok = m_control->setTextTrackIdentifier(identifier); + if (ok) + { + // Only cache once the backend confirms the change, so a rejected + // track doesn't leave mTrack out of sync with what the control is + // still actually rendering (which would make a later Initialize() + // with the same handle wrongly skip reapplying it). + mTrack = track; + mTrackFormat = format; + } + return ok ? 0 : -1; +} + +void PlayerDirectRialtoCCManager::StartRendering() +{ + MW_LOG_INFO("ENTRY — unmuting CC"); + std::lock_guard lock(m_controlMutex); + if (m_control == nullptr) + { + MW_LOG_WARN("No control handle — cannot unmute"); + return; + } + m_control->setCCMute(false); + MW_LOG_INFO("EXIT"); +} + +void PlayerDirectRialtoCCManager::StopRendering() +{ + MW_LOG_INFO("ENTRY — muting CC"); + std::lock_guard lock(m_controlMutex); + if (m_control == nullptr) + { + MW_LOG_WARN("No control handle — cannot mute"); + return; + } + m_control->setCCMute(true); + MW_LOG_INFO("EXIT"); +} + +void PlayerDirectRialtoCCManager::ResetState() +{ + MW_LOG_INFO("ENTRY"); + // Stop() -> StopRendering() re-enters m_controlMutex; call it before + // taking the lock to avoid deadlocking on this non-recursive mutex. + Stop(); + std::lock_guard lock(m_controlMutex); + // Reset the cached track/rendering fields under the same lock that + // SetTrack()/Initialize() use to access them, avoiding a race with a + // concurrent call to either. + ResetTrackState(); + m_control = nullptr; + MW_LOG_INFO("EXIT"); +} + diff --git a/closedcaptions/direct-rialto/PlayerDirectRialtoCCManager.h b/closedcaptions/direct-rialto/PlayerDirectRialtoCCManager.h new file mode 100755 index 00000000..18c9c43a --- /dev/null +++ b/closedcaptions/direct-rialto/PlayerDirectRialtoCCManager.h @@ -0,0 +1,129 @@ +/* + * 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 PlayerDirectRialtoCCManager.h + * @brief PlayerCCManagerBase subclass for the direct-rialto backend. + * + * Replaces PlayerRialtoCCManager (which calls g_object_set on a GstElement*) + * with a GStreamer-free implementation that forwards track selection and + * mute/un-mute through the IDirectRialtoCC control interface implemented by + * AampRialtoPlayer. + * + * Lifecycle: + * 1. PlayerCCManager::GetInstance() creates this instance when + * mCCManagerType == DirectRialtoCCManager, which is set by calling + * PlayerCCManager::SetRialto(true, true) during player construction. + * 2. When the first PLAYING playback-state arrives, AampRialtoPlayer passes + * itself (as IDirectRialtoCC*) via NotifyFirstFrameReceived(), which + * flows through priv_aamp::InitializeCC() → PlayerCCManagerBase::Init() + * → Initialize() here, storing the control pointer. + * 3. Subsequent SetTrack() / StartRendering() / StopRendering() calls from + * priv_aamp drive track selection and muting through the stored pointer. + */ + +#ifndef PLAYER_DIRECT_RIALTO_CC_MANAGER_H +#define PLAYER_DIRECT_RIALTO_CC_MANAGER_H + +#include "PlayerCCManager.h" +#include "IDirectRialtoCC.h" + +#include +#include + +/** + * @class PlayerDirectRialtoCCManager + * @brief Closed-caption manager for the direct-rialto path. + */ +class PlayerDirectRialtoCCManager : public PlayerCCManagerBase +{ +public: + PlayerDirectRialtoCCManager() = default; + ~PlayerDirectRialtoCCManager() override = default; + + /// @copydoc PlayerCCManagerBase::GetId + int GetId() override; + + /// @copydoc PlayerCCManagerBase::Release + void Release(int iID) override; + + /// @copydoc PlayerCCManagerBase::InvalidateHandle + void InvalidateHandle(void *handle) override; + + /// @copydoc PlayerCCManagerBase::SetTrack + int SetTrack(const std::string &track, + CCFormat format = eCLOSEDCAPTION_FORMAT_DEFAULT) override; + +protected: + /// @copydoc PlayerCCManagerBase::Initialize + int Initialize(void *handle) override; + + /// @copydoc PlayerCCManagerBase::StartRendering + void StartRendering() override; + + /// @copydoc PlayerCCManagerBase::StopRendering + void StopRendering() override; + + /// @copydoc PlayerCCManagerBase::SetDigitalChannel + int SetDigitalChannel(unsigned int id) override { return 0; } + + /// @copydoc PlayerCCManagerBase::SetAnalogChannel + int SetAnalogChannel(unsigned int id) override { return 0; } + + /// @copydoc PlayerCCManagerBase::ResetState + void ResetState() override; + +private: + /** + * @brief Map a CC track string to the text-track-identifier expected by + * the Rialto server, mirroring PlayerRialtoCCManager::SetTrack(). + * + * If the track string starts with a digit, a prefix is prepended based + * on the format: "CC" for 608, "SERVICE" for 708. + */ + static std::string mapTrackIdentifier(const std::string &track, + CCFormat format); + + /// SetTrack() body, assuming m_controlMutex is already held. Used by + /// SetTrack() itself and by Initialize(), so handle assignment and + /// configuration happen as one atomic critical section. + int SetTrackLocked(const std::string &track, CCFormat format); + + /// Guards m_control and SetTrack()'s writes / Initialize()'s reads of + /// the inherited mTrack / mTrackFormat cache. InvalidateHandle() also + /// takes this lock, so any SetTrack() / StartRendering() / + /// StopRendering() call already in progress on the old handle completes + /// before InvalidateHandle() clears the pointer and returns - + /// preventing use-after-free when the handle owner is destroyed + /// concurrently. ResetState() clears mTrack / mTrackFormat via the base + /// class before taking this lock (see ResetState() for why). + mutable std::mutex m_controlMutex; + + /// Non-owning pointer to the AampRialtoPlayer CC control interface. + /// Null until Initialize() is called (first PLAYING state). Guarded by + /// m_controlMutex. + IDirectRialtoCC *m_control{nullptr}; + + /// Guards mId / mIdSet. + std::mutex m_idLock; + int m_id{0}; + std::set m_idSet; +}; + +#endif // PLAYER_DIRECT_RIALTO_CC_MANAGER_H diff --git a/closedcaptions/rialto/PlayerRialtoCCManager.cpp b/closedcaptions/rialto/PlayerRialtoCCManager.cpp index db48d6d9..fd72e58f 100644 --- a/closedcaptions/rialto/PlayerRialtoCCManager.cpp +++ b/closedcaptions/rialto/PlayerRialtoCCManager.cpp @@ -35,21 +35,24 @@ int PlayerRialtoCCManager::Initialize(void * handle) { MW_LOG_INFO("PlayerRialtoCCManager::Initialize(%p) called", handle); - bool changedHandle = (handle != mSubtitleControlHandle); - + // Held for the whole handle-assignment + configuration sequence so a + // concurrent Initialize()/SetTrack() call can't interleave and configure + // the wrong handle (or leave this handle unconfigured). + std::lock_guard lock(mControlMutex); + const bool changedHandle = (handle != mSubtitleControlHandle); mSubtitleControlHandle = handle; - if (GetTrack().empty()) + if (mTrack.empty()) { // Apps expect to render default CC as CC1, so set that here in case // they do not explicitly call SetTrack(). MW_LOG_INFO("PlayerRialtoCCManager::Setting default to \"CC1\""); - (void) SetTrack("CC1"); + (void) SetTrackLocked("CC1", eCLOSEDCAPTION_FORMAT_DEFAULT); } else if (changedHandle) { // Configure the new handle. - (void) SetTrack(GetTrack(), mTrackFormat); + (void) SetTrackLocked(mTrack, mTrackFormat); } return 0; @@ -73,7 +76,14 @@ int PlayerRialtoCCManager::GetId() void PlayerRialtoCCManager::ResetState() { MW_LOG_INFO("PlayerRialtoCCManager::Resetting"); - PlayerCCManagerBase::ResetState(); + // Stop() -> StopRendering() re-enters mControlMutex; call it before + // taking the lock to avoid deadlocking on this non-recursive mutex. + Stop(); + std::lock_guard lock(mControlMutex); + // Reset the cached track/rendering fields under the same lock that + // SetTrack()/Initialize() use to access them, avoiding a race with a + // concurrent call to either. + ResetTrackState(); mSubtitleControlHandle = nullptr; } @@ -105,18 +115,41 @@ void PlayerRialtoCCManager::Release(int id) return; } +/** + * @brief Clear mSubtitleControlHandle if it currently equals handle + */ +void PlayerRialtoCCManager::InvalidateHandle(void *handle) +{ + // Holding mControlMutex blocks until any SetTrack() / StartRendering() / + // StopRendering() call already in progress on the old handle has + // returned, so the caller can safely destroy the handle owner once this + // call returns. + std::lock_guard lock(mControlMutex); + if (handle != nullptr && mSubtitleControlHandle == handle) + { + mSubtitleControlHandle = nullptr; + MW_LOG_WARN("PlayerRialtoCCManager::handle:%p invalidated ahead of Release()", handle); + } +} + /** * @brief Set CC track */ int PlayerRialtoCCManager::SetTrack(const std::string &track, const CCFormat format) { - // Cache the original track string and the format so the prefix (if any) - // can be re-applied correctly from the cached values. - mTrack = track; // For PlayerCCManager::GetTrack() - mTrackFormat = format; + // mTrack/mTrackFormat are shared with Initialize() and ResetState(), so + // they must be updated under the same lock as mSubtitleControlHandle. + std::lock_guard lock(mControlMutex); + return SetTrackLocked(track, format); +} +int PlayerRialtoCCManager::SetTrackLocked(const std::string &track, const CCFormat format) +{ MW_LOG_INFO("PlayerRialtoCCManager::set track \"%s\"", track.c_str()); + mTrack = track; // For PlayerCCManager::GetTrack() + mTrackFormat = format; + if (nullptr != mSubtitleControlHandle) { // We expect 'track' to have an alphabetic prefix. If it does not, @@ -154,6 +187,7 @@ void PlayerRialtoCCManager::StartRendering() { MW_LOG_INFO("PlayerRialtoCCManager::unmuting"); + std::lock_guard lock(mControlMutex); if (nullptr != mSubtitleControlHandle) { g_object_set(mSubtitleControlHandle, "mute", FALSE, NULL); @@ -172,6 +206,7 @@ void PlayerRialtoCCManager::StopRendering() { MW_LOG_INFO("PlayerRialtoCCManager::muting"); + std::lock_guard lock(mControlMutex); if (nullptr != mSubtitleControlHandle) { g_object_set(mSubtitleControlHandle, "mute", TRUE, NULL); diff --git a/closedcaptions/rialto/PlayerRialtoCCManager.h b/closedcaptions/rialto/PlayerRialtoCCManager.h index bbbd9882..a2ec6d5f 100644 --- a/closedcaptions/rialto/PlayerRialtoCCManager.h +++ b/closedcaptions/rialto/PlayerRialtoCCManager.h @@ -48,6 +48,9 @@ class PlayerRialtoCCManager : public PlayerCCManagerBase */ void Release(int iID) override; + /// @copydoc PlayerCCManagerBase::InvalidateHandle + void InvalidateHandle(void *handle) override; + /** * @fn GetId * @return int - unique ID @@ -118,7 +121,21 @@ class PlayerRialtoCCManager : public PlayerCCManagerBase */ void ResetState() override; + /// SetTrack() body, assuming mControlMutex is already held. Used by + /// SetTrack() itself and by Initialize(), so handle assignment and + /// configuration happen as one atomic critical section. + int SetTrackLocked(const std::string &track, const CCFormat format); + private: + /// Guards mSubtitleControlHandle and SetTrack()'s writes / Initialize()'s + /// reads of the inherited mTrack / mTrackFormat cache. InvalidateHandle() + /// also takes this lock, so any SetTrack() / StartRendering() / + /// StopRendering() call already in progress on the old handle completes + /// before InvalidateHandle() clears the pointer and returns - preventing + /// use-after-free when the handle owner is destroyed concurrently. + mutable std::mutex mControlMutex; + + /// GstElement* decoder handle. Guarded by mControlMutex. void *mSubtitleControlHandle{nullptr}; std::mutex mIdLock{}; diff --git a/drm/DrmSession.cpp b/drm/DrmSession.cpp index 35d10c95..a735cb6f 100644 --- a/drm/DrmSession.cpp +++ b/drm/DrmSession.cpp @@ -119,14 +119,3 @@ int DrmSession::decrypt(const uint8_t *f_pbIV, uint32_t f_cbIV, const uint8_t *p MW_LOG_ERR("Standard decrypt method not implemented"); return -1; } - -/** - * @brief Get the list of usable key IDs from the DRM session - * @retval Reference to vector of usable key IDs - * @note Default implementation returns the reference to an empty vector - */ -const std::vector>& DrmSession::getUsableKeys() const -{ - static const std::vector> emptyVector; - return emptyVector; -} diff --git a/drm/DrmSession.h b/drm/DrmSession.h index c760c012..d96290f0 100755 --- a/drm/DrmSession.h +++ b/drm/DrmSession.h @@ -87,6 +87,29 @@ class DrmSession bool mMarkedForDestruction; public: + /** + * @fn DrmSession + * @param keySystem : DRM key system uuid + */ + DrmSession(const string &keySystem); + + /** + * @brief Copy constructor disabled + * + */ + DrmSession(const DrmSession&) = delete; + + /** + * @fn ~DrmSession + */ + virtual ~DrmSession(); + + /** + * @brief assignment operator disabled + * + */ + DrmSession& operator=(const DrmSession&) = delete; + /** * @fn AcquireForUse * @brief Must be called by any external caller (e.g. the GStreamer @@ -151,7 +174,7 @@ class DrmSession * @param caps : Caps of the media that is currently being decrypted * @retval Returns status of decrypt request. */ - virtual int decrypt(GstBuffer* keyIDBuffer, GstBuffer* ivBuffer, GstBuffer* buffer, unsigned subSampleCount, GstBuffer* subSamplesBuffer, GstCaps* caps = NULL); + virtual int decrypt(GstBuffer* keyIDBuffer, GstBuffer* ivBuffer, GstBuffer* buffer, unsigned subSampleCount, GstBuffer* subSamplesBuffer, GstCaps* caps = NULL); /** * @fn decrypt @@ -187,30 +210,18 @@ class DrmSession /** * @brief Get the list of usable key IDs from the DRM session - * @retval Reference to vector of usable key IDs - * @note Default implementation returns the reference to an empty vector + * @retval Snapshot copy of usable key IDs, taken under the session's + * internal lock where applicable. Callers receive their own + * independent copy and need not hold any external lock. */ - virtual const std::vector>& getUsableKeys() const; + virtual std::vector> getUsableKeys() const { return {}; } /** - * @fn DrmSession - * @param keySystem : DRM key system uuid - */ - DrmSession(const string &keySystem); - /** - * @brief Copy constructor disabled - * - */ - DrmSession(const DrmSession&) = delete; - /** - * @brief assignment operator disabled - * - */ - DrmSession& operator=(const DrmSession&) = delete; - /** - * @fn ~DrmSession + * @brief Return the Rialto media key session ID, or -1 if not applicable. */ - virtual ~DrmSession(); + virtual int32_t getMediaKeySessionId() const { return -1; } + + virtual void setKeyId(const std::vector& keyId) {}; /** * @fn getKeySystem @@ -224,9 +235,6 @@ class DrmSession * @retval void */ void setOutputProtection(bool bValue) { m_OutputProtectionEnabled = bValue;} -#if defined(USE_OPENCDM_ADAPTER) - virtual void setKeyId(const std::vector& keyId) {}; -#endif void setSecManagerSession(ContentSecurityManagerSession session){mContentSecurityManagerSession=session;} ContentSecurityManagerSession getSecManagerSession() const { return mContentSecurityManagerSession;} }; diff --git a/drm/DrmSessionManager.cpp b/drm/DrmSessionManager.cpp index 8c3da56b..29e62cfa 100755 --- a/drm/DrmSessionManager.cpp +++ b/drm/DrmSessionManager.cpp @@ -48,13 +48,14 @@ KeyIdEntries::KeyIdEntries() : creationTime(0), isFailedKeyEntries(false), isPri /** * @brief DrmSessionManager constructor. */ -DrmSessionManager::DrmSessionManager(int maxDrmSessions, void *player, std::function watermarkSessionUpdateCallback) : drmSessionContexts(NULL), +DrmSessionManager::DrmSessionManager(int maxDrmSessions, void *player, std::function watermarkSessionUpdateCallback, DrmSessionCreator creator) : drmSessionContexts(NULL), cachedKeyIDs(NULL), accessToken(NULL), accessTokenLen(0), sessionMgrState(SessionMgrState::eSESSIONMGR_ACTIVE), accessTokenMutex(), cachedKeyMutex() ,mEnableAccessAttributes(true) ,mDrmSessionLock() ,mMaxDRMSessions(maxDrmSessions) + ,m_sessionCreator(std::move(creator)) ,playerSecInstance(nullptr) ,mContentSecurityManagerSession() ,mIsVideoOnMute(false) @@ -527,7 +528,19 @@ DrmSession* DrmSessionManager::createDrmSession(int &responseCode, int &err, std } return nullptr; } - code =this->AcquireLicenseCb(responseCode, std::move(drmHelper), selectedSlot, cdmError, (GstMediaType)streamType, metaDataPtr, false); + + if (!AcquireLicenseCb) + { + MW_LOG_WARN("AcquireLicenseCb not registered - cannot acquire license"); + err = MW_DRM_INIT_FAILED; + std::lock_guard guard(cachedKeyMutex); + if (cachedKeyIDs) + { + cachedKeyIDs[selectedSlot].isFailedKeyEntries = true; + } + return nullptr; + } + code = AcquireLicenseCb(responseCode, std::move(drmHelper), selectedSlot, cdmError, (GstMediaType)streamType, metaDataPtr, false); if (code != KEY_READY) { MW_LOG_WARN(" Unable to get Ready Status DrmSession : Key State %d ", code); @@ -900,7 +913,15 @@ KeyState DrmSessionManager::getDrmSession(int &err, std::shared_ptr d } this->ProfileUpdateCb(); - drmSessionContexts[sessionSlot].drmSession = DrmSessionFactory::GetDrmSession(drmHelper, Instance); + if (m_sessionCreator) + { + auto owned = m_sessionCreator(drmHelper, Instance); + drmSessionContexts[sessionSlot].drmSession = owned.release(); + } + else + { + drmSessionContexts[sessionSlot].drmSession = DrmSessionFactory::GetDrmSession(drmHelper, Instance); + } if (drmSessionContexts[sessionSlot].drmSession != NULL) { MW_LOG_INFO("Created new DrmSession for DrmSystemId %s", systemId.c_str()); @@ -913,6 +934,9 @@ KeyState DrmSessionManager::getDrmSession(int &err, std::shared_ptr d drmSessionContexts[sessionSlot].drmSession->setOutputProtection(true); drmHelper->setOutputProtectionFlag(true); } + // setKeyId() is a no-op default on DrmSession, so it's safe to call + // for every backend (OpenCDM, Direct-Rialto, etc.), not just OpenCDM. + drmSessionContexts[sessionSlot].drmSession->setKeyId(keyIdArray); } else { @@ -920,10 +944,6 @@ KeyState DrmSessionManager::getDrmSession(int &err, std::shared_ptr d err = MW_DRM_INIT_FAILED ; } -#if defined(USE_OPENCDM_ADAPTER) - drmSessionContexts[sessionSlot].drmSession->setKeyId(keyIdArray); -#endif - return code; } diff --git a/drm/DrmSessionManager.h b/drm/DrmSessionManager.h index 3e8a0d31..7ff90da7 100644 --- a/drm/DrmSessionManager.h +++ b/drm/DrmSessionManager.h @@ -32,6 +32,7 @@ #include #include #include +#include #include "DrmHelper.h" #include "PlayerSecInterface.h" @@ -39,6 +40,15 @@ #include +/** + * @brief Factory callable type for creating DRM sessions. + * + * Stored per-player in DrmSessionManager to support creator injection + * (e.g. the direct-Rialto path). + */ +using DrmSessionCreator = + std::function(DrmHelperPtr, DrmCallbacks*)>; + #define VIDEO_SESSION 0 #define AUDIO_SESSION 1 @@ -176,6 +186,7 @@ class DrmSessionManager std::mutex mDrmSessionLock; bool mEnableAccessAttributes; int mMaxDRMSessions; + DrmSessionCreator m_sessionCreator; std::function mPlayerSendWatermarkSessionUpdateEventCB; /** * @brief Copy constructor disabled @@ -221,11 +232,13 @@ class DrmSessionManager /** * @fn DrmSessionManager + * @brief creator defaults to null (no injection) rather than being a + * separate overload: a distinct 3-arg symbol would let a + * prebuilt binary compiled before m_sessionCreator existed link + * successfully against this header's larger layout and silently + * heap-corrupt on construction, instead of failing to compile. */ - DrmSessionManager(int maxDrmSessions, void *player, std::function watermarkSessionUpdateCallback); - - void initializeDrmSessions(); - + DrmSessionManager(int maxDrmSessions, void *player, std::function watermarkSessionUpdateCallback, DrmSessionCreator creator = nullptr); /** * @fn watermarkSessionHandlerWrapper * @brief Wrapper function to handle session watermark. @@ -517,12 +530,12 @@ class DrmSessionManager /** * @brief Configuration parameters needed from Player */ - void UpdateDRMConfig( + void UpdateDRMConfig( bool useSecManager, bool enablePROutputProtection, bool propagateURIParam, bool isFakeTune, - bool wideVineKIDWorkaround); + bool wideVineKIDWorkaround); }; diff --git a/drm/ocdm/opencdmsessionadapter.cpp b/drm/ocdm/opencdmsessionadapter.cpp index c5d8d8ed..9753c781 100644 --- a/drm/ocdm/opencdmsessionadapter.cpp +++ b/drm/ocdm/opencdmsessionadapter.cpp @@ -451,9 +451,9 @@ bool OCDMSessionAdapter::verifyOutputProtection() /** * @fn getUsableKeys * @brief Get the list of usable key IDs from the DRM session - * @retval Reference to vector of usable key IDs + * @retval Snapshot copy of usable key IDs, taken under m_usableKeysMutex */ -const std::vector>& OCDMSessionAdapter::getUsableKeys() const +std::vector> OCDMSessionAdapter::getUsableKeys() const { std::lock_guard lock(m_usableKeysMutex); return m_usableKeys; diff --git a/drm/ocdm/opencdmsessionadapter.h b/drm/ocdm/opencdmsessionadapter.h index 36dd5649..25513ff2 100644 --- a/drm/ocdm/opencdmsessionadapter.h +++ b/drm/ocdm/opencdmsessionadapter.h @@ -125,7 +125,7 @@ class OCDMSessionAdapter : public DrmSession void processOCDMChallenge(const char destUrl[], const uint8_t challenge[], const uint16_t challengeSize); void keysUpdatedOCDM(); void keyUpdateOCDM(const uint8_t key[], const uint8_t keySize); - const std::vector>& getUsableKeys() const override; + std::vector> getUsableKeys() const override; long long timeBeforeCallback; private: diff --git a/test/pi-cli/commandProcessing.cpp b/test/pi-cli/commandProcessing.cpp index b47cd9b3..32ee3344 100644 --- a/test/pi-cli/commandProcessing.cpp +++ b/test/pi-cli/commandProcessing.cpp @@ -206,7 +206,10 @@ void configurePipelineCommand(InterfacePlayerRDK& player, const std::vector(format)), + MediaCodecInfo(static_cast(audioFormat)), + MediaCodecInfo(static_cast(subFormat))}, bESChangeStatus, setReadyAfterPipelineCreation, isSubEnable, trackId, rate, pipelineName, PipelinePriority, subBool, url ); diff --git a/test/utests/fakes/CMakeLists.txt b/test/utests/fakes/CMakeLists.txt index 2a6d819c..74a81cbc 100644 --- a/test/utests/fakes/CMakeLists.txt +++ b/test/utests/fakes/CMakeLists.txt @@ -35,6 +35,7 @@ include_directories(${UTESTS_ROOT}/drm/ocdm) include_directories(${UTESTS_ROOT}/drm/mocks) include_directories(${UTESTS_ROOT}/mocks) include_directories(${UTESTS_ROOT}/ocdm) +include_directories(${UTESTS_ROOT}/rialto) include_directories(${PLAYER_ROOT}/drm) include_directories(${PLAYER_ROOT}/drm/ocdm) include_directories(${PLAYER_ROOT}/baseConversion) diff --git a/test/utests/fakes/FakeDRMSessionManager.cpp b/test/utests/fakes/FakeDRMSessionManager.cpp index 289cb852..a778e4f0 100644 --- a/test/utests/fakes/FakeDRMSessionManager.cpp +++ b/test/utests/fakes/FakeDRMSessionManager.cpp @@ -22,7 +22,7 @@ #include "MockDrmSessionManager.h" MockDRMSessionManager *g_mockDRMSessionManager = nullptr; -DrmSessionManager::DrmSessionManager(int maxDrmSessions, void *player, std::function watermarkSessionUpdateCallback) +DrmSessionManager::DrmSessionManager(int maxDrmSessions, void *player, std::function watermarkSessionUpdateCallback, DrmSessionCreator creator) { } diff --git a/test/utests/fakes/Fakeopencdmsessionadapter.cpp b/test/utests/fakes/Fakeopencdmsessionadapter.cpp index 596450ca..e9875f65 100644 --- a/test/utests/fakes/Fakeopencdmsessionadapter.cpp +++ b/test/utests/fakes/Fakeopencdmsessionadapter.cpp @@ -24,7 +24,6 @@ MockOpenCdmSessionAdapter *g_mockOpenCdmSessionAdapter = nullptr; std::vector g_mockKeyId{1,2,3,4,5,6,7,8,9,0,1,2,3,4}; -const std::vector> g_emptyUsableKeys{}; OCDMSessionAdapter::OCDMSessionAdapter(std::shared_ptr drmHelper, DrmCallbacks *callbacks) : DrmSession("ocdmkeysystem"), m_keyId{g_mockKeyId}, m_drmHelper{drmHelper} @@ -79,15 +78,14 @@ bool OCDMSessionAdapter::waitForState(KeyState state, const uint32_t timeout) /** * @brief Get the list of usable key IDs from the DRM session - * @retval Reference to vector of usable key IDs - * @note Default implementation returns the reference to an empty vector + * @retval Snapshot copy of usable key IDs */ -const std::vector>& OCDMSessionAdapter::getUsableKeys() const +std::vector> OCDMSessionAdapter::getUsableKeys() const { if (g_mockOpenCdmSessionAdapter) { return g_mockOpenCdmSessionAdapter->getUsableKeys(); } - return g_emptyUsableKeys; + return {}; } #if defined(USE_OPENCDM_ADAPTER) diff --git a/test/utests/mocks/MockDrmSession.h b/test/utests/mocks/MockDrmSession.h index f10c7e4c..fb384c50 100644 --- a/test/utests/mocks/MockDrmSession.h +++ b/test/utests/mocks/MockDrmSession.h @@ -43,9 +43,7 @@ class MockDrmSession : public DrmSession MOCK_METHOD(int, decrypt, (const uint8_t *f_pbIV, uint32_t f_cbIV, const uint8_t *payloadData, uint32_t payloadDataSize, uint8_t **ppOpaqueData), (override)); MOCK_METHOD(KeyState, getState, (), (override)); MOCK_METHOD(void, clearDecryptContext, (), (override)); -#if defined(USE_OPENCDM_ADAPTER) MOCK_METHOD(void, setKeyId, (const std::vector& keyId), (override)); -#endif }; extern MockDrmSession *g_mockDrmSession; diff --git a/test/utests/mocks/MockOpenCdmSessionAdapter.h b/test/utests/mocks/MockOpenCdmSessionAdapter.h index 372d3e94..f5aec80d 100644 --- a/test/utests/mocks/MockOpenCdmSessionAdapter.h +++ b/test/utests/mocks/MockOpenCdmSessionAdapter.h @@ -30,7 +30,7 @@ class MockOpenCdmSessionAdapter MOCK_METHOD(bool, verifyOutputProtection, ()); MOCK_METHOD(void, setKeyId, (const std::vector&)); - MOCK_METHOD(const std::vector>&, getUsableKeys, (), (const)); + MOCK_METHOD(std::vector>, getUsableKeys, (), (const)); MOCK_METHOD(void, generateDRMSession, (const uint8_t*, uint32_t, std::string&)); MOCK_METHOD(KeyState, getState, ()); }; diff --git a/test/utests/tests/CMakeLists.txt b/test/utests/tests/CMakeLists.txt index 96b98cd8..8d6c0ab9 100644 --- a/test/utests/tests/CMakeLists.txt +++ b/test/utests/tests/CMakeLists.txt @@ -46,3 +46,4 @@ add_subdirectory(FireBoltTests) add_subdirectory(DrmOcdmTests) add_subdirectory(DrmHelperTests) add_subdirectory(DrmAes) +add_subdirectory(PlayerDirectRialtoCCManagerTests) diff --git a/test/utests/tests/ClosedCaptionsTests/CMakeLists.txt b/test/utests/tests/ClosedCaptionsTests/CMakeLists.txt index 296294f6..e7b23601 100644 --- a/test/utests/tests/ClosedCaptionsTests/CMakeLists.txt +++ b/test/utests/tests/ClosedCaptionsTests/CMakeLists.txt @@ -30,6 +30,7 @@ include_directories(${PLAYER_ROOT}/playerJsonObject) include_directories(${PLAYER_ROOT}/playerLogManager) include_directories(${PLAYER_ROOT}/mp4demux) include_directories(${PLAYER_ROOT}/closedcaptions/rialto) +include_directories(${PLAYER_ROOT}/closedcaptions/direct-rialto) include_directories(${GTEST_INCLUDE_DIRS}) include_directories(${GMOCK_INCLUDE_DIRS}) include_directories(${GLIB_INCLUDE_DIRS}) diff --git a/test/utests/tests/DrmSessionManagerTests/DrmSessionManagerTestCases.cpp b/test/utests/tests/DrmSessionManagerTests/DrmSessionManagerTestCases.cpp index 6c0b9c47..f5ff98ec 100644 --- a/test/utests/tests/DrmSessionManagerTests/DrmSessionManagerTestCases.cpp +++ b/test/utests/tests/DrmSessionManagerTests/DrmSessionManagerTestCases.cpp @@ -28,6 +28,7 @@ #include "MockOpenCdmSessionAdapter.h" #include "DrmCallbacks.h" #include "MockDrmSessionFactory.h" +#include "MockDrmSession.h" #include "DrmHelper.h" #include "WidevineDrmHelper.h" #include "DrmInfo.h" @@ -55,6 +56,11 @@ class TestableDrmSessionManager : public DrmSessionManager { } + TestableDrmSessionManager(int maxDrmSessions, void *player, std::function watermarkCallback, DrmSessionCreator creator) + : DrmSessionManager(maxDrmSessions, player, watermarkCallback, std::move(creator)), mMaxSessions(maxDrmSessions) + { + } + // Provide controlled access to cachedKeyIDs with mutex protection void AddKeyIdEntryToCache(int slot, const KeyIdEntries& entries) { @@ -728,7 +734,7 @@ TEST_F(DrmSessionManagerComplexTests, ValidateMultiKeySlot_RealWidevinePssh_Thre usableKeys.push_back(RawKeyToKeyId(binaryKey3.data(), binaryKey3.size())); EXPECT_CALL(*g_mockOpenCdmSessionAdapter, getUsableKeys()) - .WillRepeatedly(ReturnRef(usableKeys)); + .WillRepeatedly(Return(usableKeys)); // Validate with first key (should match after dash normalization) bool result = mDrmSessionManager->ValidateMultiKeySlot(keyIDs[0], 0); @@ -824,7 +830,7 @@ TEST_F(DrmSessionManagerComplexTests, ValidateMultiKeySlot_CreateDrmHelperFromIn usableKeys.push_back(expectedKeyIdAscii); EXPECT_CALL(*g_mockOpenCdmSessionAdapter, getUsableKeys()) - .WillRepeatedly(ReturnRef(usableKeys)); + .WillRepeatedly(Return(usableKeys)); // Validate slot bool result = mDrmSessionManager->ValidateMultiKeySlot(keyIDs[0], 0); @@ -982,7 +988,7 @@ TEST_F(DrmSessionManagerComplexTests, CreateDrmHelperFromInitData_MultipleKeys_C usableKeys.push_back(RawKeyToKeyId(expectedSdKey.data(), expectedSdKey.size())); EXPECT_CALL(*g_mockOpenCdmSessionAdapter, getUsableKeys()) - .WillRepeatedly(ReturnRef(usableKeys)); + .WillRepeatedly(Return(usableKeys)); // Validate with HD key (second key) - should succeed bool result = mDrmSessionManager->ValidateMultiKeySlot(keyIDs[1], 0); @@ -1060,7 +1066,7 @@ TEST_F(DrmSessionManagerComplexTests, InitDataFlow_EndToEnd_CreateHelperAndValid usableKeys.push_back(RawKeyToKeyId(keyBinary.data(), keyBinary.size())); EXPECT_CALL(*g_mockOpenCdmSessionAdapter, getUsableKeys()) - .WillRepeatedly(ReturnRef(usableKeys)); + .WillRepeatedly(Return(usableKeys)); // Step 9: Validate slot bool validationResult = mDrmSessionManager->ValidateMultiKeySlot(keyIDs[0], 0); @@ -1116,7 +1122,7 @@ TEST_F(DrmSessionManagerComplexTests, ValidateMultiKeySlot_RealWidevinePssh_Thre usableKeys.push_back(RawKeyToKeyId(binaryKey.data(), binaryKey.size())); EXPECT_CALL(*g_mockOpenCdmSessionAdapter, getUsableKeys()) - .WillRepeatedly(ReturnRef(usableKeys)); + .WillRepeatedly(Return(usableKeys)); // Validate bool result = mDrmSessionManager->ValidateMultiKeySlot(keyIDs[0], 0); @@ -1178,7 +1184,7 @@ TEST_F(DrmSessionManagerComplexTests, ValidateMultiKeySlot_RealWidevinePssh_Sing usableKeys.push_back(RawKeyToKeyId(binaryKey.data(), binaryKey.size())); EXPECT_CALL(*g_mockOpenCdmSessionAdapter, getUsableKeys()) - .WillRepeatedly(ReturnRef(usableKeys)); + .WillRepeatedly(Return(usableKeys)); // Validate bool result = mDrmSessionManager->ValidateMultiKeySlot(keyIDs[0], 0); @@ -1234,7 +1240,7 @@ TEST_F(DrmSessionManagerComplexTests, ValidateMultiKeySlot_RealWidevinePssh_Part usableKeys.push_back(RawKeyToKeyId(binaryKey3.data(), binaryKey3.size())); EXPECT_CALL(*g_mockOpenCdmSessionAdapter, getUsableKeys()) - .WillRepeatedly(ReturnRef(usableKeys)); + .WillRepeatedly(Return(usableKeys)); // Validate with second key (should succeed as it's in usableKeys) bool result = mDrmSessionManager->ValidateMultiKeySlot(keyIDs[1], 0); @@ -1295,3 +1301,77 @@ TEST_F(DrmSessionManagerComplexTests, OpencdmConstructionFailureTest) EXPECT_EQ(err, MW_DRM_SESSION_CREATE_FAILED); } +/** + * @brief getDrmSession() must invoke an injected DrmSessionCreator (instead + * of DrmSessionFactory::GetDrmSession()) and transfer ownership of the + * returned unique_ptr into the selected session slot. + */ +TEST_F(DrmSessionManagerComplexTests, DrmSessionCreator_InvokedAndOwnershipTransferred) +{ + bool creatorInvoked = false; + DrmHelperPtr capturedHelper; + auto *mockSession = new NiceMock("com.widevine.alpha"); + ON_CALL(*mockSession, getState()).WillByDefault(Return(KEY_INIT)); + + DrmSessionCreator creator = + [&](DrmHelperPtr drmHelper, DrmCallbacks *instance) -> std::unique_ptr + { + creatorInvoked = true; + capturedHelper = drmHelper; + return std::unique_ptr(mockSession); + }; + + TestableDrmSessionManager manager(maxDrmSessions, nullptr, nullptr, creator); + // getDrmSession() unconditionally invokes ProfileUpdateCb; it must be + // registered first or the call throws std::bad_function_call. + manager.RegisterProfilingUpdateCb([]() {}); + + auto mockHelper = CreateMockDrmHelper("com.widevine.alpha"); + ON_CALL(*mockHelper, getKey(_)) + .WillByDefault(Invoke([](std::vector& keyID) { keyID = {0x01, 0x02, 0x03, 0x04}; })); + + int err = 0; + int selectedSlot = -1; + KeyState result = manager.getDrmSession(err, mockHelper, selectedSlot, nullptr); + + EXPECT_TRUE(creatorInvoked); + EXPECT_EQ(capturedHelper, mockHelper); + ASSERT_GE(selectedSlot, 0); + EXPECT_EQ(manager.drmSessionContexts[selectedSlot].drmSession, mockSession); + EXPECT_EQ(result, KEY_INIT); + EXPECT_EQ(err, 0); +} + +/** + * @brief getDrmSession() must report MW_DRM_INIT_FAILED and leave the slot's + * drmSession null when an injected DrmSessionCreator fails (returns nullptr). + */ +TEST_F(DrmSessionManagerComplexTests, DrmSessionCreator_ReturnsNull_ReportsInitFailure) +{ + bool creatorInvoked = false; + DrmSessionCreator creator = + [&](DrmHelperPtr, DrmCallbacks *) -> std::unique_ptr + { + creatorInvoked = true; + return nullptr; + }; + + TestableDrmSessionManager manager(maxDrmSessions, nullptr, nullptr, creator); + // getDrmSession() unconditionally invokes ProfileUpdateCb; it must be + // registered first or the call throws std::bad_function_call. + manager.RegisterProfilingUpdateCb([]() {}); + + auto mockHelper = CreateMockDrmHelper("com.widevine.alpha"); + ON_CALL(*mockHelper, getKey(_)) + .WillByDefault(Invoke([](std::vector& keyID) { keyID = {0x01, 0x02, 0x03, 0x04}; })); + + int err = 0; + int selectedSlot = -1; + manager.getDrmSession(err, mockHelper, selectedSlot, nullptr); + + EXPECT_TRUE(creatorInvoked); + ASSERT_GE(selectedSlot, 0); + EXPECT_EQ(manager.drmSessionContexts[selectedSlot].drmSession, nullptr); + EXPECT_EQ(err, MW_DRM_INIT_FAILED); +} + diff --git a/test/utests/tests/GstPlayer/FunctionalTests.cpp b/test/utests/tests/GstPlayer/FunctionalTests.cpp index 538ad5c6..5e5a07b8 100644 --- a/test/utests/tests/GstPlayer/FunctionalTests.cpp +++ b/test/utests/tests/GstPlayer/FunctionalTests.cpp @@ -253,9 +253,9 @@ class GstPlayerTests : public ::testing::Test EXPECT_CALL(*g_mockGStreamer, gst_element_set_state(&gst_element_pipeline, GST_STATE_PLAYING)) .WillOnce(Return(GST_STATE_CHANGE_SUCCESS)); - mInterfaceGstPlayer->ConfigurePipeline(GST_FORMAT_VIDEO_ES_H264, + mInterfaceGstPlayer->ConfigurePipeline(StreamCodecInfo{GST_FORMAT_VIDEO_ES_H264, GST_FORMAT_AUDIO_ES_AAC, - GST_FORMAT_SUBTITLE_TTML, + GST_FORMAT_SUBTITLE_TTML}, setup->bESChangeStatus, setup->setReadyAfterPipelineCreation, true, 0, GST_NORMAL_PLAY_RATE, "testPipeline", 0, false, "testManifest", false); diff --git a/test/utests/tests/GstPlayer/PauseOnPlaybackTests.cpp b/test/utests/tests/GstPlayer/PauseOnPlaybackTests.cpp index 48f01d9f..5848507e 100644 --- a/test/utests/tests/GstPlayer/PauseOnPlaybackTests.cpp +++ b/test/utests/tests/GstPlayer/PauseOnPlaybackTests.cpp @@ -31,14 +31,11 @@ using ::testing::NiceMock; using ::testing::Return; using ::testing::StrEq; -using ::testing::Eq; using ::testing::_; -using ::testing::Address; using ::testing::DoAll; using ::testing::SetArgPointee; using ::testing::NotNull; using ::testing::SaveArgPointee; -using ::testing::SaveArg; using ::testing::Pointer; class PauseOnPlaybackTests : public ::testing::Test @@ -141,7 +138,7 @@ TEST_F(PauseOnPlaybackTests, EnteredPausedSteHandler_ConfigurePauseOnPlayback) // Enable PauseOnPlayback mInterfaceGstPlayer->SetPauseOnStartPlayback(true); - mInterfaceGstPlayer->ConfigurePipeline(GST_FORMAT_VIDEO_ES_H264, GST_FORMAT_AUDIO_ES_AAC, GST_FORMAT_SUBTITLE_WEBVTT, false, false, false, 0, GST_NORMAL_PLAY_RATE, "testPipeline", 0, false, "testManifest", false); + mInterfaceGstPlayer->ConfigurePipeline(StreamCodecInfo{GST_FORMAT_VIDEO_ES_H264, GST_FORMAT_AUDIO_ES_AAC, GST_FORMAT_SUBTITLE_WEBVTT}, false, false, false, 0, GST_NORMAL_PLAY_RATE, "testPipeline", 0, false, "testManifest", false); } // Test configuration of pipeline with PauseOnPlayback not enabled @@ -176,7 +173,7 @@ TEST_F(PauseOnPlaybackTests, EnteredPausedSteHandler_ConfigureNormalPlayback) EXPECT_CALL(*g_mockGStreamer, gst_element_set_state(&gst_element_pipeline, GST_STATE_PLAYING)) .WillOnce(Return(GST_STATE_CHANGE_SUCCESS)); - mInterfaceGstPlayer->ConfigurePipeline(GST_FORMAT_VIDEO_ES_H264, GST_FORMAT_AUDIO_ES_AAC, GST_FORMAT_SUBTITLE_WEBVTT, false, false, false, 0, GST_NORMAL_PLAY_RATE, "testPipeline", 0, false, "testManifest", false); + mInterfaceGstPlayer->ConfigurePipeline(StreamCodecInfo{GST_FORMAT_VIDEO_ES_H264, GST_FORMAT_AUDIO_ES_AAC, GST_FORMAT_SUBTITLE_WEBVTT}, false, false, false, 0, GST_NORMAL_PLAY_RATE, "testPipeline", 0, false, "testManifest", false); } // Test bus_message callback when PauseOnPlayback has been enabled, and sink @@ -233,7 +230,7 @@ TEST_F(PauseOnPlaybackTests, bus_messsage_FrameStepPropertyAvailable) mInterfaceGstPlayer->SetPauseOnStartPlayback(true); - mInterfaceGstPlayer->ConfigurePipeline(GST_FORMAT_VIDEO_ES_H264, GST_FORMAT_AUDIO_ES_AAC, GST_FORMAT_SUBTITLE_WEBVTT, false, false, false, 0, GST_NORMAL_PLAY_RATE, "testPipeline", 0, false, "testManifest", false); + mInterfaceGstPlayer->ConfigurePipeline(StreamCodecInfo{GST_FORMAT_VIDEO_ES_H264, GST_FORMAT_AUDIO_ES_AAC, GST_FORMAT_SUBTITLE_WEBVTT}, false, false, false, 0, GST_NORMAL_PLAY_RATE, "testPipeline", 0, false, "testManifest", false); ASSERT_TRUE(bus_sync_func != nullptr); ASSERT_TRUE(bus_message_func != nullptr); @@ -333,7 +330,7 @@ TEST_F(PauseOnPlaybackTests, bus_message_FrameStepPropertyNotAvailable) mInterfaceGstPlayer->SetPauseOnStartPlayback(true); - mInterfaceGstPlayer->ConfigurePipeline(GST_FORMAT_VIDEO_ES_H264, GST_FORMAT_AUDIO_ES_AAC, GST_FORMAT_SUBTITLE_WEBVTT, false, false, false, 0, GST_NORMAL_PLAY_RATE, "testPipeline", 0, false, "testManifest", false); + mInterfaceGstPlayer->ConfigurePipeline(StreamCodecInfo{GST_FORMAT_VIDEO_ES_H264, GST_FORMAT_AUDIO_ES_AAC, GST_FORMAT_SUBTITLE_WEBVTT}, false, false, false, 0, GST_NORMAL_PLAY_RATE, "testPipeline", 0, false, "testManifest", false); ASSERT_TRUE(bus_sync_func != nullptr); ASSERT_TRUE(bus_message_func != nullptr); diff --git a/test/utests/tests/InterfacePlayerTests/InterfacePlayerFunctionTests.cpp b/test/utests/tests/InterfacePlayerTests/InterfacePlayerFunctionTests.cpp index 40a6fb29..db362abc 100644 --- a/test/utests/tests/InterfacePlayerTests/InterfacePlayerFunctionTests.cpp +++ b/test/utests/tests/InterfacePlayerTests/InterfacePlayerFunctionTests.cpp @@ -129,7 +129,7 @@ TEST_F(InterfacePlayerTests, ConfigurePipeline_WithWesterosAndRealtoSink) mPlayerConfigParams->useRialtoSink = true; EXPECT_EQ(mPlayerContext->usingRialtoSink, false); - mInterfaceGstPlayer->ConfigurePipeline(GST_FORMAT_INVALID, GST_FORMAT_INVALID, GST_FORMAT_INVALID, false, false, false, 0, GST_NORMAL_PLAY_RATE, "testPipeline", 0, false, "testManifest", false); + mInterfaceGstPlayer->ConfigurePipeline(StreamCodecInfo{GST_FORMAT_INVALID, GST_FORMAT_INVALID, GST_FORMAT_INVALID}, false, false, false, 0, GST_NORMAL_PLAY_RATE, "testPipeline", 0, false, "testManifest", false); EXPECT_EQ(mPlayerContext->using_westerossink, true); EXPECT_EQ(mPlayerContext->usingRialtoSink, true); @@ -138,18 +138,60 @@ TEST_F(InterfacePlayerTests, ConfigurePipeline_WithWesterosAndRealtoSink) TEST_F(InterfacePlayerTests, ConfigurePipeline_WithSubtitlesEnabled) { g_mockGStreamer = nullptr; - mInterfaceGstPlayer->ConfigurePipeline(GST_FORMAT_INVALID, GST_FORMAT_INVALID, GST_FORMAT_INVALID, false, false, true, 0, GST_NORMAL_PLAY_RATE, "testPipeline", 0, false, "testManifest", false); + mInterfaceGstPlayer->ConfigurePipeline(StreamCodecInfo{GST_FORMAT_INVALID, GST_FORMAT_INVALID, GST_FORMAT_INVALID}, false, false, true, 0, GST_NORMAL_PLAY_RATE, "testPipeline", 0, false, "testManifest", false); EXPECT_EQ(mPlayerContext->stream[eGST_MEDIATYPE_SUBTITLE].format, GST_FORMAT_INVALID); } +TEST_F(InterfacePlayerTests, ConfigurePipeline_WithEncryptedCodecInfo) +{ + delete g_mockGStreamer; + g_mockGStreamer = nullptr; + MediaCodecInfo videoCodecInfo(GST_FORMAT_VIDEO_ES_H264); + videoCodecInfo.mIsEncrypted = true; + + mInterfaceGstPlayer->ConfigurePipeline(StreamCodecInfo{std::move(videoCodecInfo), GST_FORMAT_INVALID, GST_FORMAT_INVALID}, false, false, false, 0, GST_NORMAL_PLAY_RATE, "testPipeline", 0, false, "testManifest", false); + + EXPECT_TRUE(mPlayerContext->stream[eGST_MEDIATYPE_VIDEO].codecInfo.mIsEncrypted); +} + +// Validate that an encryption-only change does not configure or count a track when its format is invalid. +TEST_F(InterfacePlayerTests, ConfigurePipeline_IgnoresEncryptionChangeForInvalidFormat) +{ + delete g_mockGStreamer; + g_mockGStreamer = nullptr; + mPlayerContext->NumberOfTracks = 0; + MediaCodecInfo videoCodecInfo(GST_FORMAT_INVALID); + videoCodecInfo.mIsEncrypted = true; + + mInterfaceGstPlayer->ConfigurePipeline(StreamCodecInfo{std::move(videoCodecInfo), GST_FORMAT_INVALID, GST_FORMAT_INVALID}, false, false, false, 0, GST_NORMAL_PLAY_RATE, "testPipeline", 0, false, "testManifest", false); + + EXPECT_EQ(mPlayerContext->NumberOfTracks, 0); + EXPECT_FALSE(mPlayerContext->stream[eGST_MEDIATYPE_VIDEO].codecInfo.mIsEncrypted); +} + +// A manifest may not reveal the video format (muxed HLS/TS, or audio remapped to the video slot). +// The track must still be set up on first configuration so the source exists and injection can start; +// the real caps arrive later from the demuxer via SetStreamCaps(). +TEST_F(InterfacePlayerTests, ConfigurePipeline_ConfiguresTrackWithUnknownFormatOnInitialSetup) +{ + g_mockGStreamer = nullptr; + mPlayerContext->NumberOfTracks = 0; + mPlayerContext->rate = 1.0; + + mInterfaceGstPlayer->ConfigurePipeline(StreamCodecInfo{GST_FORMAT_UNKNOWN, GST_FORMAT_AUDIO_ES_AAC, GST_FORMAT_INVALID}, false, false, false, 0, GST_NORMAL_PLAY_RATE, "testPipeline", 0, false, "testManifest", false); + + EXPECT_EQ(mPlayerContext->NumberOfTracks, 2); + EXPECT_EQ(mPlayerContext->stream[eGST_MEDIATYPE_VIDEO].format, GST_FORMAT_UNKNOWN); +} + TEST_F(InterfacePlayerTests, ConfigurePipeline_WithBufferingEnabled) { g_mockGStreamer = nullptr; mPlayerContext->buffering_enabled = true; mPlayerContext->rate = GST_NORMAL_PLAY_RATE; - mInterfaceGstPlayer->ConfigurePipeline(GST_FORMAT_MPEGTS, GST_FORMAT_INVALID, GST_FORMAT_INVALID, false, false, false, 0, GST_NORMAL_PLAY_RATE, "testPipeline", 0, false, "testManifest", false); + mInterfaceGstPlayer->ConfigurePipeline(StreamCodecInfo{GST_FORMAT_MPEGTS, GST_FORMAT_INVALID, GST_FORMAT_INVALID}, false, false, false, 0, GST_NORMAL_PLAY_RATE, "testPipeline", 0, false, "testManifest", false); EXPECT_EQ(mPlayerContext->buffering_in_progress, true); EXPECT_EQ(mPlayerContext->buffering_target_state, GST_STATE_PLAYING); @@ -164,7 +206,7 @@ TEST_F(InterfacePlayerTests, ConfigurePipeline_StreamConfiguration) EXPECT_EQ(mPlayerContext->NumberOfTracks, 0); - mInterfaceGstPlayer->ConfigurePipeline(GST_FORMAT_ISO_BMFF, GST_FORMAT_AUDIO_ES_AC3, GST_FORMAT_SUBTITLE_MP4, false, false, false, 0, GST_NORMAL_PLAY_RATE, "testPipeline", 0, false, "testManifest", false); + mInterfaceGstPlayer->ConfigurePipeline(StreamCodecInfo{GST_FORMAT_ISO_BMFF, GST_FORMAT_AUDIO_ES_AC3, GST_FORMAT_SUBTITLE_MP4}, false, false, false, 0, GST_NORMAL_PLAY_RATE, "testPipeline", 0, false, "testManifest", false); EXPECT_EQ(mPlayerContext->NumberOfTracks, 2); EXPECT_EQ(cbResponse, 5); //callback was called @@ -179,7 +221,7 @@ TEST_F(InterfacePlayerTests, ConfigurePipeline_ESChange) EXPECT_EQ(mPlayerContext->NumberOfTracks, 0); - mInterfaceGstPlayer->ConfigurePipeline(GST_FORMAT_ISO_BMFF, GST_FORMAT_AUDIO_ES_AC3, GST_FORMAT_SUBTITLE_MP4, true, false, false, 0, GST_NORMAL_PLAY_RATE, "testPipeline", 0, false, "testManifest", false); + mInterfaceGstPlayer->ConfigurePipeline(StreamCodecInfo{GST_FORMAT_ISO_BMFF, GST_FORMAT_AUDIO_ES_AC3, GST_FORMAT_SUBTITLE_MP4}, true, false, false, 0, GST_NORMAL_PLAY_RATE, "testPipeline", 0, false, "testManifest", false); EXPECT_EQ(mPlayerContext->NumberOfTracks, 1); EXPECT_EQ(cbResponse, 5); @@ -316,6 +358,9 @@ TEST_F(InterfacePlayerTests, TearDownStreamTest_successvideo) gst_media_stream* stream = &mPlayerContext->stream[eGST_MEDIATYPE_VIDEO]; stream->format = GST_FORMAT_VIDEO_ES_H264; + stream->codecInfo.mCodecFormat = GST_FORMAT_VIDEO_ES_H264; + stream->codecInfo.mCodecData = {0x01, 0x02}; + stream->codecInfo.mIsEncrypted = true; stream->eosReached = true; stream->bufferUnderrun = true; @@ -332,6 +377,9 @@ TEST_F(InterfacePlayerTests, TearDownStreamTest_successvideo) mInterfaceGstPlayer->TearDownStream(eGST_MEDIATYPE_VIDEO); EXPECT_EQ(stream->format, GST_FORMAT_INVALID); + EXPECT_EQ(stream->codecInfo.mCodecFormat, GST_FORMAT_INVALID); + EXPECT_TRUE(stream->codecInfo.mCodecData.empty()); + EXPECT_FALSE(stream->codecInfo.mIsEncrypted); EXPECT_EQ(stream->bufferUnderrun, false); EXPECT_EQ(stream->eosReached, false); EXPECT_EQ(mPlayerContext->buffering_in_progress, false); @@ -369,7 +417,11 @@ TEST_F(InterfacePlayerTests, TearDownStreamTest_misc) mInterfaceGstPlayer->TearDownStream(eGST_MEDIATYPE_AUDIO); stream = &mPlayerContext->stream[eGST_MEDIATYPE_SUBTITLE]; stream->format = GST_FORMAT_VIDEO_ES_H264; //dummy value + stream->codecInfo.mCodecFormat = GST_FORMAT_VIDEO_ES_H264; + stream->codecInfo.mIsEncrypted = true; mInterfaceGstPlayer->TearDownStream(eGST_MEDIATYPE_SUBTITLE); + EXPECT_EQ(stream->codecInfo.mCodecFormat, GST_FORMAT_INVALID); + EXPECT_FALSE(stream->codecInfo.mIsEncrypted); } TEST_F(InterfacePlayerTests, GstStopTestTrue) @@ -797,6 +849,105 @@ TEST_F(InterfacePlayerTests, InitializeSourceForPlayer_Video) delete g_mockGstUtils; } +// Validate that encrypted elementary stream caps include the original media type and configured DRM system. +TEST_F(InterfacePlayerTests, InitializeSourceForPlayer_EncryptedElementaryStreamCaps) +{ + g_mockGstUtils = new StrictMock(); + + void* playerInstance = mInterfaceGstPlayer; + void* source = reinterpret_cast(0x1234); + GstMediaType mediaType = eGST_MEDIATYPE_VIDEO; + GstCaps caps = {}; + GstStructure structure = {}; + gst_media_stream* stream = &mPlayerContext->stream[mediaType]; + stream->format = GST_FORMAT_VIDEO_ES_H264; + stream->codecInfo = MediaCodecInfo(stream->format); + stream->codecInfo.mIsEncrypted = true; + mPlayerConfigParams->videoBufBytes = 500; + mInterfaceGstPlayer->SetPreferredDRM("urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"); + + EXPECT_CALL(*g_mockGstUtils, GetCaps(GST_FORMAT_VIDEO_ES_H264)).WillOnce(Return(&caps)); + EXPECT_CALL(*g_mockGStreamer, gst_caps_get_structure(&caps, 0)).WillOnce(Return(&structure)); + EXPECT_CALL(*g_mockGStreamer, gst_structure_set(&structure, StrEq("original-media-type"))).Times(1); + EXPECT_CALL(*g_mockGStreamer, gst_structure_set(&structure, StrEq("protection-system"))).Times(1); + + mInterfaceGstPlayer->InitializeSourceForPlayer(playerInstance, source, mediaType); + + delete g_mockGstUtils; + g_mockGstUtils = nullptr; +} + +TEST_F(InterfacePlayerTests, InitializeSourceForPlayer_EncryptedContainerCapsRemainUnchanged) +{ + g_mockGstUtils = new StrictMock(); + + void* playerInstance = mInterfaceGstPlayer; + void* source = reinterpret_cast(0x1234); + GstMediaType mediaType = eGST_MEDIATYPE_VIDEO; + GstCaps caps = {}; + gst_media_stream* stream = &mPlayerContext->stream[mediaType]; + stream->format = GST_FORMAT_MPEGTS; + stream->codecInfo = MediaCodecInfo(stream->format); + stream->codecInfo.mIsEncrypted = true; + mPlayerConfigParams->videoBufBytes = 500; + mInterfaceGstPlayer->SetPreferredDRM("urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"); + + EXPECT_CALL(*g_mockGstUtils, GetCaps(GST_FORMAT_MPEGTS)).WillOnce(Return(&caps)); + EXPECT_CALL(*g_mockGStreamer, gst_caps_get_structure(_, _)).Times(0); + + mInterfaceGstPlayer->InitializeSourceForPlayer(playerInstance, source, mediaType); + + delete g_mockGstUtils; + g_mockGstUtils = nullptr; +} + +// Validate that encrypted elementary stream caps remain unchanged when no DRM system is configured. +TEST_F(InterfacePlayerTests, InitializeSourceForPlayer_EncryptedElementaryStreamCapsWithoutDrmSystemRemainUnchanged) +{ + g_mockGstUtils = new StrictMock(); + + void* playerInstance = mInterfaceGstPlayer; + void* source = reinterpret_cast(0x1234); + GstMediaType mediaType = eGST_MEDIATYPE_VIDEO; + GstCaps caps = {}; + gst_media_stream* stream = &mPlayerContext->stream[mediaType]; + stream->format = GST_FORMAT_VIDEO_ES_H264; + stream->codecInfo = MediaCodecInfo(stream->format); + stream->codecInfo.mIsEncrypted = true; + mPlayerConfigParams->videoBufBytes = 500; + + EXPECT_CALL(*g_mockGstUtils, GetCaps(GST_FORMAT_VIDEO_ES_H264)).WillOnce(Return(&caps)); + EXPECT_CALL(*g_mockGStreamer, gst_caps_get_structure(_, _)).Times(0); + + mInterfaceGstPlayer->InitializeSourceForPlayer(playerInstance, source, mediaType); + + delete g_mockGstUtils; + g_mockGstUtils = nullptr; +} + +TEST_F(InterfacePlayerTests, InitializeSourceForPlayer_ClearElementaryStreamCaps) +{ + g_mockGstUtils = new StrictMock(); + + void* playerInstance = mInterfaceGstPlayer; + void* source = reinterpret_cast(0x1234); + GstMediaType mediaType = eGST_MEDIATYPE_VIDEO; + GstCaps caps = {}; + gst_media_stream* stream = &mPlayerContext->stream[mediaType]; + stream->format = GST_FORMAT_VIDEO_ES_H264; + stream->codecInfo = MediaCodecInfo(stream->format); + stream->codecInfo.mIsEncrypted = false; + mPlayerConfigParams->videoBufBytes = 500; + + EXPECT_CALL(*g_mockGstUtils, GetCaps(GST_FORMAT_VIDEO_ES_H264)).WillOnce(Return(&caps)); + EXPECT_CALL(*g_mockGStreamer, gst_caps_get_structure(_, _)).Times(0); + + mInterfaceGstPlayer->InitializeSourceForPlayer(playerInstance, source, mediaType); + + delete g_mockGstUtils; + g_mockGstUtils = nullptr; +} + TEST_F(InterfacePlayerTests, InitializeSourceForPlayer_Audio_CapsNull) { @@ -3106,12 +3257,29 @@ TEST_F(InterfacePlayerTests, SetStreamCaps_ValidVideoCodecFormat) delete g_mockGstUtils; } +TEST_F(InterfacePlayerTests, SetStreamCaps_EncryptedContainerCapsRemainUnchanged) +{ + g_mockGstUtils = new StrictMock(); + MediaCodecInfo codecInfo; + codecInfo.mCodecFormat = GST_FORMAT_ISO_BMFF; + codecInfo.mIsEncrypted = true; + GstCaps caps; + EXPECT_CALL(*g_mockGstUtils, GetCaps(GST_FORMAT_ISO_BMFF)).WillOnce(Return(&caps)); + EXPECT_CALL(*g_mockGStreamer, gst_caps_get_structure(_, _)).Times(0); + EXPECT_CALL(*g_mockGStreamer, gst_app_src_set_caps(_, &caps)).Times(1); + + mInterfaceGstPlayer->SetStreamCaps(eGST_MEDIATYPE_VIDEO, std::move(codecInfo)); + + delete g_mockGstUtils; +} + /** * SetStreamCaps Tests with encrypted audio codec format */ TEST_F(InterfacePlayerTests, SetStreamCaps_EncryptedAudioCodecFormat) { g_mockGstUtils = new StrictMock(); + mInterfaceGstPlayer->SetPreferredDRM("urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"); MediaCodecInfo codecInfo; codecInfo.mCodecFormat = GST_FORMAT_AUDIO_ES_AAC_RAW; codecInfo.mCodecData = std::vector{0x00, 0x00, 0x00, 0x01}; @@ -3125,6 +3293,7 @@ TEST_F(InterfacePlayerTests, SetStreamCaps_EncryptedAudioCodecFormat) EXPECT_CALL(*g_mockGStreamer, gst_caps_get_structure(&caps, 0)).WillRepeatedly(Return(&s)); EXPECT_CALL(*g_mockGStreamer, gst_structure_set(&s, StrEq("codec_data"))).Times(1); EXPECT_CALL(*g_mockGStreamer, gst_structure_set(&s, StrEq("original-media-type"))).Times(1); + EXPECT_CALL(*g_mockGStreamer, gst_structure_set(&s, StrEq("protection-system"))).Times(1); EXPECT_CALL(*g_mockGStreamer, gst_caps_set_simple(&caps, StrEq("channels"))).Times(1); EXPECT_CALL(*g_mockGStreamer, gst_app_src_set_caps(_, &caps)).Times(1); @@ -3133,3 +3302,208 @@ TEST_F(InterfacePlayerTests, SetStreamCaps_EncryptedAudioCodecFormat) delete g_mockGstUtils; } +/* + * shouldReconfigure truth table (all 10 reachable rows are covered below) + * + * Formula: + * shouldReconfigure = (F && (V || I)) || (E && V) + * + * Symbols: + * F = format changed (stream->format != newFormat) + * I = initial UNKNOWN setup only (stream INVALID and newFormat UNKNOWN) + * V = valid new format (newFormat is neither INVALID nor UNKNOWN) + * E = encryption changed + * + * Reachable rows and expected result: + * +------+---+---+---+---+-------------------+ + * | Row | F | I | V | E | shouldReconfigure | + * +------+---+---+---+---+-------------------+ + * | R1 | 1 | 1 | 0 | 0 | 1 | + * | R2 | 1 | 1 | 0 | 1 | 1 | + * | R3 | 1 | 0 | 1 | 0 | 1 | + * | R4 | 1 | 0 | 1 | 1 | 1 | + * | R5 | 1 | 0 | 0 | 0 | 0 | + * | R6 | 1 | 0 | 0 | 1 | 0 | + * | R7 | 0 | 0 | 1 | 0 | 0 | + * | R8 | 0 | 0 | 1 | 1 | 1 | + * | R9 | 0 | 0 | 0 | 0 | 0 | + * | R10 | 0 | 0 | 0 | 1 | 0 | + * +------+---+---+---+---+-------------------+ + * + * Unreachable by definition: + * - I=1 with F=0 (INVALID cannot equal UNKNOWN) + * - I=1 with V=1 (UNKNOWN is not a valid format) + */ +// Define test parameters for the decision matrix +struct ShouldReconfigureTestParams { + std::string testName; // e.g., "R1", "R2", etc. + GstStreamOutputFormat initialFormat; + GstStreamOutputFormat newFormat; + bool initiallyEncrypted; + bool newEncrypted; + int expectedTrackCount; + GstStreamOutputFormat expectedFinalFormat; + bool expectedEncrypted; +}; + +class ConfigurePipelineShouldReconfigureTest : public InterfacePlayerTests, + public ::testing::WithParamInterface { +}; + +INSTANTIATE_TEST_SUITE_P( + DecisionMatrix, + ConfigurePipelineShouldReconfigureTest, + ::testing::Values( + // R1: F=1, I=1, V=0, E=0 -> shouldReconfigure=true + ShouldReconfigureTestParams{ + "R1_FormatChangeInvalidToUnknown_NoEncryption", + GST_FORMAT_INVALID, + GST_FORMAT_UNKNOWN, + false, + false, + 1, + GST_FORMAT_UNKNOWN, + false + }, + // R2: F=1, I=1, V=0, E=1 -> shouldReconfigure=true + ShouldReconfigureTestParams{ + "R2_FormatChangeInvalidToUnknown_WithEncryption", + GST_FORMAT_INVALID, + GST_FORMAT_UNKNOWN, + false, + true, + 1, + GST_FORMAT_UNKNOWN, + true + }, + // R3: F=1, I=0, V=1, E=0 -> shouldReconfigure=true + ShouldReconfigureTestParams{ + "R3_ValidFormatChange_NoEncryption", + GST_FORMAT_VIDEO_ES_H264, + GST_FORMAT_VIDEO_ES_HEVC, + false, + false, + 1, + GST_FORMAT_VIDEO_ES_HEVC, + false + }, + // R4: F=1, I=0, V=1, E=1 -> shouldReconfigure=true + ShouldReconfigureTestParams{ + "R4_ValidFormatChange_WithEncryption", + GST_FORMAT_VIDEO_ES_H264, + GST_FORMAT_VIDEO_ES_HEVC, + false, + true, + 1, + GST_FORMAT_VIDEO_ES_HEVC, + true + }, + // R5: F=1, I=0, V=0, E=0 -> shouldReconfigure=false + ShouldReconfigureTestParams{ + "R5_NoValidFormatChange_NoEncryption", + GST_FORMAT_VIDEO_ES_H264, + GST_FORMAT_UNKNOWN, + false, + false, + 0, + GST_FORMAT_VIDEO_ES_H264, + false + }, + // R5 additional: UNKNOWN -> INVALID transition + ShouldReconfigureTestParams{ + "R5_UnknownToInvalid_NoEncryption", + GST_FORMAT_UNKNOWN, + GST_FORMAT_INVALID, + false, + false, + 0, + GST_FORMAT_UNKNOWN, + false + }, + // R6: F=1, I=0, V=0, E=1 -> shouldReconfigure=false + ShouldReconfigureTestParams{ + "R6_NoValidFormatChange_WithEncryption", + GST_FORMAT_UNKNOWN, + GST_FORMAT_INVALID, + false, + true, + 0, + GST_FORMAT_UNKNOWN, + false + }, + // R7: F=0, I=0, V=1, E=0 -> shouldReconfigure=false + ShouldReconfigureTestParams{ + "R7_NoFormatChange_SameFormat_NoEncryption", + GST_FORMAT_VIDEO_ES_H264, + GST_FORMAT_VIDEO_ES_H264, + false, + false, + 0, + GST_FORMAT_VIDEO_ES_H264, + false + }, + // R8: F=0, I=0, V=1, E=1 -> shouldReconfigure=true + ShouldReconfigureTestParams{ + "R8_NoFormatChange_EncryptionChange", + GST_FORMAT_VIDEO_ES_H264, + GST_FORMAT_VIDEO_ES_H264, + false, + true, + 1, + GST_FORMAT_VIDEO_ES_H264, + true + }, + // R9: F=0, I=0, V=0, E=0 -> shouldReconfigure=false + ShouldReconfigureTestParams{ + "R9_NoFormatChange_UnknownFormat_NoEncryption", + GST_FORMAT_UNKNOWN, + GST_FORMAT_UNKNOWN, + false, + false, + 0, + GST_FORMAT_UNKNOWN, + false + }, + // R10: F=0, I=0, V=0, E=1 -> shouldReconfigure=false + ShouldReconfigureTestParams{ + "R10_NoFormatChange_UnknownFormat_EncryptionChange", + GST_FORMAT_UNKNOWN, + GST_FORMAT_UNKNOWN, + false, + true, + 0, + GST_FORMAT_UNKNOWN, + false + } + ), + [](const ::testing::TestParamInfo& info) { + return info.param.testName; + } +); + +TEST_P(ConfigurePipelineShouldReconfigureTest, ConfigurePipeline_ShouldReconfigureDecisionMatrix) +{ + auto param = GetParam(); + g_mockGStreamer = nullptr; + + mPlayerContext->NumberOfTracks = 0; + mPlayerContext->stream[eGST_MEDIATYPE_VIDEO].format = param.initialFormat; + mPlayerContext->stream[eGST_MEDIATYPE_VIDEO].codecInfo.mIsEncrypted = param.initiallyEncrypted; + + // Build the codec info based on parameters + MediaCodecInfo codecInfo; + codecInfo.mCodecFormat = param.newFormat; + codecInfo.mIsEncrypted = param.newEncrypted; + + mInterfaceGstPlayer->ConfigurePipeline( + StreamCodecInfo{std::move(codecInfo), GST_FORMAT_INVALID, GST_FORMAT_INVALID}, + false, false, false, 0, GST_NORMAL_PLAY_RATE, "testPipeline", 0, false, "testManifest", false); + + EXPECT_EQ(mPlayerContext->NumberOfTracks, param.expectedTrackCount); + EXPECT_EQ(mPlayerContext->stream[eGST_MEDIATYPE_VIDEO].format, param.expectedFinalFormat); + if (param.expectedTrackCount > 0) { + EXPECT_EQ(mPlayerContext->stream[eGST_MEDIATYPE_VIDEO].codecInfo.mIsEncrypted, param.expectedEncrypted); + } else { + EXPECT_EQ(mPlayerContext->stream[eGST_MEDIATYPE_VIDEO].codecInfo.mIsEncrypted, param.initiallyEncrypted); + } +} diff --git a/test/utests/tests/OCDMSessionAdapter/FunctionalTests.cpp b/test/utests/tests/OCDMSessionAdapter/FunctionalTests.cpp index e3805f60..0c6f477c 100644 --- a/test/utests/tests/OCDMSessionAdapter/FunctionalTests.cpp +++ b/test/utests/tests/OCDMSessionAdapter/FunctionalTests.cpp @@ -81,7 +81,17 @@ TEST_F(OCDMSessionAdapterTests, generateDRMSession) const char *initDataType = "cenc"; uint8_t initDataTypeLen = strlen(initDataType); - ((*g_mockopencdm).gmock_opencdm_construct_session(ocdmSystem, LicenseType::Temporary, MemBufEq(initDataType, initDataTypeLen), MemBufEq(initData, initDataLen), f_cbInitData, MemBufEq(customData.c_str(), customData.length()), customData.length(),_,_,_))(::testing::internal::GetWithoutMatchers(), nullptr) .InternalExpectedAt("/home/rekha/RDK/latest/l1_Final/aamp/middleware/test/utests/tests/OCDMSessionAdapter/FunctionalTests.cpp", 91, "*g_mockopencdm", "opencdm_construct_session(ocdmSystem, LicenseType::Temporary, MemBufEq(initDataType, initDataTypeLen), MemBufEq(initData, initDataLen), f_cbInitData, MemBufEq(customData.c_str(), customData.length()), customData.length(),_,_,_)").WillOnce(Return(ERROR_NONE)); + EXPECT_CALL(*g_mockopencdm, + opencdm_construct_session( + ocdmSystem, + LicenseType::Temporary, + MemBufEq(initDataType, initDataTypeLen), + MemBufEq(initData, initDataLen), + f_cbInitData, + MemBufEq(customData.c_str(), customData.length()), + customData.length(), + _, _, _)) + .WillOnce(Return(ERROR_NONE)); m_ocdmsessionadapter->generateDRMSession(initData, f_cbInitData, customData); } diff --git a/test/utests/tests/OcdmBasicSessionAdapterTests/FunctionalTests.cpp b/test/utests/tests/OcdmBasicSessionAdapterTests/FunctionalTests.cpp index 8d5157bd..9cdad983 100644 --- a/test/utests/tests/OcdmBasicSessionAdapterTests/FunctionalTests.cpp +++ b/test/utests/tests/OcdmBasicSessionAdapterTests/FunctionalTests.cpp @@ -59,7 +59,6 @@ std::shared_ptr drmHelper; DrmInfo drminfo; MockDrmMemorySystem *g_mockMemorySystem; static std::string g_defaultSystemId = "com.widevine.alpha"; -static std::vector> g_emptyKeys; class OcdmBasicSessionAdapterTests : public ::testing::Test { @@ -82,7 +81,7 @@ class OcdmBasicSessionAdapterTests : public ::testing::Test g_mockOpenCdmSessionAdapter = new NiceMock(); // Set default return value for getUsableKeys() to return an empty vector - ON_CALL(*g_mockOpenCdmSessionAdapter, getUsableKeys()).WillByDefault(testing::ReturnRef(g_emptyKeys)); + ON_CALL(*g_mockOpenCdmSessionAdapter, getUsableKeys()).WillByDefault(testing::Return(std::vector>{})); g_mockMemorySystem = new NiceMock(); } diff --git a/test/utests/tests/PlayerDirectRialtoCCManagerTests/CMakeLists.txt b/test/utests/tests/PlayerDirectRialtoCCManagerTests/CMakeLists.txt new file mode 100644 index 00000000..f49ec9d6 --- /dev/null +++ b/test/utests/tests/PlayerDirectRialtoCCManagerTests/CMakeLists.txt @@ -0,0 +1,68 @@ +# 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(GoogleTest) + +set(PLAYER_ROOT "../../../../") +set(UTESTS_ROOT "../../") +set(EXEC_NAME PlayerDirectRialtoCCManagerTests) + +include_directories(${PLAYER_ROOT} + ${PLAYER_ROOT}/closedcaptions + ${PLAYER_ROOT}/closedcaptions/direct-rialto + ${PLAYER_ROOT}/playerLogManager) + +include_directories(${GTEST_INCLUDE_DIRS}) +include_directories(${GMOCK_INCLUDE_DIRS}) +include_directories(${GLIB_INCLUDE_DIRS}) +include_directories(${GSTREAMER_INCLUDE_DIRS}) +include_directories(SYSTEM ${UTESTS_ROOT}/mocks) + +set(TEST_SOURCES + PlayerDirectRialtoCCManagerTests.cpp + PlayerDirectRialtoCCManagerTestCases.cpp + PlayerDirectRialtoCCManagerBaseStubs.cpp) + +set(PLAYER_SOURCES + ${PLAYER_ROOT}/closedcaptions/direct-rialto/PlayerDirectRialtoCCManager.cpp + ${PLAYER_ROOT}/playerLogManager/PlayerLogManager.cpp) + +add_executable(${EXEC_NAME} + ${TEST_SOURCES} + ${PLAYER_SOURCES}) + +set_target_properties(${EXEC_NAME} PROPERTIES FOLDER "utests") + +if (CMAKE_XCODE_BUILD_SYSTEM) + # XCode schema target + xcode_define_schema(${EXEC_NAME}) +endif() + +if (COVERAGE_ENABLED) + include(CodeCoverage) + APPEND_COVERAGE_COMPILER_FLAGS() +endif() + +target_link_libraries(${EXEC_NAME} + fakes + -pthread + ${GLIB_LINK_LIBRARIES} + ${OS_LD_FLAGS} + ${GMOCK_LINK_LIBRARIES} + ${GTEST_LINK_LIBRARIES}) + +player_utest_run_add(${EXEC_NAME}) \ No newline at end of file diff --git a/test/utests/tests/PlayerDirectRialtoCCManagerTests/PlayerDirectRialtoCCManagerBaseStubs.cpp b/test/utests/tests/PlayerDirectRialtoCCManagerTests/PlayerDirectRialtoCCManagerBaseStubs.cpp new file mode 100644 index 00000000..bb3d1323 --- /dev/null +++ b/test/utests/tests/PlayerDirectRialtoCCManagerTests/PlayerDirectRialtoCCManagerBaseStubs.cpp @@ -0,0 +1,95 @@ +/* + * 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 "PlayerCCManager.h" + +int PlayerCCManagerBase::Init(void *handle) +{ + (void) handle; + return 0; +} + +void PlayerCCManagerBase::RestoreCC(bool shouldRestoreCC) +{ + if (!mEnabled && shouldRestoreCC) + { + mEnabled = shouldRestoreCC; + } +} + +bool PlayerCCManagerBase::IsOOBCCRenderingSupported() +{ + return false; +} + +int PlayerCCManagerBase::SetStatus(bool enable) +{ + mEnabled = enable; + return 0; +} + +int PlayerCCManagerBase::SetStyle(const std::string &options) +{ + mOptions = options; + return 0; +} + +int PlayerCCManagerBase::SetTrack(const std::string &track, + const CCFormat format) +{ + mTrack = track; + mTrackFormat = format; + return 0; +} + +void PlayerCCManagerBase::SetTrickplayStatus(bool enable) +{ + mTrickplayStarted = enable; +} + +void PlayerCCManagerBase::SetParentalControlStatus(bool locked) +{ + mParentalCtrlLocked = locked; +} + +void PlayerCCManagerBase::Stop() +{ + StopRendering(); +} + +void PlayerCCManagerBase::ResetState() +{ + // Mirrors the real PlayerCCManagerBase::ResetState(), which calls Stop() + // (-> StopRendering()) before clearing state; this exercises the same + // mutex re-entrancy that PlayerDirectRialtoCCManager::ResetState() must + // avoid deadlocking on. + Stop(); + ResetTrackState(); +} + +void PlayerCCManagerBase::ResetTrackState() +{ + mOptions.clear(); + mTrack.clear(); + mTrackFormat = eCLOSEDCAPTION_FORMAT_DEFAULT; + mLastTextTracks.clear(); + mEnabled = false; + mTrickplayStarted = false; + mParentalCtrlLocked = false; +} \ No newline at end of file diff --git a/test/utests/tests/PlayerDirectRialtoCCManagerTests/PlayerDirectRialtoCCManagerTestCases.cpp b/test/utests/tests/PlayerDirectRialtoCCManagerTests/PlayerDirectRialtoCCManagerTestCases.cpp new file mode 100644 index 00000000..39adb15d --- /dev/null +++ b/test/utests/tests/PlayerDirectRialtoCCManagerTests/PlayerDirectRialtoCCManagerTestCases.cpp @@ -0,0 +1,318 @@ +/* + * 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 PlayerDirectRialtoCCManagerTestCases.cpp + * @brief L1 unit tests for PlayerDirectRialtoCCManager. + * + * Tests verify the CC manager's orchestration of its IDirectRialtoCC + * dependency using a strict mock. Per the L1 golden rule, every assertion + * verifies how the component under test (PlayerDirectRialtoCCManager) + * behaves, not how the mock behaves. + */ + +#include +#include +#include +#include + +#include "PlayerDirectRialtoCCManager.h" +#include "IDirectRialtoCC.h" + +using ::testing::_; +using ::testing::Return; +using ::testing::StrictMock; + +class MockIDirectRialtoCC : public IDirectRialtoCC +{ +public: + MOCK_METHOD(bool, setTextTrackIdentifier, (const std::string &id), (override)); + MOCK_METHOD(bool, setCCMute, (bool muted), (override)); +}; + +class PlayerDirectRialtoCCManagerTestable : public PlayerDirectRialtoCCManager +{ +public: + using PlayerDirectRialtoCCManager::Initialize; + using PlayerDirectRialtoCCManager::StartRendering; + using PlayerDirectRialtoCCManager::StopRendering; + using PlayerDirectRialtoCCManager::ResetState; +}; + +class PlayerDirectRialtoCCManagerTest : public ::testing::Test +{ +protected: + void SetUp() override + { + m_mock = std::make_unique>(); + } + + void TearDown() override + { + m_mock.reset(); + } + + void InitWithDefaultTrack() + { + EXPECT_CALL(*m_mock, setTextTrackIdentifier("CC1")) + .Times(1) + .WillOnce(Return(true)); + m_mgr.Initialize(m_mock.get()); + } + + PlayerDirectRialtoCCManagerTestable m_mgr; + std::unique_ptr m_mock; +}; + +/** + * @test Initialize_WithNullHandle_DoesNotCallControl + * @brief Passing nullptr must not crash and must not call the mock. + */ +TEST_F(PlayerDirectRialtoCCManagerTest, + Initialize_WithNullHandle_DoesNotCallControl) +{ + EXPECT_NO_FATAL_FAILURE(m_mgr.Initialize(nullptr)); +} + +/** + * @test Initialize_WithHandle_SetsDefaultTrackCC1 + * @brief When no track is cached, Initialize() should apply "CC1" as the + * default text-track identifier. + */ +TEST_F(PlayerDirectRialtoCCManagerTest, + Initialize_WithHandle_SetsDefaultTrackCC1) +{ + EXPECT_CALL(*m_mock, setTextTrackIdentifier("CC1")) + .Times(1) + .WillOnce(Return(true)); + + m_mgr.Initialize(m_mock.get()); +} + +/** + * @test Initialize_ControlRejectsDefaultTrack_ReturnsFailure + * @brief When the control rejects the default "CC1" identifier, + * Initialize() must propagate the failure so + * PlayerCCManagerBase::Init() reports it, instead of silently + * discarding the SetTrack() result and returning 0. + */ +TEST_F(PlayerDirectRialtoCCManagerTest, + Initialize_ControlRejectsDefaultTrack_ReturnsFailure) +{ + EXPECT_CALL(*m_mock, setTextTrackIdentifier("CC1")) + .Times(1) + .WillOnce(Return(false)); + + EXPECT_EQ(m_mgr.Initialize(m_mock.get()), -1); +} + +/** + * @test Initialize_WithCachedTrack_ReappliesCachedTrack + * @brief When a track has already been cached via SetTrack(), Initialize() + * with a new handle must re-apply the cached identifier, not "CC1". + */ +TEST_F(PlayerDirectRialtoCCManagerTest, + Initialize_WithCachedTrack_ReappliesCachedTrack) +{ + m_mgr.SetTrack("CC3"); + + EXPECT_CALL(*m_mock, setTextTrackIdentifier("CC3")) + .Times(1) + .WillOnce(Return(true)); + + m_mgr.Initialize(m_mock.get()); +} + +/** + * @test Initialize_ControlRejectsCachedTrack_ReturnsFailure + * @brief When the control rejects the cached identifier on re-apply, + * Initialize() must propagate the failure instead of returning 0. + */ +TEST_F(PlayerDirectRialtoCCManagerTest, + Initialize_ControlRejectsCachedTrack_ReturnsFailure) +{ + m_mgr.SetTrack("CC3"); + + EXPECT_CALL(*m_mock, setTextTrackIdentifier("CC3")) + .Times(1) + .WillOnce(Return(false)); + + EXPECT_EQ(m_mgr.Initialize(m_mock.get()), -1); +} + +/** + * @test Initialize_RetryAfterRejectedDefaultTrack_ReappliesTrack + * @brief After a failed default-track application, Initialize() must clear + * the unconfigured control so a retry with the same handle is + * treated as a handle change and re-attempts SetTrack(), instead of + * seeing an unchanged handle and returning success without + * configuring the control. + */ +TEST_F(PlayerDirectRialtoCCManagerTest, + Initialize_RetryAfterRejectedDefaultTrack_ReappliesTrack) +{ + { + ::testing::InSequence seq; + EXPECT_CALL(*m_mock, setTextTrackIdentifier("CC1")) + .WillOnce(Return(false)); + EXPECT_CALL(*m_mock, setTextTrackIdentifier("CC1")) + .WillOnce(Return(true)); + } + + EXPECT_EQ(m_mgr.Initialize(m_mock.get()), -1); + EXPECT_EQ(m_mgr.Initialize(m_mock.get()), 0); +} + +/** + * @test SetTrack_ControlRejectsIdentifier_ReturnsFailure + * @brief When setTextTrackIdentifier() reports failure, SetTrack() must + * return -1, per the PlayerCCManagerBase::SetTrack() 0/-1 contract. + */ +TEST_F(PlayerDirectRialtoCCManagerTest, + SetTrack_ControlRejectsIdentifier_ReturnsFailure) +{ + InitWithDefaultTrack(); + + EXPECT_CALL(*m_mock, setTextTrackIdentifier("CC2")) + .Times(1) + .WillOnce(Return(false)); + + EXPECT_EQ(m_mgr.SetTrack("2", eCLOSEDCAPTION_FORMAT_608), -1); +} + +/** + * @test SetTrack_ControlRejectsIdentifier_CacheKeepsPreviousTrack + * @brief A rejected SetTrack() must not update mTrack/mTrackFormat, so a + * later Initialize() with the same handle still sees a changed + * handle correctly re-apply the last-accepted track rather than + * skipping it because the cache already matched the rejected one. + */ +TEST_F(PlayerDirectRialtoCCManagerTest, + SetTrack_ControlRejectsIdentifier_CacheKeepsPreviousTrack) +{ + InitWithDefaultTrack(); + + EXPECT_CALL(*m_mock, setTextTrackIdentifier("CC2")) + .Times(1) + .WillOnce(Return(false)); + + EXPECT_EQ(m_mgr.SetTrack("2", eCLOSEDCAPTION_FORMAT_608), -1); + EXPECT_EQ(m_mgr.GetTrack(), "CC1"); +} + +/** + * @test SetTrack_NumericWith608Format_PrependsCCPrefix + * @brief A numeric track string with 608 format should become "CC". + */ +TEST_F(PlayerDirectRialtoCCManagerTest, + SetTrack_NumericWith608Format_PrependsCCPrefix) +{ + InitWithDefaultTrack(); + + EXPECT_CALL(*m_mock, setTextTrackIdentifier("CC2")) + .Times(1) + .WillOnce(Return(true)); + + m_mgr.SetTrack("2", eCLOSEDCAPTION_FORMAT_608); +} + +/** + * @test SetTrack_NumericWith708Format_PrependsServicePrefix + * @brief A numeric track string with 708 format should become "SERVICE". + */ +TEST_F(PlayerDirectRialtoCCManagerTest, + SetTrack_NumericWith708Format_PrependsServicePrefix) +{ + InitWithDefaultTrack(); + + EXPECT_CALL(*m_mock, setTextTrackIdentifier("SERVICE3")) + .Times(1) + .WillOnce(Return(true)); + + m_mgr.SetTrack("3", eCLOSEDCAPTION_FORMAT_708); +} + +/** + * @test SetTrack_AlphabeticTrack_PassedThrough + * @brief A track string that already has an alphabetic prefix must be + * forwarded unchanged. + */ +TEST_F(PlayerDirectRialtoCCManagerTest, + SetTrack_AlphabeticTrack_PassedThrough) +{ + InitWithDefaultTrack(); + + EXPECT_CALL(*m_mock, setTextTrackIdentifier("CC1")) + .Times(1) + .WillOnce(Return(true)); + + m_mgr.SetTrack("CC1"); +} + +/** + * @test StartRendering_CallsCCMuteFalse + * @brief StartRendering() must un-mute CC by calling setCCMute(false). + */ +TEST_F(PlayerDirectRialtoCCManagerTest, + StartRendering_CallsCCMuteFalse) +{ + InitWithDefaultTrack(); + + EXPECT_CALL(*m_mock, setCCMute(false)) + .Times(1) + .WillOnce(Return(true)); + + m_mgr.StartRendering(); +} + +/** + * @test StopRendering_CallsCCMuteTrue + * @brief StopRendering() must mute CC by calling setCCMute(true). + */ +TEST_F(PlayerDirectRialtoCCManagerTest, + StopRendering_CallsCCMuteTrue) +{ + InitWithDefaultTrack(); + + EXPECT_CALL(*m_mock, setCCMute(true)) + .Times(1) + .WillOnce(Return(true)); + + m_mgr.StopRendering(); +} + +/** + * @test ResetState_ClearsControlHandle + * @brief ResetState() must call Stop() (-> StopRendering()) on the still- + * valid control without deadlocking on m_controlMutex, then clear + * the control so a subsequent StartRendering() is a no-op. + */ +TEST_F(PlayerDirectRialtoCCManagerTest, + ResetState_ClearsControlHandle) +{ + InitWithDefaultTrack(); + + EXPECT_CALL(*m_mock, setCCMute(true)) + .Times(1) + .WillOnce(Return(true)); + + m_mgr.ResetState(); + + EXPECT_NO_FATAL_FAILURE(m_mgr.StartRendering()); +} \ No newline at end of file diff --git a/test/utests/tests/PlayerDirectRialtoCCManagerTests/PlayerDirectRialtoCCManagerTests.cpp b/test/utests/tests/PlayerDirectRialtoCCManagerTests/PlayerDirectRialtoCCManagerTests.cpp new file mode 100644 index 00000000..9be451e7 --- /dev/null +++ b/test/utests/tests/PlayerDirectRialtoCCManagerTests/PlayerDirectRialtoCCManagerTests.cpp @@ -0,0 +1,31 @@ +/* + * 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 PlayerDirectRialtoCCManagerTests.cpp + * @brief Test runner for PlayerDirectRialtoCCManager unit tests. + */ + +#include + +int main(int argc, char **argv) +{ + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} \ No newline at end of file diff --git a/vendor/SocInterface.cpp b/vendor/SocInterface.cpp index db271ff8..7724c85c 100644 --- a/vendor/SocInterface.cpp +++ b/vendor/SocInterface.cpp @@ -27,6 +27,7 @@ #include "vendor/mtk/MtkSocInterface.h" #endif + /**Initially re-sets the IsRialtoMode */ bool SocInterface::mIsRialtoMode = false;