diff --git a/src/Session.cpp b/src/Session.cpp index f7ce1c2dc..9b07ac0a8 100644 --- a/src/Session.cpp +++ b/src/Session.cpp @@ -17,6 +17,7 @@ #include "common/Chooser.h" #include "decrypters/DrmFactory.h" #include "decrypters/Helpers.h" +#include "samplereader/SampleReaderFactory.h" #include "utils/Base64Utils.h" #include "utils/CurlUtils.h" #include "utils/StringUtils.h" @@ -52,32 +53,9 @@ void SESSION::CSession::DeleteStreams() m_streams.clear(); } -void SESSION::CSession::SetSupportedDecrypterURN(std::vector& keySystems) -{ - std::string decrypterPath = CSrvBroker::GetSettings().GetDecrypterPath(); - if (decrypterPath.empty()) - { - LOG::Log(LOGWARNING, "Decrypter path not set in the add-on settings"); - return; - } - - const std::string keySystem = CSrvBroker::GetKodiProps().GetDrmKeySystem(); - m_decrypter = DRM::FACTORY::GetDecrypter(GetCryptoKeySystem(keySystem)); - if (!m_decrypter) - return; - - if (!m_decrypter->Initialize()) - { - LOG::Log(LOGERROR, "The decrypter library cannot be initialized."); - return; - } - - keySystems = m_decrypter->SelectKeySystems(keySystem); - m_decrypter->SetLibraryPath(decrypterPath); -} - void SESSION::CSession::DisposeSampleDecrypter() { + /* if (m_decrypter) { for (auto& cdmSession : m_cdmSessions) @@ -86,12 +64,13 @@ void SESSION::CSession::DisposeSampleDecrypter() cdmSession.m_cencSingleSampleDecrypter = nullptr; } } + */ } void SESSION::CSession::DisposeDecrypter() { - DisposeSampleDecrypter(); - m_decrypter = nullptr; + //DisposeSampleDecrypter(); + //m_decrypter = nullptr; } /*---------------------------------------------------------------------- @@ -119,7 +98,7 @@ bool SESSION::CSession::Initialize(std::string manifestUrl) } auto& kodiProps = CSrvBroker::GetKodiProps(); - + /* REMOVE ME // Get URN's wich are supported by this addon std::vector supportedKeySystems; if (!kodiProps.GetDrmKeySystem().empty()) @@ -130,26 +109,18 @@ bool SESSION::CSession::Initialize(std::string manifestUrl) LOG::Log(LOGDEBUG, "Supported URN: %s", keySystem.data()); } } - + */ std::map manifestHeaders = kodiProps.GetManifestHeaders(); bool isSessionOpened{false}; - // Preinitialize the DRM, if pre-initialisation data are provided - if (!kodiProps.GetDrmConfig().preInitData.empty()) + DRM::DRMSession session; + // Pre-initialize the DRM allow to generate the challenge and session ID data + // used to make licensed manifest requests + if (m_drmEngine.PreInitializeDRM(session)) { - std::string challengeB64; - std::string sessionId; - // Pre-initialize the DRM allow to generate the challenge and session ID data - // used to make licensed manifest requests (via proxy callback) - if (PreInitializeDRM(challengeB64, sessionId, isSessionOpened)) - { - manifestHeaders["challengeB64"] = STRING::URLEncode(challengeB64); - manifestHeaders["sessionId"] = sessionId; - } - else - { - return false; - } + // The following are custom headers that must be handled through a proxy server + manifestHeaders["challengeB64"] = STRING::URLEncode(session.challenge); + manifestHeaders["sessionId"] = session.id; } URL::RemovePipePart(manifestUrl); // No pipe char uses, must be used Kodi properties only @@ -175,7 +146,7 @@ bool SESSION::CSession::Initialize(std::string manifestUrl) if (!m_adaptiveTree) return false; - m_adaptiveTree->Configure(m_reprChooser, supportedKeySystems, kodiProps.GetManifestUpdParams()); + m_adaptiveTree->Configure(m_reprChooser, kodiProps.GetManifestUpdParams()); if (!m_adaptiveTree->Open(manifestResp.effectiveUrl, manifestResp.headers, manifestResp.data)) { @@ -188,9 +159,14 @@ bool SESSION::CSession::Initialize(std::string manifestUrl) CSrvBroker::GetInstance()->InitStage2(m_adaptiveTree); + m_drmEngine.Initialize(); + return InitializePeriod(isSessionOpened); } +/* +* TO BE REIMPLEMENTED +* void SESSION::CSession::CheckHDCP() { //! @todo: is needed to implement an appropriate CP check to @@ -232,304 +208,25 @@ void SESSION::CSession::CheckHDCP() } } } - -bool SESSION::CSession::PreInitializeDRM(std::string& challengeB64, - std::string& sessionId, - bool& isSessionOpened) -{ - auto& drmPropCfg = CSrvBroker::GetKodiProps().GetDrmConfig(); - - std::string psshData; - std::string kidData; - // Parse the PSSH/KID data - size_t posSplitter = drmPropCfg.preInitData.find("|"); - if (posSplitter != std::string::npos) - { - psshData = drmPropCfg.preInitData.substr(0, posSplitter); - kidData = drmPropCfg.preInitData.substr(posSplitter + 1); - } - - if (psshData.empty() || kidData.empty()) - { - LOG::LogF(LOGERROR, "Invalid DRM pre-init data, must be as: {PSSH as base64}|{KID as base64}"); - return false; - } - - m_cdmSessions.resize(2); - - // Try to initialize an SingleSampleDecryptor - LOG::LogF(LOGDEBUG, "Entering encryption section"); - - if (!m_decrypter) - { - LOG::LogF(LOGERROR, "No decrypter found for encrypted stream"); - return false; - } - - if (!m_decrypter->IsInitialised()) - { - DRM::Config drmCfg = DRM::CreateDRMConfig(DRM::KS_WIDEVINE, drmPropCfg); - if (!m_decrypter->OpenDRMSystem(drmCfg)) - { - LOG::LogF(LOGERROR, "OpenDRMSystem failed"); - return false; - } - } - - std::vector initData; - - // Set the provided PSSH - initData = BASE64::Decode(psshData); - - // Decode the provided KID - const std::vector decKid = BASE64::Decode(kidData); - - CCdmSession& session(m_cdmSessions[1]); - - std::string hexKid{STRING::ToHexadecimal(decKid)}; - LOG::LogF(LOGDEBUG, "Initializing session with KID: %s", hexKid.c_str()); - - if (m_decrypter && (session.m_cencSingleSampleDecrypter = - m_decrypter->CreateSingleSampleDecrypter(initData, decKid, "", true, - CryptoMode::AES_CTR)) != nullptr) - { - session.m_sessionId = session.m_cencSingleSampleDecrypter->GetSessionId(); - sessionId = session.m_sessionId; - challengeB64 = m_decrypter->GetChallengeB64Data(session.m_cencSingleSampleDecrypter); - } - else - { - LOG::LogF(LOGERROR, "Initialize failed (SingleSampleDecrypter)"); - session.m_cencSingleSampleDecrypter = nullptr; - return false; - } -#if defined(ANDROID) - // On android is not possible add the default KID key - // then we cannot re-use same session - DisposeSampleDecrypter(); -#else - isSessionOpened = true; -#endif - return true; -} - -bool SESSION::CSession::InitializeDRM(bool addDefaultKID /* = false */) -{ - bool isSecureVideoSession{false}; - m_cdmSessions.resize(m_adaptiveTree->m_currentPeriod->GetPSSHSets().size()); - - // Try to initialize an SingleSampleDecryptor - if (m_adaptiveTree->m_currentPeriod->GetEncryptionState() == EncryptionState::ENCRYPTED_DRM) - { - const std::string keySystem = CSrvBroker::GetKodiProps().GetDrmKeySystem(); - auto& drmPropCfg = CSrvBroker::GetKodiProps().GetDrmConfig(); - - DRM::Config drmCfg = DRM::CreateDRMConfig(keySystem, drmPropCfg); - - if (drmCfg.license.serverUrl.empty()) - drmCfg.license.serverUrl = m_adaptiveTree->GetLicenseUrl(); - - LOG::Log(LOGDEBUG, "Entering encryption section"); - - if (!m_decrypter) - { - LOG::Log(LOGERROR, "No decrypter found for encrypted stream"); - return false; - } - - if (!m_decrypter->IsInitialised()) - { - if (!m_decrypter->OpenDRMSystem(drmCfg)) - { - LOG::Log(LOGERROR, "OpenDRMSystem failed"); - return false; - } - } - - // cdmSession 0 is reserved for unencrypted streams - for (size_t ses{1}; ses < m_cdmSessions.size(); ++ses) - { - CCdmSession& session{m_cdmSessions[ses]}; - - // Check if the decrypter has been previously initialized, if so skip it, - // sessions are collected and never removed and InitializeDRM can be called more times - // depending on how it is used: - // 1) CSession::Initialize->InitializePeriod->InitializeDRM - Used by DASH/SS (single call) - // 2) CInputStreamAdaptive::DemuxRead->m_session->InitializePeriod()->InitializeDRM - On chapter change (single call) - // 3) CInputStreamAdaptive::OpenStream->m_session->PrepareStream->InitializeDRM - Used by HLS (a call for each stream) - if (session.m_cencSingleSampleDecrypter) - continue; - - const CPeriod::PSSHSet& sessionPsshset = m_adaptiveTree->m_currentPeriod->GetPSSHSets()[ses]; - - if (sessionPsshset.adaptation_set_->GetStreamType() == StreamType::NOTYPE) - continue; - - std::vector initData = sessionPsshset.pssh_; - std::string defaultKidStr = sessionPsshset.defaultKID_; - - std::vector customInitData = BASE64::Decode(drmPropCfg.initData); - - if (m_adaptiveTree->GetTreeType() == adaptive::TreeType::SMOOTH_STREAMING && - keySystem == DRM::KS_WIDEVINE) - { - if (DRM::IsValidPsshHeader(customInitData)) - { - initData = customInitData; - } - else - { - LOG::Log(LOGDEBUG, "License data: Create Widevine PSSH for SmoothStreaming %s", - customInitData.empty() ? "" : "(with custom data)"); - - initData = - DRM::PSSH::MakeWidevine({DRM::ConvertKidStrToBytes(defaultKidStr)}, customInitData); - } - } - else if (!customInitData.empty()) - { - // Custom license PSSH data provided from property - // This can allow to initialize a DRM that could be also not specified - // as supported in the manifest (e.g. missing DASH ContentProtection tags) - LOG::Log(LOGDEBUG, "License data: Use PSSH data provided by the license data property"); - initData = customInitData; - } - - // If no KID, but init data, extract the KID from init data - if (!initData.empty() && defaultKidStr.empty()) - { - DRM::PSSH parser; - if (parser.Parse(initData) && !parser.GetKeyIds().empty()) - { - LOG::Log(LOGDEBUG, "Default KID parsed from init data"); - defaultKidStr = STRING::ToHexadecimal(parser.GetKeyIds()[0]); - } - } - - //! @todo: as is implemented InitializeDRM will initialize all PSSHSet's also when are not used, - //! therefore ExtractStreamProtectionData can perform many (not needed) downloads of mp4 init files - if ((initData.empty() && keySystem != DRM::KS_CLEARKEY) || defaultKidStr.empty()) - { - // Try extract the PSSH/KID from the stream - ExtractStreamProtectionData(sessionPsshset, defaultKidStr, initData, - m_adaptiveTree->m_supportedKeySystems); - } - - const std::vector defaultKid = DRM::ConvertKidStrToBytes(defaultKidStr); - - if (addDefaultKID && ses == 1 && session.m_cencSingleSampleDecrypter) - { - // If the CDM has been pre-initialized, on non-android systems - // we use the same session opened then we have to add the current KID - // because the session has been opened with a different PSSH/KID - session.m_cencSingleSampleDecrypter->AddKeyId(defaultKid); - session.m_cencSingleSampleDecrypter->SetDefaultKeyId(defaultKid); - } - - if (!defaultKid.empty()) - { - LOG::Log(LOGDEBUG, "Initializing stream with KID: %s", defaultKidStr.c_str()); - - // If a decrypter has the default KID, re-use the same decrypter for also this session - for (size_t i{1}; i < ses; ++i) - { - if (m_decrypter->HasLicenseKey(m_cdmSessions[i].m_cencSingleSampleDecrypter, defaultKid)) - { - session.m_cencSingleSampleDecrypter = m_cdmSessions[i].m_cencSingleSampleDecrypter; - break; - } - } - } - else if (defaultKid.empty()) - { - for (size_t i{1}; i < ses; ++i) - { - if (sessionPsshset.pssh_ == m_adaptiveTree->m_currentPeriod->GetPSSHSets()[i].pssh_) - { - session.m_cencSingleSampleDecrypter = m_cdmSessions[i].m_cencSingleSampleDecrypter; - break; - } - } - if (!session.m_cencSingleSampleDecrypter) - { - LOG::Log(LOGWARNING, "Initializing stream with unknown KID!"); - } - } - - if (session.m_cencSingleSampleDecrypter || - (session.m_cencSingleSampleDecrypter = m_decrypter->CreateSingleSampleDecrypter( - initData, defaultKid, sessionPsshset.m_licenseUrl, false, - sessionPsshset.m_cryptoMode == CryptoMode::NONE ? CryptoMode::AES_CTR - : sessionPsshset.m_cryptoMode)) != - nullptr) - { - m_decrypter->GetCapabilities(session.m_cencSingleSampleDecrypter, defaultKid, - sessionPsshset.media_, session.m_decrypterCaps); - - session.m_sessionId = session.m_cencSingleSampleDecrypter->GetSessionId(); - - if (session.m_decrypterCaps.flags & DRM::DecrypterCapabilites::SSD_INVALID) - { - m_adaptiveTree->m_currentPeriod->RemovePSSHSet(static_cast(ses)); - } - else if (session.m_decrypterCaps.flags & DRM::DecrypterCapabilites::SSD_SECURE_PATH) - { - isSecureVideoSession = true; - - // Allow to disable the secure decoder - bool disableSecureDecoder = CSrvBroker::GetSettings().IsDisableSecureDecoder(); - // but, DRM config can override it - if (drmPropCfg.isSecureDecoderEnabled.has_value()) - disableSecureDecoder = !*drmPropCfg.isSecureDecoderEnabled; - // but, manifest config can override all others - if (m_adaptiveTree->m_currentPeriod->IsSecureDecodeNeeded().has_value()) - disableSecureDecoder = !*m_adaptiveTree->m_currentPeriod->IsSecureDecodeNeeded(); - if (disableSecureDecoder) - { - LOG::Log(LOGDEBUG, "Initialize DRM: Configured with secure decoder disabled"); - session.m_decrypterCaps.flags &= ~DRM::DecrypterCapabilites::SSD_SECURE_DECODER; - } - } - } - else - { - LOG::Log(LOGERROR, "Initialize failed (SingleSampleDecrypter)"); - for (size_t i(ses); i < m_cdmSessions.size(); ++i) - m_cdmSessions[i].m_cencSingleSampleDecrypter = nullptr; - - return false; - } - } - } - - bool isHdcpOverride = CSrvBroker::GetSettings().IsHdcpOverride(); - if (isHdcpOverride) - LOG::Log(LOGDEBUG, "Ignore HDCP status is enabled"); - - if (!isHdcpOverride) - CheckHDCP(); - - m_reprChooser->SetSecureSession(isSecureVideoSession); - - return true; -} +*/ bool SESSION::CSession::InitializePeriod(bool isSessionOpened /* = false */) { - bool isPsshChanged{true}; - bool isReusePssh{true}; + //REMOVE ME + //bool isPsshChanged{true}; + //bool isReusePssh{true}; if (m_adaptiveTree->IsChangingPeriod()) - { + { /* REMOVE ME isPsshChanged = !(m_adaptiveTree->m_currentPeriod->GetPSSHSets() == m_adaptiveTree->m_nextPeriod->GetPSSHSets()); isReusePssh = !isPsshChanged && m_adaptiveTree->m_nextPeriod->GetEncryptionState() == - EncryptionState::ENCRYPTED_DRM; + EncryptionState::ENCRYPTED_DRM;*/ m_adaptiveTree->m_currentPeriod = m_adaptiveTree->m_nextPeriod; } m_chapterStartTime = GetChapterStartTime(); - + /* REMOVE ME if (m_adaptiveTree->m_currentPeriod->GetEncryptionState() == EncryptionState::NOT_SUPPORTED) { LOG::LogF(LOGERROR, "Unhandled encrypted stream."); @@ -559,7 +256,7 @@ bool SESSION::CSession::InitializePeriod(bool isSessionOpened /* = false */) if (!InitializeDRM(isSessionOpened)) return false; } - + */ uint32_t adpIndex{0}; CAdaptationSet* adp{nullptr}; CHOOSER::StreamSelection streamSelectionMode = m_reprChooser->GetStreamSelectionMode(); @@ -697,7 +394,6 @@ void SESSION::CSession::UpdateStream(CStream& stream) return; } - stream.m_isEncrypted = rep->GetPsshSetPos() != PSSHSET_POS_DEFAULT; stream.m_info.SetExtraData(nullptr, 0); if (!rep->GetCodecPrivateData().empty()) @@ -855,29 +551,76 @@ void SESSION::CSession::UpdateStream(CStream& stream) stream.m_info.SetCodecInternalName(codecStr); } -void SESSION::CSession::PrepareStream(CStream* stream) +bool SESSION::CSession::PrepareStream(CStream* stream, uint64_t startPts) { - if (!m_adaptiveTree->IsReqPrepareStream()) - return; - - CRepresentation* repr = stream->m_adStream.getRepresentation(); const EVENT_TYPE startEvent = stream->m_adStream.GetStartEvent(); + CPeriod* period = stream->m_adStream.getPeriod(); + CAdaptationSet* adp = stream->m_adStream.getAdaptationSet(); + CRepresentation* repr = stream->m_adStream.getRepresentation(); // Prepare the representation when the period change usually its not needed, // because the timeline is always already updated - if ((!m_adaptiveTree->IsChangingPeriod() || repr->Timeline().IsEmpty()) && - (startEvent == EVENT_TYPE::STREAM_START || startEvent == EVENT_TYPE::STREAM_ENABLE)) + if (m_adaptiveTree->IsReqPrepareStream()) + { + if ((!m_adaptiveTree->IsChangingPeriod() || repr->Timeline().IsEmpty()) && + (startEvent == EVENT_TYPE::STREAM_START || startEvent == EVENT_TYPE::STREAM_ENABLE)) + { + m_adaptiveTree->PrepareRepresentation(period, adp, repr); + } + } + + // this check should not needed + //if (stream->m_adStream.getPeriod()->GetEncryptionState() == EncryptionState::ENCRYPTED_DRM) + if (!repr->DrmInfos().empty()) { - m_adaptiveTree->PrepareRepresentation(stream->m_adStream.getPeriod(), - stream->m_adStream.getAdaptationSet(), repr); + DRM::DRMMediaType drmMediaType{DRM::DRMMediaType::UNKNOWN}; + const StreamType sType = adp->GetStreamType(); + + if (sType == StreamType::VIDEO || sType == StreamType::VIDEO_AUDIO) + drmMediaType = DRM::DRMMediaType::VIDEO; + else if (sType == StreamType::AUDIO) + drmMediaType = DRM::DRMMediaType::AUDIO; + else + { + LOG::LogF(LOGWARNING, "Stream media type \"%i\" is not supported by the DRM engine", sType); + return false; + } + + if (!m_drmEngine.InitializeSession(repr->DrmInfos(), drmMediaType, + period->IsSecureDecodeNeeded(), stream->m_info, repr, adp)) + { + return false; + } } - if (stream->m_adStream.getPeriod()->GetEncryptionState() == EncryptionState::ENCRYPTED_DRM) + stream->m_adStream.start_stream(startPts); + stream->SetAdByteStream(std::make_unique(&stream->m_adStream)); + + ContainerType reprContainerType = repr->GetContainerType(); + uint32_t mask = (1U << stream->m_info.GetStreamType()) | GetIncludedStreamMask(); + auto reader = ADP::CreateStreamReader(reprContainerType, stream, mask); + + if (!reader) + return false; + + const auto& session = m_drmEngine.GetSession(stream->m_info.GetCryptoSession().GetSessionId()); + if (session) + reader->SetDecrypter(session->decrypter, session->capabilities); + else + reader->SetDecrypter(nullptr, static_cast(0)); //! @todo: clenanup + + stream->SetReader(std::move(reader)); + + if (reprContainerType == ContainerType::TS) { - InitializeDRM(); + // With TS streams the elapsed time would be calculated incorrectly as during the tree refresh, + // nextSegment would be deleted by the FreeSegments/newsegments swap. Do this now before the tree refresh. + // Also, when reopening a stream (switching reps) the elapsed time would be incorrectly set until the + // second segment plays, now force a correct calculation at the start of the stream. + OnSegmentChanged(&stream->m_adStream); } - stream->m_isEncrypted = repr->GetPsshSetPos() != PSSHSET_POS_DEFAULT; + return true; } void CSession::EnableStream(CStream* stream, bool enable) @@ -898,40 +641,6 @@ void CSession::EnableStream(CStream* stream, bool enable) } } -bool SESSION::CSession::IsCDMSessionSecurePath(size_t index) -{ - if (index >= m_cdmSessions.size()) - { - LOG::LogF(LOGERROR, "No CDM session at index %u", index); - return false; - } - - return (m_cdmSessions[index].m_decrypterCaps.flags & - DRM::DecrypterCapabilites::SSD_SECURE_PATH) != 0; -} - -std::string SESSION::CSession::GetCDMSession(unsigned int index) -{ - if (index >= m_cdmSessions.size()) - { - LOG::LogF(LOGERROR, "No CDM session at index %u", index); - return {}; - } - return m_cdmSessions[index].m_sessionId; -} - -std::shared_ptr SESSION::CSession::GetSingleSampleDecryptor( - unsigned int index) const -{ - if (index >= m_cdmSessions.size()) - { - LOG::LogF(LOGERROR, "Index %u out of range, cannot get single sample decrypter", index); - return nullptr; - } - - return m_cdmSessions[index].m_cencSingleSampleDecrypter; -} - uint64_t SESSION::CSession::PTSToElapsed(uint64_t pts) { if (m_timingStream) @@ -1189,7 +898,7 @@ bool SESSION::CSession::SeekTime(double seekTime, unsigned int streamId, bool pr double destTime{static_cast(PTSToElapsed(streamReader->PTS())) / STREAM_TIME_BASE}; LOG::Log(LOGINFO, - "Seek time %0.1lf for stream: %u (physical index %u) continues at %0.1lf " + "Seek time %0.1lf for stream: %i (physical index %u) continues at %0.1lf " "(PTS: %llu)", seekTime, streamReader->GetStreamId(), stream->m_info.GetPhysicalIndex(), destTime, streamReader->PTS()); @@ -1255,51 +964,27 @@ void SESSION::CSession::OnStreamChange(adaptive::AdaptiveStream* adStream) bool SESSION::CSession::OnGetStream(int streamid, kodi::addon::InputstreamInfo& info) { - CStream* stream(GetStream(streamid - GetPeriodId() * 1000)); - - if (stream) + if (m_drmEngine.GetStatus() == DRM::EngineStatus::DRM_NOT_SUPPORTED || + m_drmEngine.GetStatus() == DRM::EngineStatus::DECRYPTER_ERROR) { - const uint16_t psshSetPos = stream->m_adStream.getRepresentation()->m_psshSetPos; - if (psshSetPos != PSSHSET_POS_DEFAULT || - stream->m_adStream.getPeriod()->GetEncryptionState() == EncryptionState::NOT_SUPPORTED) - { - // NOTE "psshSetPos < m_cdmSessions.size()" CONDITION: - // is required because the GetNextRepresentation method called by AdaptiveStream "ensure segment" method - // can change stream quality that download new manifests, parsing new manifests may add new PSSH's, - // so there will be a higher psshSetPos value than m_cdmSessions - // this happens for HLS case because the m_cdmSessions is updated with OpenStream. - // On DEMUX_SPECIALID_STREAMCHANGE event Kodi query all streams by calling GetStream in advance - // than OpenStream so there is a higher psshSetPos value and GetSingleSampleDecryptor cannot get a ptr - if (psshSetPos < m_cdmSessions.size() && !GetSingleSampleDecryptor(psshSetPos)) - { - // If the stream is protected with a unsupported DRM, we have to stop the playback, - // since there are no ways to stop playback when Kodi request streams - // we are forced to delete all CStream's here, so that when demux reader will starts - // will have no data to process, and so stop the playback - // (other streams may have been requested/opened before this one) - LOG::Log(LOGERROR, "GetStream(%d): Decrypter for the stream not found", streamid); - DeleteStreams(); - return false; - } - } + // If the stream is protected with a unsupported DRM, we have to stop the playback, + // since there are no ways to stop playback when Kodi request streams + // we are forced to delete all CStream's here, so that when demux reader will starts + // will have no data to process, and so stop the playback + // (other streams may have been requested/opened before this one) + DeleteStreams(); + return false; + } + else + { + CStream* stream = GetStream(streamid - GetPeriodId() * 1000); + if (!stream) + return false; info = stream->m_info; - return true; } - return false; -} - -std::shared_ptr SESSION::CSession::GetSingleSampleDecrypter( - std::string sessionId) -{ - for (std::vector::iterator b(m_cdmSessions.begin() + 1), e(m_cdmSessions.end()); - b != e; ++b) - { - if (sessionId == b->m_sessionId) - return b->m_cencSingleSampleDecrypter; - } - return nullptr; + return true; } uint32_t SESSION::CSession::GetIncludedStreamMask() const @@ -1439,99 +1124,3 @@ bool SESSION::CSession::SeekChapter(int ch) } return false; } - -void SESSION::CSession::ExtractStreamProtectionData(const PLAYLIST::CPeriod::PSSHSet& psshSet, - std::string& defaultKid, - std::vector& initData, - const std::vector& keySystems) -{ - auto initialRepr = m_reprChooser->GetRepresentation(psshSet.adaptation_set_); - - if (initialRepr->GetContainerType() != ContainerType::MP4) - return; - - LOG::LogF(LOGDEBUG, "Parse protection data from stream"); - CStream stream{m_adaptiveTree, psshSet.adaptation_set_, initialRepr}; - - stream.m_isEnabled = true; - stream.m_adStream.start_stream(); - stream.SetAdByteStream(std::make_unique(&stream.m_adStream)); - stream.SetStreamFile(std::make_unique(*stream.GetAdByteStream(), - AP4_DefaultAtomFactory::Instance_, true)); - AP4_Movie* movie{stream.GetStreamFile()->GetMovie()}; - if (!movie) - { - LOG::LogF(LOGERROR, "No MOOV atom in stream"); - stream.Disable(); - return; - } - - AP4_Track* track = - movie->GetTrack(static_cast(stream.m_adStream.GetTrackType())); - - if (track) // Try extract the default KID from tenc / piff mp4 box - { - AP4_ProtectedSampleDescription* protSampleDesc = - static_cast(track->GetSampleDescription(0)); - - if (protSampleDesc) - { - AP4_ProtectionSchemeInfo* psi = protSampleDesc->GetSchemeInfo(); - if (psi) - { - AP4_ContainerAtom* schi = protSampleDesc->GetSchemeInfo()->GetSchiAtom(); - if (schi) - { - AP4_TencAtom* tenc = - AP4_DYNAMIC_CAST(AP4_TencAtom, schi->GetChild(AP4_ATOM_TYPE_TENC, 0)); - if (tenc) - { - defaultKid = STRING::ToHexadecimal(tenc->GetDefaultKid(), 16); - } - else - { - AP4_PiffTrackEncryptionAtom* piff = - AP4_DYNAMIC_CAST(AP4_PiffTrackEncryptionAtom, - schi->GetChild(AP4_UUID_PIFF_TRACK_ENCRYPTION_ATOM, 0)); - if (piff) - { - defaultKid = STRING::ToHexadecimal(piff->GetDefaultKid(), 16); - } - } - } - } - } - } - - if (initData.empty() || defaultKid.empty()) - { - const std::vector systemIds = DRM::UrnsToSystemIds(keySystems); - AP4_Array& pssh{movie->GetPsshAtoms()}; - - for (unsigned int i = 0; i < pssh.ItemCount(); ++i) - { - AP4_PsshAtom& psshAtom = pssh[i]; - - std::string systemId = STRING::ToHexadecimal(psshAtom.GetSystemId(), 16); - - // Check if the system id is supported - if (std::find(systemIds.cbegin(), systemIds.cend(), systemId) != systemIds.cend()) - { - const AP4_DataBuffer& dataBuf = psshAtom.GetData(); - const std::vector psshData{dataBuf.GetData(), - dataBuf.GetData() + dataBuf.GetDataSize()}; - - initData = DRM::PSSH::Make(psshAtom.GetSystemId(), {}, psshData); - - if (psshAtom.GetKid(0)) - { - defaultKid = STRING::ToHexadecimal(pssh[i].GetKid(0), 16); - } - - break; - } - } - } - - stream.Disable(); -} diff --git a/src/Session.h b/src/Session.h index ad9decd87..1cf181b08 100644 --- a/src/Session.h +++ b/src/Session.h @@ -11,6 +11,7 @@ #include "Stream.h" #include "common/AdaptiveStream.h" #include "common/AdaptiveTree.h" +#include "decrypters/DrmEngine.h" #include "decrypters/IDecrypter.h" #if defined(ANDROID) @@ -40,21 +41,7 @@ class ATTR_DLL_LOCAL CSession : public adaptive::AdaptiveStreamObserver /* * \brief Check HDCP parameters to remove unplayable representations */ - void CheckHDCP(); - - /*! \brief Pre-Initialize the DRM - * \param challengeB64 [OUT] Provide the challenge data as base64 - * \param sessionId [OUT] Provide the session ID - * \param isSessionOpened [OUT] Will be true if the DRM session has been opened - * \return True if has success, false otherwise - */ - bool PreInitializeDRM(std::string& challengeB64, std::string& sessionId, bool& isSessionOpened); - - /*! \brief Initialize the DRM - * \param addDefaultKID Set True to add the default KID to the first session - * \return True if has success, false otherwise - */ - bool InitializeDRM(bool addDefaultKID = false); + //void CheckHDCP(); /*! \brief Initialize adaptive tree period * \param isSessionOpened Set True to kept and re-use the DRM session opened, @@ -93,7 +80,7 @@ class ATTR_DLL_LOCAL CSession : public adaptive::AdaptiveStreamObserver * \brief Update stream's InputstreamInfo * \param stream The stream to prepare */ - void PrepareStream(CStream* stream); + bool PrepareStream(CStream* stream, uint64_t startPts); /*! \brief Get a stream by index (starting at 1) * \param sid The one-indexed stream id @@ -115,50 +102,11 @@ class ATTR_DLL_LOCAL CSession : public adaptive::AdaptiveStreamObserver */ unsigned int GetStreamCount() const { return static_cast(m_streams.size()); } - /*! - * \brief Determines if the CDM session at specified index require Secure Path (TEE). - * \return True if Secure Path is required, otherwise false. - */ - bool IsCDMSessionSecurePath(size_t index); - - /*! \brief Get a session string (session id) by index from the cdm sessions - * \param index The index (psshSet number) of the cdm session - * \return The session string - */ - std::string GetCDMSession(unsigned int index); - /*! \brief Get the media type mask * \return The media type mask */ uint8_t GetMediaTypeMask() const { return m_mediaTypeMask; } - /*! \brief Get a single sample decrypter by index from the cdm sessions - * \param index The index (psshSet number) of the cdm session - * \return The single sample decrypter - */ - std::shared_ptr GetSingleSampleDecryptor( - unsigned int index) const; - - /*! \brief Get the decrypter (DRM lib) - * \return The decrypter - */ - DRM::IDecrypter* GetDecrypter() { return m_decrypter.get(); } - - /*! \brief Get a single sample decrypter matching the session id provided - * \param sessionId The session id string to match - * \return The single sample decrypter - */ - std::shared_ptr GetSingleSampleDecrypter(std::string sessionId); - - /*! \brief Get decrypter capabilities for a single sample decrypter - * \param index The index (psshSet number) of the cdm session - * \return The single sample decrypter capabilities - */ - const DRM::DecrypterCapabilites& GetDecrypterCaps(unsigned int index) const - { - return m_cdmSessions[index].m_decrypterCaps; - }; - /*! \brief Get the total time in ms of the stream * \return The total time in ms of the stream */ @@ -324,13 +272,9 @@ class ATTR_DLL_LOCAL CSession : public adaptive::AdaptiveStreamObserver */ bool OnGetStream(int streamid, kodi::addon::InputstreamInfo& info); -protected: - /*! \brief Check for and load decrypter module matching the supplied key system - * \param key_system [OUT] Will be assigned to if a decrypter is found matching - * the set license type - */ - void SetSupportedDecrypterURN(std::vector& keySystems); + const DRM::CDRMEngine& GetDRMEngine() const { return m_drmEngine; } +protected: /*! \brief Destroy all CencSingleSampleDecrypter instances */ void DisposeSampleDecrypter(); @@ -339,21 +283,8 @@ class ATTR_DLL_LOCAL CSession : public adaptive::AdaptiveStreamObserver */ void DisposeDecrypter(); - void ExtractStreamProtectionData(const PLAYLIST::CPeriod::PSSHSet& psshSet, - std::string& defaultKid, - std::vector& initData, - const std::vector& keySystems); - private: - std::shared_ptr m_decrypter; - - struct CCdmSession - { - DRM::DecrypterCapabilites m_decrypterCaps; - std::shared_ptr m_cencSingleSampleDecrypter; - std::string m_sessionId; - }; - std::vector m_cdmSessions; + DRM::CDRMEngine m_drmEngine; adaptive::AdaptiveTree* m_adaptiveTree{nullptr}; CHOOSER::IRepresentationChooser* m_reprChooser{nullptr}; diff --git a/src/Stream.cpp b/src/Stream.cpp index 2f3994436..f79ea05a0 100644 --- a/src/Stream.cpp +++ b/src/Stream.cpp @@ -26,7 +26,6 @@ void CStream::Disable() Reset(); m_isEnabled = false; - m_isEncrypted = false; } } diff --git a/src/Stream.h b/src/Stream.h index ad2a15e1e..305308f92 100644 --- a/src/Stream.h +++ b/src/Stream.h @@ -23,14 +23,11 @@ class ATTR_DLL_LOCAL CStream CStream(adaptive::AdaptiveTree* tree, PLAYLIST::CAdaptationSet* adp, PLAYLIST::CRepresentation* initialRepr) - : m_isEnabled{false}, - m_isEncrypted{false}, - m_mainId{0}, - m_adStream{tree, adp, initialRepr}, - m_isValid{true} {}; - + : m_isEnabled{false}, m_mainId{0}, m_adStream{tree, adp, initialRepr}, m_isValid{true} + { + } - ~CStream() { Disable(); }; + ~CStream() { Disable(); } /*! * \brief Stop/disable the AdaptiveStream and reset @@ -82,7 +79,6 @@ class ATTR_DLL_LOCAL CStream } bool m_isEnabled; - bool m_isEncrypted; uint16_t m_mainId; adaptive::AdaptiveStream m_adStream; kodi::addon::InputstreamInfo m_info; diff --git a/src/common/AdaptiveDecrypter.h b/src/common/AdaptiveDecrypter.h index 27e741409..19192a2f3 100644 --- a/src/common/AdaptiveDecrypter.h +++ b/src/common/AdaptiveDecrypter.h @@ -55,5 +55,5 @@ class Adaptive_CencSingleSampleDecrypter : public AP4_CencSingleSampleDecrypter virtual AP4_UI32 AddPool() { return 0; } virtual void RemovePool(AP4_UI32 poolId) {} - virtual std::string GetSessionId() { return {}; } + virtual std::string GetSessionId() = 0; }; diff --git a/src/common/AdaptiveTree.cpp b/src/common/AdaptiveTree.cpp index 2699cc285..b14b49582 100644 --- a/src/common/AdaptiveTree.cpp +++ b/src/common/AdaptiveTree.cpp @@ -40,7 +40,6 @@ namespace adaptive m_manifestParams = left.m_manifestParams; m_manifestHeaders = left.m_manifestHeaders; m_settings = left.m_settings; - m_supportedKeySystems = left.m_supportedKeySystems; m_pathSaveManifest = left.m_pathSaveManifest; stream_start_ = left.stream_start_; @@ -49,11 +48,9 @@ namespace adaptive } void AdaptiveTree::Configure(CHOOSER::IRepresentationChooser* reprChooser, - std::vector supportedKeySystems, std::string_view manifestUpdParams) { m_reprChooser = reprChooser; - m_supportedKeySystems = supportedKeySystems; auto srvBroker = CSrvBroker::GetInstance(); diff --git a/src/common/AdaptiveTree.h b/src/common/AdaptiveTree.h index a9e716135..b3913dde5 100644 --- a/src/common/AdaptiveTree.h +++ b/src/common/AdaptiveTree.h @@ -73,7 +73,7 @@ class ATTR_DLL_LOCAL AdaptiveTree uint64_t available_time_{0}; // in ms uint64_t m_liveDelay{0}; // Apply a delay in seconds from the live edge - std::vector m_supportedKeySystems; + std::vector m_supportedKeySystems; // REMOVE ME std::string location_; CryptoMode m_cryptoMode{CryptoMode::NONE}; @@ -86,11 +86,10 @@ class ATTR_DLL_LOCAL AdaptiveTree /*! * \brief Configure the adaptive tree. - * \param kodiProps The Kodi properties + * \param reprChooser The representation chooser * \param manifestUpdParams Parameters to be add to manifest request url, depends on manifest implementation */ virtual void Configure(CHOOSER::IRepresentationChooser* reprChooser, - std::vector supportedKeySystems, std::string_view manifestUpdParams); /* diff --git a/src/common/Representation.h b/src/common/Representation.h index 9ca61ff70..0504e97c4 100644 --- a/src/common/Representation.h +++ b/src/common/Representation.h @@ -15,6 +15,7 @@ #include "SegmentBase.h" #include "SegTemplate.h" #include "SegmentList.h" +#include "decrypters/DrmEngineDefines.h" #ifdef INPUTSTREAM_TEST_BUILD #include "test/KodiStubs.h" @@ -158,6 +159,9 @@ class ATTR_DLL_LOCAL CRepresentation : public CCommonSegAttribs, public CCommonA bool IsIncludedStream() const { return m_isIncludedStream; } void SetIsIncludedStream(bool isIncludedStream) { m_isIncludedStream = isIncludedStream; } + std::vector& DrmInfos() { return m_drmInfo; } + void AddDrmInfo(DRM::DRMInfo drmInfo) { m_drmInfo.emplace_back(drmInfo); } + void CopyHLSData(const CRepresentation* other); static bool CompareBandwidth(std::unique_ptr& left, @@ -239,6 +243,7 @@ class ATTR_DLL_LOCAL CRepresentation : public CCommonSegAttribs, public CCommonA bool m_isWaitForSegment{false}; bool m_isIncludedStream{false}; + std::vector m_drmInfo; }; } // namespace PLAYLIST diff --git a/src/decrypters/CMakeLists.txt b/src/decrypters/CMakeLists.txt index 50ef58ecb..923c308ca 100644 --- a/src/decrypters/CMakeLists.txt +++ b/src/decrypters/CMakeLists.txt @@ -1,4 +1,5 @@ set(SOURCES + DrmEngine.cpp DrmFactory.cpp Helpers.cpp HelperPr.cpp @@ -6,6 +7,8 @@ set(SOURCES ) set(HEADERS + DrmEngine.h + DrmEngineDefines.h DrmFactory.h Helpers.h HelperPr.h diff --git a/src/decrypters/DrmEngine.cpp b/src/decrypters/DrmEngine.cpp new file mode 100644 index 000000000..cf2ee4536 --- /dev/null +++ b/src/decrypters/DrmEngine.cpp @@ -0,0 +1,636 @@ +/* + * Copyright (C) 2024 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "DrmEngine.h" + +#include "CompKodiProps.h" +#include "CompResources.h" +#include "CompSettings.h" +#include "DrmFactory.h" +#include "Helpers.h" +#include "SrvBroker.h" +#include "Stream.h" +#include "common/AdaptationSet.h" +#include "common/AdaptiveDecrypter.h" +#include "common/Representation.h" +#include "utils/Base64Utils.h" +#include "utils/StringUtils.h" +#include "utils/log.h" + +#include +#include +#include + +using namespace DRM; +using namespace UTILS; + +namespace +{ +STREAM_CRYPTO_KEY_SYSTEM KSToCryptoKeySystem(std::string_view keySystem) +{ + if (keySystem == DRM::KS_WIDEVINE) + return STREAM_CRYPTO_KEY_SYSTEM_WIDEVINE; + else if (keySystem == DRM::KS_WISEPLAY) + return STREAM_CRYPTO_KEY_SYSTEM_WISEPLAY; + else if (keySystem == DRM::KS_PLAYREADY) + return STREAM_CRYPTO_KEY_SYSTEM_PLAYREADY; + else if (keySystem == DRM::KS_CLEARKEY) + return STREAM_CRYPTO_KEY_SYSTEM_CLEARKEY; + else + return STREAM_CRYPTO_KEY_SYSTEM_NONE; +} + +std::shared_ptr CreateDRM(std::string_view keySystem) +{ + std::string decrypterPath = CSrvBroker::GetSettings().GetDecrypterPath(); + if (decrypterPath.empty()) + { + LOG::Log(LOGWARNING, + "Cannot create the decrypter, the decrypter path is not set in the add-on settings"); + return nullptr; + } + + std::shared_ptr drm; + + drm = DRM::FACTORY::GetDecrypter(KSToCryptoKeySystem(keySystem)); + + if (!drm) + { + LOG::LogF(LOGERROR, "Unable to create the DRM decrypter"); + return nullptr; + } + + drm->SetLibraryPath(decrypterPath); + + if (!drm->Initialize()) + { + LOG::Log(LOGERROR, "The DRM decrypter cannot be initialized"); + return nullptr; + } + + return drm; +} + +} // unnamed namespace + +void DRM::CDRMEngine::Initialize() +{ + // Build the list of the supported DRM's by KeySystem, + // the list depends on the operative system used, + // the order of addition also defines the DRM priority (always prefer real DRM's over ClearKey) + // in future could be improved to take in account also the DRM capability on the target system + + // Widevine currently always preferred as first because on android can reach 4k on L1 devices + m_supportedKs.emplace_back(KS_WIDEVINE); +#if ANDROID + m_supportedKs.emplace_back(KS_PLAYREADY); + m_supportedKs.emplace_back(KS_WISEPLAY); +#endif + m_supportedKs.emplace_back(KS_CLEARKEY); + + // Sort key systems based on priorities + const auto& kodiProps = CSrvBroker::GetKodiProps(); + + for (auto& [ks, cfg] : kodiProps.GetDrmConfigs()) + { + if (cfg.priority.has_value() && *cfg.priority != 0) + { + auto it = std::find(m_supportedKs.begin(), m_supportedKs.end(), ks); + if (it != m_supportedKs.end()) + { + m_supportedKs.erase(it); + + size_t index = *cfg.priority - 1; + if (index >= m_supportedKs.size()) + index = m_supportedKs.size() - 1; + + m_supportedKs.insert(m_supportedKs.begin() + index, ks); + } + } + } +} + +bool DRM::CDRMEngine::PreInitializeDRM(DRMSession& session) +{ + auto& kodiProps = CSrvBroker::GetKodiProps(); + std::string_view preInitData; + + // Pre-initialize the DRM is available for Widevine only. + // Since the manifest will be downloaded later its assumed that + // the manifest support the DRM and so the priority must be set to 1. + + for (auto& [ks, cfg] : kodiProps.GetDrmConfigs()) + { + if (ks == KS_WIDEVINE && + std::find(m_supportedKs.cbegin(), m_supportedKs.cend(), ks) != m_supportedKs.cend()) + { + if (!cfg.priority.has_value() || *cfg.priority != 1) + { + LOG::LogF(LOGERROR, "Cannot preinitialize the DRM, the DRM priority must be set to 1."); + continue; + } + + preInitData = cfg.preInitData; + break; + } + } + + if (preInitData.empty()) + return false; + + std::vector initData; + std::vector kidData; + // Parse the init data (PSSH, KID) + size_t posSplitter = preInitData.find("|"); + if (posSplitter != std::string::npos) + { + initData = BASE64::Decode(preInitData.substr(0, posSplitter)); + kidData = BASE64::Decode(preInitData.substr(posSplitter + 1)); + } + + if (initData.empty() || kidData.empty()) + { + LOG::LogF(LOGERROR, "Invalid DRM pre-init data, must be as: {PSSH as base64}|{KID as base64}"); + return false; + } + + m_keySystem = KS_WIDEVINE; + + auto drm = CreateDRM(m_keySystem); + if (!drm) + return false; + + DRM::Config drmCfg = CreateDRMConfig(m_keySystem, kodiProps.GetDrmConfig(m_keySystem)); + + if (!drm->OpenDRMSystem(drmCfg)) + { + LOG::LogF(LOGERROR, "Failed to open DRM"); + return false; + } + + LOG::LogF(LOGDEBUG, "Initializing session with KID: %s", STRING::ToHexadecimal(kidData).c_str()); + + auto dec = drm->CreateSingleSampleDecrypter(initData, kidData, "", true, CryptoMode::AES_CTR); + + if (!dec) + { + LOG::LogF(LOGERROR, "Failed to initialize the decrypter"); + return false; + } + + m_drms.emplace(m_keySystem, drm); + + session.id = dec->GetSessionId(); + session.challenge = drm->GetChallengeB64Data(dec); + session.drm = drm; + session.decrypter = dec; +#ifndef ANDROID + // On android is not possible add the default KID key used to open DRM + // then dont add this DRM session, since must be reinitialized + m_sessions.emplace_back(session); +#endif + + m_isPreinitialized = true; + + return true; +} + +bool DRM::CDRMEngine::InitializeSession(std::vector drmInfos, + DRM::DRMMediaType mediaType, + std::optional isForceSecureDecoder, + kodi::addon::InputstreamInfo& streamInfo, + PLAYLIST::CRepresentation* repr, + PLAYLIST::CAdaptationSet* adp) +{ + if (drmInfos.empty()) + return false; + + LOG::Log(LOGDEBUG, "Initialize crypto session"); + + // ck if no kid but there is a init segment, we can download init segment to get kid + ConfigureClearKey(drmInfos); + + if (!SelectDRM(drmInfos)) + return false; + + // Find a compatible DRM info + auto itDrmInfo = std::find_if(drmInfos.begin(), drmInfos.end(), [&](const DRMInfo& info) { + return info.keySystem == m_keySystem;}); + + if (itDrmInfo == drmInfos.end()) + { + LOG::LogF(LOGERROR, "No supported DRM found for the stream"); + m_status = EngineStatus::DRM_NOT_SUPPORTED; + return false; + } + + DRM::DRMInfo& drmInfo = *itDrmInfo; + const auto& drmPropCfg = CSrvBroker::GetKodiProps().GetDrmConfig(drmInfo.keySystem); + + // Set custom init data PSSH provided from property, + // can allow to initialize a DRM that could be also not specified + // as supported in the manifest (e.g. missing DASH ContentProtection tags) + if (!drmPropCfg.initData.empty()) + { + drmInfo.initData.clear(); + LOG::Log(LOGDEBUG, "Custom init PSSH provided by the \"license\" property"); + std::vector customInitData = BASE64::Decode(drmPropCfg.initData); + + if (DRM::IsValidPsshHeader(customInitData)) + { + drmInfo.initData = customInitData; + } + else if (m_keySystem == DRM::KS_WIDEVINE) // Try to create a PSSH box, KID should be provided by manifest + { + drmInfo.initData = + DRM::PSSH::MakeWidevine({DRM::ConvertKidStrToBytes(drmInfo.defaultKid)}, customInitData); + } + + if (drmInfo.initData.empty()) + LOG::LogF(LOGERROR, "The custom init PSSH has not valid data"); + } + + // If no KID, but init data, extract the KID from init data + if (!drmInfo.initData.empty() && drmInfo.defaultKid.empty() && + DRM::IsValidPsshHeader(drmInfo.initData)) + { + DRM::PSSH parser; + if (parser.Parse(drmInfo.initData) && !parser.GetKeyIds().empty()) + { + LOG::Log(LOGDEBUG, "Default KID parsed from init data"); + drmInfo.defaultKid = STRING::ToHexadecimal(parser.GetKeyIds()[0]); + } + } + + if ((drmInfo.initData.empty() && m_keySystem != DRM::KS_CLEARKEY) || drmInfo.defaultKid.empty()) + { + // Try extract the PSSH/KID from the stream, as last resort because its expensive + ExtractStreamProtectionData(repr, adp, drmInfo); + } + + // Create the DRM decrypter + if (!STRING::KeyExists(m_drms, m_keySystem)) + { + //! @todo: to test a way to preinitialize DRM when manifest is downloaded/parsed + //! in the hoping to have a more smoother playback transition + //! this can be tested with multiperiods video where first period is unencrypted and second one DRM crypted + auto drm = CreateDRM(m_keySystem); + if (!drm) + return false; + m_drms.emplace(m_keySystem, drm); + } + + if (m_isPreinitialized && m_sessions.size() == 1) + { + // Widevine only, when the CDM is preinitialized for non-android systems + // the session has been created with a custom PSSH/KID and it should + // be assumed that there is a single session for all streams. + // In order to reuse this session is needed to add the current KID. + const std::vector kid = DRM::ConvertKidStrToBytes(drmInfo.defaultKid); + if (!m_sessions[0].drm->HasLicenseKey(m_sessions[0].decrypter, kid)) + { + m_sessions[0].decrypter->AddKeyId(kid); + m_sessions[0].decrypter->SetDefaultKeyId(kid); + } + } + + // Find if there is an already opened DRM session + // with the same init data and mediatype + DRMSession* session{nullptr}; + for (DRMSession& s : m_sessions) + { + // Check for media type because its recommended to create a separate session for audio/video + // this avoids possible decryption problems that usually affect android devices + // causing side effects such as corrupted/pixellated video + if (s.mediaType == mediaType && s.initData == drmInfo.initData) + { + const std::vector kid = DRM::ConvertKidStrToBytes(drmInfo.defaultKid); + if (s.drm->HasLicenseKey(s.decrypter, kid)) + { + session = &s; + break; + } + } + } + + // No reausable DRM session, create a new one + if (!session) + { + const std::vector kid = DRM::ConvertKidStrToBytes(drmInfo.defaultKid); + + DRMSession newSes; + newSes.drm = m_drms[m_keySystem]; + newSes.mediaType = mediaType; + newSes.initData = drmInfo.initData; + + if (!newSes.drm->IsInitialised()) + { + DRM::Config drmCfg = DRM::CreateDRMConfig(m_keySystem, drmPropCfg); + if (!newSes.drm->OpenDRMSystem(drmCfg)) + { + LOG::LogF(LOGERROR, "Failed to open DRM"); + return false; + } + } + + newSes.decrypter = newSes.drm->CreateSingleSampleDecrypter( + drmInfo.initData, kid, drmInfo.licenseServerUri, false, + drmInfo.cryptoMode == CryptoMode::NONE ? CryptoMode::AES_CTR : drmInfo.cryptoMode); + if (!newSes.decrypter) + { + LOG::Log(LOGERROR, "Failed to initialize the decrypter"); + m_status = EngineStatus::DECRYPTER_ERROR; + return false; + } + + newSes.id = newSes.decrypter->GetSessionId(); + + uint32_t dMedia = mediaType == DRM::DRMMediaType::VIDEO ? DecrypterCapabilites::SSD_MEDIA_VIDEO + : DecrypterCapabilites::SSD_MEDIA_AUDIO; + newSes.drm->GetCapabilities(newSes.decrypter, kid, dMedia, newSes.capabilities); + + if (newSes.capabilities.flags & DRM::DecrypterCapabilites::SSD_INVALID) + { + m_status = EngineStatus::DECRYPTER_ERROR; + return false; + } + else if (newSes.capabilities.flags & DRM::DecrypterCapabilites::SSD_SECURE_PATH) + { + // Allow to disable the secure decoder + bool disableSecureDecoder = CSrvBroker::GetSettings().IsDisableSecureDecoder(); + // but, DRM config can override it + if (drmPropCfg.isSecureDecoderEnabled.has_value()) + disableSecureDecoder = !*drmPropCfg.isSecureDecoderEnabled; + // but, external config can override all others (e.g. manifest) + if (isForceSecureDecoder.has_value()) + disableSecureDecoder = !*isForceSecureDecoder; + if (disableSecureDecoder) + { + LOG::Log(LOGDEBUG, "DRM configured with secure decoder disabled"); + newSes.capabilities.flags &= ~DRM::DecrypterCapabilites::SSD_SECURE_DECODER; + } + } + + + //alternative solution to avoid depends from repr chooser, example chooser can query the drm engine <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + //m_reprChooser->SetSecureSession(isSecureVideoSession); + + // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>reimplement CheckHDCP to skip the stream instead of delete stream + // this can be achieved by add a drmstatus to the stream repr, that in future must be moved on a new stream/track class + + m_sessions.emplace_back(newSes); + session = &m_sessions.back(); + LOG::Log(LOGDEBUG, "Initialized new DRM session (id: %s)", session->id.c_str()); + } + else + LOG::Log(LOGDEBUG, "Reused existing DRM session (id: %s)", session->id.c_str()); + + // Create crypto session + kodi::addon::StreamCryptoSession cryptoSession; + + cryptoSession.SetSessionId(session->id); + // Set the key system will enable the crypto session to kodi decoders (e.g. ffmpeg) + if (session->capabilities.flags & DRM::DecrypterCapabilites::SSD_SECURE_PATH) + cryptoSession.SetKeySystem(KSToCryptoKeySystem(m_keySystem)); + + if (session->capabilities.flags & DRM::DecrypterCapabilites::SSD_SECURE_PATH && + session->capabilities.flags & DRM::DecrypterCapabilites::SSD_SUPPORTS_DECODING) + { + LOG::Log(LOGDEBUG, "Secure crypto session enabled (id: %s)", session->id.c_str()); + streamInfo.SetFeatures(INPUTSTREAM_FEATURE_DECODE); + } + else + streamInfo.SetFeatures(INPUTSTREAM_FEATURE_NONE); + + if (session->capabilities.flags & DRM::DecrypterCapabilites::SSD_SECURE_PATH && + session->capabilities.flags & DRM::DecrypterCapabilites::SSD_SECURE_DECODER) + { + // Enable the ISA VideoCodecAdaptive decoder + LOG::Log(LOGDEBUG, "Secure crypto add-on decoder enabled (id: %s)", session->id.c_str()); + cryptoSession.SetFlags(STREAM_CRYPTO_FLAG_SECURE_DECODER); + } + else + cryptoSession.SetFlags(STREAM_CRYPTO_FLAG_NONE); + + streamInfo.SetCryptoSession(cryptoSession); + + return true; +} + +const DRMSession* DRM::CDRMEngine::GetSession(const std::string& id) const +{ + for (const auto& session : m_sessions) + { + if (session.id == id) + return &session; + } + return nullptr; +} + +bool DRM::CDRMEngine::ConfigureClearKey(std::vector& drmInfos) +{ + const auto& drmCfgs = CSrvBroker::GetKodiProps().GetDrmConfigs(); + const ADP::KODI_PROPS::DrmCfg* drmCfg{nullptr}; + + // ClearKey can be configured to replace DRM infos + // only when have priority 1 + for (auto& [ks, cfg] : drmCfgs) + { + if (ks == KS_CLEARKEY) + { + if (!cfg.priority.has_value() || *cfg.priority != 1) + { + return false; + } + + drmCfg = &cfg; + break; + } + } + + if (!drmCfg || drmCfg->license.keys.empty() || drmInfos.empty()) + return false; + + // Get info from any drm info item, since should be the same + const CryptoMode cryptoMode = drmInfos[0].cryptoMode; + const std::string defaultKid = drmInfos[0].defaultKid; + + // Currently ClearKey works with CENC protection scheme only + if (cryptoMode != CryptoMode::AES_CTR) + { + LOG::LogF(LOGERROR, "The stream use an unsupported protection scheme for ClearKey DRM"); + return false; + } + + drmInfos.clear(); + + std::string licenseUri; + + if (!drmCfg->license.keys.empty()) + { + // Create license uri with jwkSets + rapidjson::Document jDoc; + jDoc.SetObject(); + auto& allocator = jDoc.GetAllocator(); + + rapidjson::Value jwkSets{rapidjson::kArrayType}; + + for (auto& [kid, key] : drmCfg->license.keys) + { + rapidjson::Value jwkSet{rapidjson::kObjectType}; + jwkSet.AddMember("k", rapidjson::Value(BASE64::Encode(key).c_str(), allocator), allocator); + jwkSet.AddMember("kid", rapidjson::Value(BASE64::Encode(kid).c_str(), allocator), allocator); + jwkSet.AddMember("kty", "oct", allocator); + jwkSets.PushBack(jwkSet, allocator); + } + + jDoc.AddMember("keys", jwkSets, allocator); + jDoc.AddMember("type", "temporary", allocator); + + rapidjson::StringBuffer buffer; + rapidjson::Writer writer{buffer}; + jDoc.Accept(writer); + + licenseUri = + "data:application/json;base64," + BASE64::Encode(buffer.GetString(), buffer.GetSize()); + } + else if (!drmCfg->license.serverUrl.empty()) + licenseUri = drmCfg->license.serverUrl; + + DRM::DRMInfo drmInfo; + drmInfo.keySystem = KS_CLEARKEY; + drmInfo.cryptoMode = cryptoMode; + // drmInfo.initData = DRM::MakeClearKeyInitData() + drmInfo.defaultKid = defaultKid; + drmInfo.licenseServerUri = licenseUri; + + drmInfos.emplace_back(drmInfo); + + return true; +} + +bool DRM::CDRMEngine::SelectDRM(std::vector& drmInfos) +{ + if (!m_keySystem.empty()) + return true; + + // Iterate supported DRM Key System's to find a match with the drmInfo's, + // the supported DRM's are ordered by priority + // the lower index have the higher priority + for (const std::string& ks : m_supportedKs) + { + auto itDrmInfo = std::find_if(drmInfos.begin(), drmInfos.end(), + [&](const DRMInfo& info) { return info.keySystem == ks; }); + + if (itDrmInfo != drmInfos.end()) + { + m_keySystem = ks; + break; + } + } + + return !m_keySystem.empty(); +} + +//! @todo: to remove requirements for CRepresentation CAdaptationSet vars +//! see also todo comment below +void DRM::CDRMEngine::ExtractStreamProtectionData(PLAYLIST::CRepresentation* repr, + PLAYLIST::CAdaptationSet* adp, + DRM::DRMInfo& drmInfo) +{ + if (repr->GetContainerType() != PLAYLIST::ContainerType::MP4) + return; + + LOG::LogF(LOGDEBUG, "Parse MP4 protection data from stream"); + + //! @todo: AdaptiveTree* const_cast its not good thing to do, it should be removed + //! with a code rework, for example by reusing the CStream created by OpenStream + //! and/or maybe create a way to avoid involve DRMEngine with CStream directly + SESSION::CStream stream{ + const_cast(&CSrvBroker::GetResources().GetTree()), adp, repr}; + + stream.m_isEnabled = true; + stream.m_adStream.start_stream(); + stream.SetAdByteStream(std::make_unique(&stream.m_adStream)); + stream.SetStreamFile(std::make_unique(*stream.GetAdByteStream(), + AP4_DefaultAtomFactory::Instance_, true)); + AP4_Movie* movie{stream.GetStreamFile()->GetMovie()}; + if (!movie) + { + LOG::LogF(LOGERROR, "No MOOV atom in stream"); + stream.Disable(); + return; + } + + AP4_Track* track = + movie->GetTrack(static_cast(stream.m_adStream.GetTrackType())); + + if (track) // Try extract the default KID from tenc / piff mp4 box + { + AP4_ProtectedSampleDescription* protSampleDesc = + static_cast(track->GetSampleDescription(0)); + + if (protSampleDesc) + { + AP4_ProtectionSchemeInfo* psi = protSampleDesc->GetSchemeInfo(); + if (psi) + { + AP4_ContainerAtom* schi = protSampleDesc->GetSchemeInfo()->GetSchiAtom(); + if (schi) + { + AP4_TencAtom* tenc = + AP4_DYNAMIC_CAST(AP4_TencAtom, schi->GetChild(AP4_ATOM_TYPE_TENC, 0)); + if (tenc) + { + drmInfo.defaultKid = STRING::ToHexadecimal(tenc->GetDefaultKid(), 16); + } + else + { + AP4_PiffTrackEncryptionAtom* piff = + AP4_DYNAMIC_CAST(AP4_PiffTrackEncryptionAtom, + schi->GetChild(AP4_UUID_PIFF_TRACK_ENCRYPTION_ATOM, 0)); + if (piff) + { + drmInfo.defaultKid = STRING::ToHexadecimal(piff->GetDefaultKid(), 16); + } + } + } + } + } + } + + if (drmInfo.initData.empty() || drmInfo.defaultKid.empty()) + { + AP4_Array& pssh{movie->GetPsshAtoms()}; + const uint8_t* currSystemId = DRM::KeySystemToUUID(m_keySystem); + + for (unsigned int i = 0; i < pssh.ItemCount(); ++i) + { + AP4_PsshAtom& psshAtom = pssh[i]; + + // Try find the system id + if (std::memcmp(psshAtom.GetSystemId(), currSystemId, 16) == 0) + { + const AP4_DataBuffer& dataBuf = psshAtom.GetData(); + const std::vector psshData{dataBuf.GetData(), + dataBuf.GetData() + dataBuf.GetDataSize()}; + + drmInfo.initData = DRM::PSSH::Make(psshAtom.GetSystemId(), {}, psshData); + + if (psshAtom.GetKid(0)) + { + drmInfo.defaultKid = STRING::ToHexadecimal(pssh[i].GetKid(0), 16); + } + + break; + } + } + } + + stream.Disable(); +} diff --git a/src/decrypters/DrmEngine.h b/src/decrypters/DrmEngine.h new file mode 100644 index 000000000..f1f323bb3 --- /dev/null +++ b/src/decrypters/DrmEngine.h @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "DrmEngineDefines.h" +#include "IDecrypter.h" +#include "utils/CryptoUtils.h" + +#ifdef INPUTSTREAM_TEST_BUILD +#include "test/KodiStubs.h" +#else +#include +#endif + +#include +#include +#include +#include + +// forwards +namespace PLAYLIST +{ +class CRepresentation; +class CAdaptationSet; +} // namespace PLAYLIST + +namespace DRM +{ + +class ATTR_DLL_LOCAL CDRMEngine +{ +public: + CDRMEngine() = default; + virtual ~CDRMEngine() = default; + + void Initialize(); + + // Return true only when DRM has been pre-initialized + bool PreInitializeDRM(DRMSession& session); + + bool InitializeSession(std::vector drmInfos, + DRM::DRMMediaType mediaType, + std::optional isForceSecureDecoder, + kodi::addon::InputstreamInfo& streamInfo, + PLAYLIST::CRepresentation* repr, + PLAYLIST::CAdaptationSet* adp); + + const std::vector& GetSessions() const { return m_sessions; } + const DRMSession* GetSession(const std::string& id) const; + + bool ConfigureClearKey(std::vector& drmInfos); + + // Determines if the decrypter and sessions are ready for the streams + EngineStatus GetStatus() const { return m_status; } + +private: + // \brief Select and set a DRM compatible with a DRM info + bool SelectDRM(std::vector& drmInfos); + + void ExtractStreamProtectionData(PLAYLIST::CRepresentation* repr, + PLAYLIST::CAdaptationSet* adp, + DRM::DRMInfo& drmInfo); + + std::vector m_supportedKs; // Supported key systems, the lower index has higher priority + std::string m_keySystem; // Choosen key system + std::map> m_drms; // KeySystem - DRM instance + std::vector m_sessions; + EngineStatus m_status{EngineStatus::NONE}; + bool m_isPreinitialized{false}; +}; + +} // namespace DRM diff --git a/src/decrypters/DrmEngineDefines.h b/src/decrypters/DrmEngineDefines.h new file mode 100644 index 000000000..9f963e3f4 --- /dev/null +++ b/src/decrypters/DrmEngineDefines.h @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2024 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "utils/CryptoUtils.h" + +#include +#include +#include +#include +#include +#include + +// forwards +class Adaptive_CencSingleSampleDecrypter; +namespace DRM +{ +class IDecrypter; +} + +namespace DRM +{ +struct DecrypterCapabilites +{ + static const uint32_t SSD_SUPPORTS_DECODING = 1; + static const uint32_t SSD_SECURE_PATH = 2; + static const uint32_t SSD_ANNEXB_REQUIRED = 4; + static const uint32_t SSD_HDCP_RESTRICTED = 8; + static const uint32_t SSD_SINGLE_DECRYPT = 16; + static const uint32_t SSD_SECURE_DECODER = 32; + static const uint32_t SSD_INVALID = 64; + + static const uint32_t SSD_MEDIA_VIDEO = 1; + static const uint32_t SSD_MEDIA_AUDIO = 2; + + uint16_t flags{0}; + + /* The following 2 fields are set as followed: + - If licenseresponse return hdcp information, hdcpversion is 0 and + hdcplimit either 0 (if hdcp is supported) or given value (if hdcpversion is not supported) + - if no hdcp information is passed in licenseresponse, we set hdcpversion to the value we support + manifest / representation have to check if they are allowed to be played. + */ + uint16_t hdcpVersion{0}; //The HDCP version streams has to be restricted 0,10,20,21,22..... + int hdcpLimit{0}; // If set (> 0) streams that are greater than the multiplication of "Width x Height" cannot be played. +}; + +struct Config +{ + // To enable persistent state CDM behaviour + bool isPersistentStorage{false}; + // Optional parameters to make the CDM key request (CDM specific parameters) + std::map optKeyReqParams; + + struct License + { + // The license server certificate + std::vector serverCert; + // The license server url + std::string serverUrl; + // To force an HTTP GET request, instead that POST request + bool isHttpGetRequest{false}; + // HTTP request headers + std::map reqHeaders; + // HTTP parameters to append to the url + std::string reqParams; + // Custom license data encoded as base64 to make the HTTP license request + std::string reqData; + // License data wrappers + // Multiple wrappers supported e.g. "base64,json", the name order defines the order + // in which data will be wrapped, (1) base64 --> (2) url + std::string wrapper; + // License data unwrappers + // Multiple un-wrappers supported e.g. "base64,json", the name order defines the order + // in which data will be unwrapped, (1) base64 --> (2) json + std::string unwrapper; + // License data unwrappers parameters + std::map unwrapperParams; + // Clear key's for ClearKey DRM (KID / KEY pair) + std::map keys; + }; + + // The license configuration + License license; + // Specifies if has been parsed the new DRM config ("drm" or "drm_legacy" kodi property) + //! @todo: to remove when deprecated DRM properties will be removed + bool isNewConfig{true}; +}; + +constexpr std::string_view ROBUSTNESS_HW_SECDEC = "HW_SECURE_DECODE"; + +enum class EngineStatus +{ + NONE, + DRM_NOT_SUPPORTED, + DECRYPTER_ERROR, +}; + +enum class DRMMediaType +{ + UNKNOWN, + VIDEO, + AUDIO +}; + +struct DRMInfo +{ + std::string keySystem; + CryptoMode cryptoMode = CryptoMode::NONE; // Encryption scheme + std::string robustness; + std::vector initData; + std::string defaultKid; + std::string licenseServerUri; + std::vector serverCert; // Server certificate +}; + +struct DRMSession +{ + std::string id; + std::string challenge; // Key request (Challenge) as base64 + std::shared_ptr drm; + std::shared_ptr decrypter; + DRM::DecrypterCapabilites capabilities; + std::vector initData; + DRMMediaType mediaType{DRMMediaType::VIDEO}; +}; + +} // namespace DRM diff --git a/src/decrypters/Helpers.cpp b/src/decrypters/Helpers.cpp index 48123ac71..c33feb647 100644 --- a/src/decrypters/Helpers.cpp +++ b/src/decrypters/Helpers.cpp @@ -94,6 +94,20 @@ std::vector DRM::UrnsToSystemIds(const std::vector UrnsToSystemIds(const std::vector& urns); +std::string_view UrnToKeySystem(std::string_view urn); + /*! * \brief Convert a hexdecimal KeyId of 32 chars to 16 bytes. * \param kidStr The hexdecimal KeyId diff --git a/src/decrypters/IDecrypter.h b/src/decrypters/IDecrypter.h index e0c962113..17f50f4b8 100644 --- a/src/decrypters/IDecrypter.h +++ b/src/decrypters/IDecrypter.h @@ -8,6 +8,8 @@ #pragma once +#include "DrmEngineDefines.h" + #include #include #include @@ -21,73 +23,6 @@ enum class CryptoMode; namespace DRM { -struct DecrypterCapabilites -{ - static const uint32_t SSD_SUPPORTS_DECODING = 1; - static const uint32_t SSD_SECURE_PATH = 2; - static const uint32_t SSD_ANNEXB_REQUIRED = 4; - static const uint32_t SSD_HDCP_RESTRICTED = 8; - static const uint32_t SSD_SINGLE_DECRYPT = 16; - static const uint32_t SSD_SECURE_DECODER = 32; - static const uint32_t SSD_INVALID = 64; - - static const uint32_t SSD_MEDIA_VIDEO = 1; - static const uint32_t SSD_MEDIA_AUDIO = 2; - - uint16_t flags{0}; - - /* The following 2 fields are set as followed: - - If licenseresponse return hdcp information, hdcpversion is 0 and - hdcplimit either 0 (if hdcp is supported) or given value (if hdcpversion is not supported) - - if no hdcp information is passed in licenseresponse, we set hdcpversion to the value we support - manifest / representation have to check if they are allowed to be played. - */ - uint16_t hdcpVersion{0}; //The HDCP version streams has to be restricted 0,10,20,21,22..... - int hdcpLimit{0}; // If set (> 0) streams that are greater than the multiplication of "Width x Height" cannot be played. -}; - -struct Config -{ - // To enable persistent state CDM behaviour - bool isPersistentStorage{false}; - // Optional parameters to make the CDM key request (CDM specific parameters) - std::map optKeyReqParams; - - struct License - { - // The license server certificate - std::vector serverCert; - // The license server url - std::string serverUrl; - // To force an HTTP GET request, instead that POST request - bool isHttpGetRequest{false}; - // HTTP request headers - std::map reqHeaders; - // HTTP parameters to append to the url - std::string reqParams; - // Custom license data encoded as base64 to make the HTTP license request - std::string reqData; - // License data wrappers - // Multiple wrappers supported e.g. "base64,json", the name order defines the order - // in which data will be wrapped, (1) base64 --> (2) url - std::string wrapper; - // License data unwrappers - // Multiple un-wrappers supported e.g. "base64,json", the name order defines the order - // in which data will be unwrapped, (1) base64 --> (2) json - std::string unwrapper; - // License data unwrappers parameters - std::map unwrapperParams; - // Clear key's for ClearKey DRM (KID / KEY pair) - std::map keys; - }; - - // The license configuration - License license; - // Specifies if has been parsed the new DRM config ("drm" or "drm_legacy" kodi property) - //! @todo: to remove when deprecated DRM properties will be removed - bool isNewConfig{true}; -}; - class IDecrypter { public: diff --git a/src/decrypters/clearkey/ClearKeyCencSingleSampleDecrypter.cpp b/src/decrypters/clearkey/ClearKeyCencSingleSampleDecrypter.cpp index bb31e8dde..3ff8e5d57 100644 --- a/src/decrypters/clearkey/ClearKeyCencSingleSampleDecrypter.cpp +++ b/src/decrypters/clearkey/ClearKeyCencSingleSampleDecrypter.cpp @@ -25,6 +25,8 @@ using namespace UTILS; +uint32_t CClearKeyCencSingleSampleDecrypter::g_sessionIdCount = 1; + namespace { void CkB64Encode(std::string& str) @@ -102,14 +104,8 @@ CClearKeyCencSingleSampleDecrypter::CClearKeyCencSingleSampleDecrypter( } const std::vector keyBytes = BASE64::Decode(m_keyPairs[b64DefaultKeyId]); - if (AP4_FAILED(AP4_CencSingleSampleDecrypter::Create(AP4_CENC_CIPHER_AES_128_CTR, keyBytes.data(), - static_cast(keyBytes.size()), 0, 0, - nullptr, false, m_singleSampleDecrypter))) - { - LOG::LogF(LOGERROR, "Failed to create AP4_CencSingleSampleDecrypter"); - } - SetParentIsOwner(false); - AddSessionKey(defaultKeyId); + + InitDecrypter(defaultKeyId, keyBytes); } CClearKeyCencSingleSampleDecrypter::CClearKeyCencSingleSampleDecrypter( @@ -135,11 +131,20 @@ CClearKeyCencSingleSampleDecrypter::CClearKeyCencSingleSampleDecrypter( LOG::LogF(LOGERROR, "Missing KeyId \"%s\" on DRM configuration", defaultKeyId.data()); } - AP4_CencSingleSampleDecrypter::Create(AP4_CENC_CIPHER_AES_128_CTR, hexKey.data(), - static_cast(hexKey.size()), 0, 0, nullptr, false, + InitDecrypter(defaultKeyId, hexKey); +} + +void CClearKeyCencSingleSampleDecrypter::InitDecrypter(const std::vector& defaultKeyId, + const std::vector& key) +{ + AP4_CencSingleSampleDecrypter::Create(AP4_CENC_CIPHER_AES_128_CTR, key.data(), + static_cast(key.size()), 0, 0, nullptr, false, m_singleSampleDecrypter); SetParentIsOwner(false); AddSessionKey(defaultKeyId); + + // Define a session id + m_sessionId = "ck_" + std::to_string(g_sessionIdCount++); } void CClearKeyCencSingleSampleDecrypter::AddSessionKey(const std::vector& keyId) diff --git a/src/decrypters/clearkey/ClearKeyCencSingleSampleDecrypter.h b/src/decrypters/clearkey/ClearKeyCencSingleSampleDecrypter.h index f707859f3..520c97ec2 100644 --- a/src/decrypters/clearkey/ClearKeyCencSingleSampleDecrypter.h +++ b/src/decrypters/clearkey/ClearKeyCencSingleSampleDecrypter.h @@ -48,12 +48,18 @@ class CClearKeyCencSingleSampleDecrypter : public Adaptive_CencSingleSampleDecry void SetDefaultKeyId(const std::vector& keyId) override{}; void AddKeyId(const std::vector& keyId) override{}; bool HasKeys() { return !m_keyIds.empty(); } + std::string GetSessionId() override { return m_sessionId; } private: + void InitDecrypter(const std::vector& defaultKeyId, const std::vector& key); + AP4_CencSingleSampleDecrypter* m_singleSampleDecrypter{nullptr}; std::string m_strSession; std::string m_licenceDefaultKeyId; std::vector> m_keyIds; std::map m_keyPairs; CClearKeyDecrypter* m_host; + + std::string m_sessionId; + static uint32_t g_sessionIdCount; }; diff --git a/src/main.cpp b/src/main.cpp index 0552d0593..23d9a078c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -12,7 +12,6 @@ #include "CompKodiProps.h" #include "SrvBroker.h" #include "Stream.h" -#include "samplereader/SampleReaderFactory.h" #include "utils/log.h" using namespace PLAYLIST; @@ -241,37 +240,13 @@ bool CInputStreamAdaptive::OpenStream(int streamid) return false; } - m_session->PrepareStream(stream); - - stream->m_adStream.start_stream(m_lastPts); - stream->SetAdByteStream(std::make_unique(&stream->m_adStream)); - - ContainerType reprContainerType = rep->GetContainerType(); - uint32_t mask = (1U << stream->m_info.GetStreamType()) | m_session->GetIncludedStreamMask(); - auto reader = ADP::CreateStreamReader(reprContainerType, stream, mask); - - if (!reader) + if (!m_session->PrepareStream(stream, m_lastPts)) { m_session->EnableStream(stream, false); return false; } - reader->SetStreamId(stream->m_info.GetStreamType(), streamid); - - uint16_t psshSetPos = stream->m_adStream.getRepresentation()->m_psshSetPos; - reader->SetDecrypter(m_session->GetSingleSampleDecryptor(psshSetPos), - m_session->GetDecrypterCaps(psshSetPos)); - - stream->SetReader(std::move(reader)); - - if (reprContainerType == ContainerType::TS) - { - // With TS streams the elapsed time would be calculated incorrectly as during the tree refresh, - // nextSegment would be deleted by the FreeSegments/newsegments swap. Do this now before the tree refresh. - // Also, when reopening a stream (switching reps) the elapsed time would be incorrectly set until the - // second segment plays, now force a correct calculation at the start of the stream. - m_session->OnSegmentChanged(&stream->m_adStream); - } + stream->GetReader()->SetStreamId(stream->m_info.GetStreamType(), streamid); if (stream->m_info.GetStreamType() == INPUTSTREAM_TYPE_VIDEO) { @@ -292,44 +267,15 @@ bool CInputStreamAdaptive::OpenStream(int streamid) } } } - m_session->EnableStream(stream, true); - - bool isInfoChanged = stream->GetReader()->GetInformation(stream->m_info); - - uint16_t cdmSessionIndex = stream->m_adStream.getRepresentation()->m_psshSetPos; - - if (stream->m_isEncrypted && m_session->IsCDMSessionSecurePath(cdmSessionIndex)) - { - LOG::Log(LOGDEBUG, "OpenStream(%d): Create secure crypto session", streamid); - - // StreamCryptoSession enable the use of ISA VideoCodecAdaptive decoder - kodi::addon::StreamCryptoSession cryptoSession; - - const std::string keySystem = CSrvBroker::GetKodiProps().GetDrmKeySystem(); - cryptoSession.SetKeySystem(m_session->GetCryptoKeySystem(keySystem)); - cryptoSession.SetSessionId(m_session->GetCDMSession(cdmSessionIndex)); - - if (m_session->GetDecrypterCaps(cdmSessionIndex).flags & - DRM::DecrypterCapabilites::SSD_SUPPORTS_DECODING) - stream->m_info.SetFeatures(INPUTSTREAM_FEATURE_DECODE); - else - stream->m_info.SetFeatures(INPUTSTREAM_FEATURE_NONE); - - if (m_session->GetDecrypterCaps(cdmSessionIndex).flags & - DRM::DecrypterCapabilites::SSD_SECURE_DECODER) - cryptoSession.SetFlags(STREAM_CRYPTO_FLAG_SECURE_DECODER); - else - cryptoSession.SetFlags(STREAM_CRYPTO_FLAG_NONE); - - stream->m_info.SetCryptoSession(cryptoSession); - isInfoChanged = true; - } + m_session->EnableStream(stream, true); + // If stream use DRM always update stream info + const bool isInfoChanged = stream->GetReader()->GetInformation(stream->m_info) || + !stream->m_info.GetCryptoSession().GetSessionId().empty(); return isInfoChanged; } - DEMUX_PACKET* CInputStreamAdaptive::DemuxRead(void) { if (!m_session) @@ -548,9 +494,9 @@ CVideoCodecAdaptive::~CVideoCodecAdaptive() bool CVideoCodecAdaptive::Open(const kodi::addon::VideoCodecInitdata& initData) { - if (!m_session || !m_session->GetDecrypter()) + if (!m_session) return false; - + if ((initData.GetCodecType() == VIDEOCODEC_H264 || initData.GetCodecType() == VIDEOCODEC_AV1) && !initData.GetExtraDataSize() && !(m_state & STATE_WAIT_EXTRADATA)) { @@ -582,11 +528,17 @@ bool CVideoCodecAdaptive::Open(const kodi::addon::VideoCodecInitdata& initData) } m_name += ".decoder"; - std::string sessionId = initData.GetCryptoSession().GetSessionId(); - std::shared_ptr ssd(m_session->GetSingleSampleDecrypter(sessionId)); + const std::string sessionId = initData.GetCryptoSession().GetSessionId(); + + auto drmSession = m_session->GetDRMEngine().GetSession(sessionId); + if (!drmSession) + { + LOG::LogF(LOGERROR, "Cannot get DRM session id: %s", sessionId.c_str()); + return false; + } - return m_session->GetDecrypter()->OpenVideoDecoder( - ssd, initData.GetCStructure()); + m_drm = drmSession->drm; + return m_drm->OpenVideoDecoder(drmSession->decrypter, initData.GetCStructure()); } bool CVideoCodecAdaptive::Reconfigure(const kodi::addon::VideoCodecInitdata& initData) @@ -596,32 +548,33 @@ bool CVideoCodecAdaptive::Reconfigure(const kodi::addon::VideoCodecInitdata& ini bool CVideoCodecAdaptive::AddData(const DEMUX_PACKET& packet) { - if (!m_session || !m_session->GetDecrypter()) + if (!m_drm) return false; - return m_session->GetDecrypter()->DecryptAndDecodeVideo( - dynamic_cast(this), &packet) != VC_ERROR; + return m_drm->DecryptAndDecodeVideo(dynamic_cast(this), + &packet) != VC_ERROR; } VIDEOCODEC_RETVAL CVideoCodecAdaptive::GetPicture(VIDEOCODEC_PICTURE& picture) { - if (!m_session || !m_session->GetDecrypter()) + if (!m_drm) return VIDEOCODEC_RETVAL::VC_ERROR; static VIDEOCODEC_RETVAL vrvm[] = {VIDEOCODEC_RETVAL::VC_NONE, VIDEOCODEC_RETVAL::VC_ERROR, VIDEOCODEC_RETVAL::VC_BUFFER, VIDEOCODEC_RETVAL::VC_PICTURE, VIDEOCODEC_RETVAL::VC_EOF}; - return vrvm[m_session->GetDecrypter()->VideoFrameDataToPicture( - dynamic_cast(this), &picture)]; + return vrvm[m_drm->VideoFrameDataToPicture(dynamic_cast(this), + &picture)]; } void CVideoCodecAdaptive::Reset() { - if (!m_session || !m_session->GetDecrypter()) + if (!m_drm) return; - m_session->GetDecrypter()->ResetVideo(); + m_drm->ResetVideo(); + m_drm = nullptr; } /*****************************************************************************************************/ diff --git a/src/main.h b/src/main.h index 4c7af7c21..731b10a82 100644 --- a/src/main.h +++ b/src/main.h @@ -92,6 +92,7 @@ class ATTR_DLL_LOCAL CVideoCodecAdaptive : public kodi::addon::CInstanceVideoCod }; std::shared_ptr m_session; + std::shared_ptr m_drm; unsigned int m_state; std::string m_name; }; diff --git a/src/parser/DASHTree.cpp b/src/parser/DASHTree.cpp index 798a1e97a..d916c07c0 100644 --- a/src/parser/DASHTree.cpp +++ b/src/parser/DASHTree.cpp @@ -106,10 +106,9 @@ adaptive::CDashTree::CDashTree(const CDashTree& left) : AdaptiveTree(left) } void adaptive::CDashTree::Configure(CHOOSER::IRepresentationChooser* reprChooser, - std::vector supportedKeySystems, std::string_view manifestUpdParams) { - AdaptiveTree::Configure(reprChooser, supportedKeySystems, manifestUpdParams); + AdaptiveTree::Configure(reprChooser, manifestUpdParams); m_isCustomInitPssh = !CSrvBroker::GetKodiProps().GetDrmConfig().initData.empty(); } @@ -940,6 +939,11 @@ void adaptive::CDashTree::ParseTagRepresentation(pugi::xml_node nodeRepr, // Store the protection data if (adpSet->HasProtectionSchemes() || repr->HasProtectionSchemes()) + { + GetProtectionData(adpSet->ProtectionSchemes(), repr->ProtectionSchemes(), *repr.get()); + } + /* REMOVE ME + if (adpSet->HasProtectionSchemes() || repr->HasProtectionSchemes()) { std::vector pssh; std::string kid; @@ -969,7 +973,7 @@ void adaptive::CDashTree::ParseTagRepresentation(pugi::xml_node nodeRepr, } } } - + */ // Parse tag xml_node nodeAudioCh = nodeRepr.child("AudioChannelConfiguration"); if (nodeAudioCh) @@ -1302,126 +1306,69 @@ void adaptive::CDashTree::ParseTagContentProtection( } } + // There are no constraints on the Kid format, it is recommended to be as UUID but not mandatory + STRING::ReplaceAll(protScheme.kid, "-", ""); + protSchemes.emplace_back(protScheme); } } -bool adaptive::CDashTree::GetProtectionData( +void adaptive::CDashTree::GetProtectionData( const std::vector& adpProtSchemes, - const std::vector& reprProtSchemes, - std::vector& pssh, - std::string& kid, - std::string& licenseUrl) + std::vector& reprProtSchemes, + PLAYLIST::CRepresentation& repr) { - // Try find a protection scheme compatible for the current systemid - const ProtectionScheme* protSelected = nullptr; - const ProtectionScheme* protCommon = nullptr; + reprProtSchemes.insert(reprProtSchemes.end(), adpProtSchemes.begin(), adpProtSchemes.end()); - for (std::string_view supportedKeySystem : m_supportedKeySystems) - { - for (const ProtectionScheme& protScheme : reprProtSchemes) - { - if (STRING::CompareNoCase(protScheme.idUri, supportedKeySystem)) - { - protSelected = &protScheme; - } - else if (protScheme.idUri == "urn:mpeg:dash:mp4protection:2011") - { - protCommon = &protScheme; - } - } + CryptoMode cryptoMode = CryptoMode::AES_CTR; // default cenc - if (!protSelected || !protCommon) + // Find the encryption scheme + for (const ProtectionScheme& protScheme : reprProtSchemes) + { + if (protScheme.idUri == "urn:mpeg:dash:mp4protection:2011") { - for (const ProtectionScheme& protScheme : adpProtSchemes) - { - if (!protSelected && STRING::CompareNoCase(protScheme.idUri, supportedKeySystem)) - { - protSelected = &protScheme; - } - else if (!protCommon && protScheme.idUri == "urn:mpeg:dash:mp4protection:2011") - { - protCommon = &protScheme; - } - } + std::string_view encryptionScheme = protScheme.value; + if (encryptionScheme == "cenc") + cryptoMode = CryptoMode::AES_CTR; + else if (encryptionScheme == "cbcs") + cryptoMode = CryptoMode::AES_CBC; + else if (!encryptionScheme.empty()) + LOG::LogF(LOGERROR, "Unsupported encryption scheme: %s", encryptionScheme.data()); + break; } } - // Workaround for ClearKey: - // if license type ClearKey is set and a manifest dont contains ClearKey protection scheme - // in any case the KID is required to allow decryption (with clear keys or license URLs provided by Kodi props) - //! @todo: this should not be a task of parser, moreover missing an appropriate KID extraction from mp4 box - auto& kodiProps = CSrvBroker::GetKodiProps(); - ProtectionScheme ckProtScheme; - if (kodiProps.GetDrmKeySystem() == DRM::KS_CLEARKEY) + // Find the default KeyId, if there are multiple they must be all the same + std::set keyIds; + for (const ProtectionScheme& protScheme : reprProtSchemes) { - std::string_view defaultKid; - if (protSelected) - defaultKid = protSelected->kid; - if (defaultKid.empty() && protCommon) - defaultKid = protCommon->kid; - - if (defaultKid.empty()) - { - for (const ProtectionScheme& protScheme : reprProtSchemes) - { - if (!protScheme.kid.empty()) - { - defaultKid = protScheme.kid; - break; - } - } - if (defaultKid.empty()) - { - for (const ProtectionScheme& protScheme : adpProtSchemes) - { - if (!protScheme.kid.empty()) - { - defaultKid = protScheme.kid; - break; - } - } - } - if (protCommon) - ckProtScheme = *protCommon; - - ckProtScheme.kid = defaultKid; - protCommon = &ckProtScheme; - } + if (!protScheme.kid.empty()) + keyIds.emplace(protScheme.kid); } - bool isEncrypted{false}; - std::string selectedKid; - std::string selectedPssh; - - if (protSelected) + if (keyIds.size() > 1) { - isEncrypted = true; - selectedKid = protSelected->kid; - selectedPssh = protSelected->pssh; - licenseUrl = protSelected->licenseUrl; + LOG::LogF(LOGERROR, "Conflicting KeyId's on ContentProtection tags"); + return; } - if (protCommon) + + for (const ProtectionScheme& protScheme : reprProtSchemes) { - isEncrypted = true; - if (selectedKid.empty()) - selectedKid = protCommon->kid; + std::string_view keySystem = DRM::UrnToKeySystem(protScheme.idUri); - // Set crypto mode - if (protCommon->value == "cenc") - m_cryptoMode = CryptoMode::AES_CTR; - else if (protCommon->value == "cbcs") - m_cryptoMode = CryptoMode::AES_CBC; + if (!keySystem.empty()) + { + DRM::DRMInfo drmInfo; + drmInfo.keySystem = keySystem; + drmInfo.initData = BASE64::Decode(protScheme.pssh); + drmInfo.licenseServerUri = protScheme.licenseUrl; + drmInfo.cryptoMode = cryptoMode; + if (!keyIds.empty()) + drmInfo.defaultKid = *keyIds.begin(); + + repr.AddDrmInfo(drmInfo); + } } - - if (!selectedPssh.empty()) - pssh = BASE64::Decode(selectedPssh); - - // There are no constraints on the Kid format, it is recommended to be as UUID but not mandatory - STRING::ReplaceAll(selectedKid, "-", ""); - kid = selectedKid; - - return isEncrypted; } std::optional adaptive::CDashTree::ParseTagContentProtectionSecDec(pugi::xml_node nodeParent) diff --git a/src/parser/DASHTree.h b/src/parser/DASHTree.h index 44b339641..7f3c261c8 100644 --- a/src/parser/DASHTree.h +++ b/src/parser/DASHTree.h @@ -35,7 +35,6 @@ class ATTR_DLL_LOCAL CDashTree : public adaptive::AdaptiveTree CDashTree(const CDashTree& left); void Configure(CHOOSER::IRepresentationChooser* reprChooser, - std::vector supportedKeySystems, std::string_view manifestUpdParams) override; virtual TreeType GetTreeType() const override { return TreeType::DASH; } @@ -84,11 +83,9 @@ class ATTR_DLL_LOCAL CDashTree : public adaptive::AdaptiveTree * \param licenseUrl[OUT] The license url (if any) * \return True if a protection has been found, otherwise false */ - bool GetProtectionData(const std::vector& adpProtSchemes, - const std::vector& reprProtSchemes, - std::vector& pssh, - std::string& kid, - std::string& licenseUrl); + void GetProtectionData(const std::vector& adpProtSchemes, + std::vector& reprProtSchemes, + PLAYLIST::CRepresentation& repr); std::optional ParseTagContentProtectionSecDec(pugi::xml_node nodeParent); diff --git a/src/parser/HLSTree.cpp b/src/parser/HLSTree.cpp index 35f9377e3..56103bf19 100644 --- a/src/parser/HLSTree.cpp +++ b/src/parser/HLSTree.cpp @@ -150,10 +150,9 @@ adaptive::CHLSTree::CHLSTree(const CHLSTree& left) : AdaptiveTree(left) } void adaptive::CHLSTree::Configure(CHOOSER::IRepresentationChooser* reprChooser, - std::vector supportedKeySystems, std::string_view manifestUpdateParam) { - AdaptiveTree::Configure(reprChooser, supportedKeySystems, manifestUpdateParam); + AdaptiveTree::Configure(reprChooser, manifestUpdateParam); m_decrypter = std::make_unique(); } @@ -1329,8 +1328,9 @@ PLAYLIST::EncryptionType adaptive::CHLSTree::ProcessEncryption( m_currentPssh = STRING::ToVecUint8(resp.data); } - if (uriUrl.empty()) // No kid provided, assume key == kid - m_currentDefaultKID = STRING::ToHexadecimal(uriData); + // try remove it, should not be needed because mp4 extracted + //if (uriUrl.empty()) // No kid provided, assume key == kid + // m_currentDefaultKID = STRING::ToHexadecimal(uriData); } else if (STRING::CompareNoCase(keyFormat, DRM::URN_WIDEVINE)) diff --git a/src/parser/HLSTree.h b/src/parser/HLSTree.h index 71754ddeb..3c4491d88 100644 --- a/src/parser/HLSTree.h +++ b/src/parser/HLSTree.h @@ -36,7 +36,6 @@ class ATTR_DLL_LOCAL CHLSTree : public AdaptiveTree virtual CHLSTree* Clone() const override { return new CHLSTree{*this}; } virtual void Configure(CHOOSER::IRepresentationChooser* reprChooser, - std::vector supportedKeySystems, std::string_view manifestUpdateParam) override; virtual bool Open(std::string_view url, diff --git a/src/test/TestDASHTree.cpp b/src/test/TestDASHTree.cpp index 1ca45f2f2..f3afb8f56 100644 --- a/src/test/TestDASHTree.cpp +++ b/src/test/TestDASHTree.cpp @@ -68,8 +68,7 @@ class DASHTreeTest : public ::testing::Test // We set the download speed to calculate the initial network bandwidth m_reprChooser->SetDownloadSpeed(500000); - tree->Configure(m_reprChooser, std::vector{DRM::URN_WIDEVINE}, - manifestUpdParams); + tree->Configure(m_reprChooser, manifestUpdParams); // Parse the manifest if (!tree->Open(resp.effectiveUrl, resp.headers, resp.data)) diff --git a/src/test/TestHLSTree.cpp b/src/test/TestHLSTree.cpp index e53d53030..a5cbc5bee 100644 --- a/src/test/TestHLSTree.cpp +++ b/src/test/TestHLSTree.cpp @@ -39,13 +39,12 @@ class HLSTreeTest : public ::testing::Test bool OpenTestFileMaster(std::string filePath, std::string url) { - return OpenTestFileMaster(filePath, url, {}, std::vector{}); + return OpenTestFileMaster(filePath, url, {}); } bool OpenTestFileMaster(std::string filePath, std::string url, - std::map manifestHeaders, - std::vector supportedKeySystems) + std::map manifestHeaders) { testHelper::testFile = filePath; @@ -64,7 +63,7 @@ class HLSTreeTest : public ::testing::Test // We set the download speed to calculate the initial network bandwidth m_reprChooser->SetDownloadSpeed(500000); - tree->Configure(m_reprChooser, supportedKeySystems, ""); + tree->Configure(m_reprChooser, ""); // Parse the manifest if (!tree->Open(resp.effectiveUrl, resp.headers, resp.data)) @@ -343,7 +342,7 @@ TEST_F(HLSTreeTest, MultipleEncryptionSequenceDrmNoKSMaster) testHelper::effectiveUrl = "https://foo.bar/hls/video/stream_name/master.m3u8"; bool ret = OpenTestFileMaster("hls/encrypt_master_drm.m3u8", - "https://baz.qux/hls/video/stream_name/master.m3u8", {}, std::vector{}); + "https://baz.qux/hls/video/stream_name/master.m3u8", {}); EXPECT_EQ(ret, false); } @@ -355,7 +354,7 @@ TEST_F(HLSTreeTest, MultipleEncryptionSequenceDrmNoKS) testHelper::effectiveUrl = "https://foo.bar/hls/video/stream_name/master.m3u8"; bool ret = OpenTestFileMaster("hls/encrypt_master.m3u8", - "https://baz.qux/hls/video/stream_name/master.m3u8", {}, std::vector{}); + "https://baz.qux/hls/video/stream_name/master.m3u8", {}); EXPECT_EQ(ret, true); @@ -373,7 +372,7 @@ TEST_F(HLSTreeTest, MultipleEncryptionSequenceDrmNoKS) EXPECT_EQ(periods[0]->GetEncryptionState(), PLAYLIST::EncryptionState::NOT_SUPPORTED); EXPECT_EQ(periods[1]->GetEncryptionState(), PLAYLIST::EncryptionState::NOT_SUPPORTED); } - +/* TEST_F(HLSTreeTest, MultipleEncryptionSequenceDrm) { // Open the master manifest with the supported Widevine key system @@ -399,3 +398,4 @@ TEST_F(HLSTreeTest, MultipleEncryptionSequenceDrm) EXPECT_EQ(periods[0]->GetEncryptionState(), PLAYLIST::EncryptionState::ENCRYPTED_DRM); EXPECT_EQ(periods[1]->GetEncryptionState(), PLAYLIST::EncryptionState::ENCRYPTED_DRM); } +*/ \ No newline at end of file diff --git a/src/test/TestSmoothTree.cpp b/src/test/TestSmoothTree.cpp index 8126378a2..0a79cf18b 100644 --- a/src/test/TestSmoothTree.cpp +++ b/src/test/TestSmoothTree.cpp @@ -59,7 +59,7 @@ class SmoothTreeTest : public ::testing::Test // We set the download speed to calculate the initial network bandwidth m_reprChooser->SetDownloadSpeed(500000); - tree->Configure(m_reprChooser, std::vector{DRM::URN_WIDEVINE}, ""); + tree->Configure(m_reprChooser, ""); // Parse the manifest if (!tree->Open(resp.effectiveUrl, resp.headers, resp.data))