diff --git a/lib/mfcdm/CMakeLists.txt b/lib/mfcdm/CMakeLists.txt new file mode 100644 index 000000000..5414d40e2 --- /dev/null +++ b/lib/mfcdm/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.10) +project(mfcdm_library) + +add_library(mfcdm_library STATIC + mfcdm/MediaFoundationCdm.h + mfcdm/MediaFoundationCdmConfig.h + mfcdm/MediaFoundationCdmTypes.h + mfcdm/MediaFoundationCdm.cpp + mfcdm/MediaFoundationCdmFactory.cpp + mfcdm/MediaFoundationCdmSession.cpp + mfcdm/MediaFoundationSession.cpp + mfcdm/Log.cpp +) + +target_include_directories(mfcdm_library PUBLIC ${PROJECT_SOURCE_DIR}) +target_link_libraries(mfcdm_library PRIVATE cdm_library propsys mf mfplat mfplay mfreadwrite mfuuid wmcodecdspuuid) +set_target_properties(mfcdm_library PROPERTIES POSITION_INDEPENDENT_CODE True) diff --git a/lib/mfcdm/mfcdm/Log.cpp b/lib/mfcdm/mfcdm/Log.cpp new file mode 100644 index 000000000..8d5824cda --- /dev/null +++ b/lib/mfcdm/mfcdm/Log.cpp @@ -0,0 +1,47 @@ +#include "Log.h" + +#include +#include + +typedef struct +{ + const char* name; + int cur_level; + void (*msg_callback)(int level, char* msg); +} debug_ctx_t; + +static debug_ctx_t debug_ctx = {"MF", MFCDM::MFLOG_NONE, NULL}; + + +static inline void __dbg(debug_ctx_t* ctx, int level, const char* fmt, va_list ap) +{ + if (ctx != NULL && level <= ctx->cur_level) + { + char msg[4096]; + int len = snprintf(msg, sizeof(msg), "[%s] ", ctx->name); + vsnprintf(msg + len, sizeof(msg) - len, fmt, ap); + if (ctx->msg_callback) + { + ctx->msg_callback(level, msg); + } + } +} + +void MFCDM::LogAll() +{ + debug_ctx.cur_level = MFLOG_ALL; +} + +void MFCDM::Log(LogLevel level, const char* fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + __dbg(&debug_ctx, level, fmt, ap); + va_end(ap); +} + +void MFCDM::SetMFMsgCallback(void (*msgcb)(int level, char*)) +{ + debug_ctx.msg_callback = msgcb; +} diff --git a/lib/mfcdm/mfcdm/Log.h b/lib/mfcdm/mfcdm/Log.h new file mode 100644 index 000000000..2bf547f96 --- /dev/null +++ b/lib/mfcdm/mfcdm/Log.h @@ -0,0 +1,17 @@ +namespace MFCDM +{ +enum LogLevel +{ + MFLOG_NONE = -1, + MFLOG_ERROR, + MFLOG_WARN, + MFLOG_INFO, + MFLOG_DEBUG, + MFLOG_ALL = 100 +}; + +void LogAll(); +void Log(LogLevel level, const char* fmt, ...); +void SetMFMsgCallback(void (*msgcb)(int level, char*)); + +} // namespace MFCDM diff --git a/lib/mfcdm/mfcdm/MediaFoundationCdm.cpp b/lib/mfcdm/mfcdm/MediaFoundationCdm.cpp new file mode 100644 index 000000000..aac1ff0f3 --- /dev/null +++ b/lib/mfcdm/mfcdm/MediaFoundationCdm.cpp @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2023 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 "MediaFoundationCdm.h" + +#include "MediaFoundationCdmFactory.h" +#include "MediaFoundationCdmModule.h" +#include "MediaFoundationCdmSession.h" +#include "utils/PMPHostWrapper.h" +#include "Log.h" + +#include + +MediaFoundationCdm::MediaFoundationCdm() = default; +MediaFoundationCdm::~MediaFoundationCdm() = default; + +bool MediaFoundationCdm::Initialize(const MediaFoundationCdmConfig& cdmConfig, + std::string_view keySystem, + std::string_view basePath) +{ + bool ret = true; + + m_session.Startup(); + + ret = m_session.HasMediaFoundation(); + if (!ret) + { + Log(MFCDM::MFLOG_ERROR, "MF doesn't exist on current system"); + return false; + } + + const auto m_factory = std::make_unique(keySystem); + + ret = m_factory->Initialize(); + if (!ret) + { + Log(MFCDM::MFLOG_ERROR, "MFFactory failed to initialize."); + return false; + } + + ret = m_factory->CreateMfCdm(cdmConfig, basePath, m_module); + if (!ret) + { + Log(MFCDM::MFLOG_ERROR, "MFFactory failed to create MF CDM."); + return false; + } + + Log(MFCDM::MFLOG_DEBUG, "MF CDM created."); + + SetupPMPServer(); + return true; +} + +/*! + * \brief Setup PMPHostApp + * IMFContentDecryptionModule->SetPMPHostApp + * needs to be set if not under UWP or else GenerateChallenge will fail + * \link https://github.com/microsoft/media-foundation/issues/37#issuecomment-1194534228 + */ +void MediaFoundationCdm::SetupPMPServer() const +{ + if (!m_module) + return; + + const winrt::com_ptr spIMFGetService = m_module->As(); + winrt::com_ptr pmpHostApp; + + if(FAILED(spIMFGetService->GetService( + MF_CONTENTDECRYPTIONMODULE_SERVICE, IID_PPV_ARGS(pmpHostApp.put())))) + { + Log(MFCDM::MFLOG_ERROR, "Failed to get MF CDM service."); + return; + } + + winrt::com_ptr spIMFPMPHostApp = winrt::make_self(pmpHostApp); + m_module->SetPMPHostApp(spIMFPMPHostApp.get()); +} + +bool MediaFoundationCdm::SetServerCertificate(const uint8_t* serverCertificateData, + uint32_t serverCertificateDataSize) const +{ + m_module->SetServerCertificate(serverCertificateData, serverCertificateDataSize); + return true; +} + +bool MediaFoundationCdm::CreateSessionAndGenerateRequest(SessionType sessionType, + InitDataType initDataType, + const std::vector& initData, + SessionClient* client) +{ + auto session = std::make_shared(client); + + if (!session->Initialize(m_module.get(), sessionType)) + { + return false; + } + + // when session id is identified, callback is ran. + // this meant to be able to access UpdateSession() + // inside MF callback because then session id is known. + int sessionToken = m_nextSessionToken++; + m_pendingSessions.emplace(sessionToken, session); + + if (!session->GenerateRequest(initDataType, initData, + std::bind(&MediaFoundationCdm::OnNewSessionId, this, sessionToken, std::placeholders::_1))) + { + return false; + } + return true; +} + +void MediaFoundationCdm::LoadSession(SessionType sessionType, const std::string& sessionId) +{ + +} + +bool MediaFoundationCdm::UpdateSession(const std::string& sessionId, + const std::vector& response) +{ + if (!m_module) + return false; + + auto* session = GetSession(sessionId); + if (!session) + { + Log(MFCDM::MFLOG_ERROR, "Couldn't find session in created sessions."); + return false; + } + + return session->Update(response); +} + +void MediaFoundationCdm::OnNewSessionId(int sessionToken, std::string_view sessionId) +{ + auto itr = m_pendingSessions.find(sessionToken); + assert(itr != m_pendingSessions.end()); + + auto session = std::move(itr->second); + assert(session); + + m_pendingSessions.erase(itr); + + m_sessions.emplace(sessionId, std::move(session)); +} + +MediaFoundationCdmSession* MediaFoundationCdm::GetSession(const std::string& sessionId) const +{ + auto itr = m_sessions.find(sessionId); + if (itr == m_sessions.end()) + return nullptr; + + return itr->second.get(); +} + diff --git a/lib/mfcdm/mfcdm/MediaFoundationCdm.h b/lib/mfcdm/mfcdm/MediaFoundationCdm.h new file mode 100644 index 000000000..96b560a69 --- /dev/null +++ b/lib/mfcdm/mfcdm/MediaFoundationCdm.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2023 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 "MediaFoundationSession.h" +#include "MediaFoundationCdmConfig.h" +#include "MediaFoundationCdmTypes.h" + +#include +#include +#include +#include + +class MediaFoundationCdmSession; +class MediaFoundationCdmModule; + +class MediaFoundationCdm { +public: + MediaFoundationCdm(); + ~MediaFoundationCdm(); + + bool IsInitialized() const { return m_module != nullptr; } + + bool Initialize(const MediaFoundationCdmConfig& cdmConfig, + std::string_view keySystem, + std::string_view basePath); + + bool SetServerCertificate(const uint8_t* serverCertificateData, + uint32_t serverCertificateDataSize) const; + + bool CreateSessionAndGenerateRequest(SessionType sessionType, + InitDataType initDataType, + const std::vector& initData, + SessionClient* client); + + void LoadSession(SessionType session_type, const std::string& session_id); + bool UpdateSession(const std::string& session_id, const std::vector& response); + +private: + void SetupPMPServer() const; + MediaFoundationCdmSession* GetSession(const std::string& sessionId) const; + void OnNewSessionId(int sessionToken, std::string_view sessionId); + + MediaFoundationSession m_session; + std::unique_ptr m_module; + + int m_nextSessionToken = 0; + std::map> m_pendingSessions; + std::map> m_sessions; +}; diff --git a/lib/mfcdm/mfcdm/MediaFoundationCdmConfig.h b/lib/mfcdm/mfcdm/MediaFoundationCdmConfig.h new file mode 100644 index 000000000..ebe476eeb --- /dev/null +++ b/lib/mfcdm/mfcdm/MediaFoundationCdmConfig.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2023 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 + +/*! + * \brief The runtime configuration for the CDM instance +*/ +struct MediaFoundationCdmConfig +{ + MediaFoundationCdmConfig(bool distinctive_identifier = false, bool persistent_state = false) + : allow_distinctive_identifier(distinctive_identifier), + allow_persistent_state(persistent_state), + use_hw_secure_codecs(false) + { + + } + + // Allow access to a distinctive identifier. + bool allow_distinctive_identifier; + + // Allow access to persistent state. + bool allow_persistent_state; + + // Use hardware-secure codecs. + bool use_hw_secure_codecs; +}; diff --git a/lib/mfcdm/mfcdm/MediaFoundationCdmFactory.cpp b/lib/mfcdm/mfcdm/MediaFoundationCdmFactory.cpp new file mode 100644 index 000000000..1901fe2da --- /dev/null +++ b/lib/mfcdm/mfcdm/MediaFoundationCdmFactory.cpp @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2023 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 "MediaFoundationCdmFactory.h" + +#include "MediaFoundationCdmModule.h" +#include "MediaFoundationCdm.h" +#include "utils/ScopedPropVariant.h" +#include "utils/Wide.h" +#include "Log.h" + +#include +#include + +using namespace UTILS; + +static void InitPropVariantFromBSTR(const wchar_t* str, PROPVARIANT* propVariant) +{ + propVariant->vt = VT_BSTR; + propVariant->bstrVal = SysAllocString(str); +} + +MediaFoundationCdmFactory::MediaFoundationCdmFactory(std::string_view keySystem) + : m_keySystem(keySystem) +{ +} + +bool MediaFoundationCdmFactory::Initialize() +{ + const winrt::com_ptr classFactory = winrt::create_instance( + CLSID_MFMediaEngineClassFactory, CLSCTX_INPROC_SERVER); + const std::wstring keySystemWide = WIDE::ConvertUtf8ToWide(m_keySystem); + + return SUCCEEDED(classFactory->CreateContentDecryptionModuleFactory( + keySystemWide.c_str(), IID_PPV_ARGS(&m_cdmFactory))); +} + +bool MediaFoundationCdmFactory::IsTypeSupported(std::string_view keySystem) const +{ + return m_cdmFactory->IsTypeSupported(WIDE::ConvertUtf8ToWide(keySystem).c_str(), nullptr); +} + +/*! + * \brief Returns a property store similar to EME MediaKeySystemMediaCapability. + */ +bool CreateVideoCapability(const MediaFoundationCdmConfig& cdm_config, + winrt::com_ptr& video_capability) +{ + winrt::com_ptr temp_video_capability; + if(FAILED(PSCreateMemoryPropertyStore(IID_PPV_ARGS(&temp_video_capability)))) + { + Log(MFCDM::MFLOG_ERROR, "Failed to create property store for video capabilities."); + return false; + } + + if (cdm_config.use_hw_secure_codecs) + { + ScopedPropVariant robustness; + robustness->vt = VT_BSTR; + robustness->bstrVal = SysAllocString(L"HW_SECURE_ALL"); + temp_video_capability->SetValue(MF_EME_ROBUSTNESS, robustness.get()); + } + + video_capability = temp_video_capability; + return true; +} + +/*! + * \brief Creates a IPropertyStore for CDM based on cdm config settings. + * \link https://github.com/chromium/chromium/blob/ea198b54e3f6b0cfdd6bacbb01c2307fd1797b63/media/cdm/win/media_foundation_cdm_util.cc#L68 + * \link https://github.com/microsoft/media-foundation/blob/969f38b9fff9892f5d75bc353c72d213da807739/samples/MediaEngineEMEUWPSample/src/media/eme/MediaKeySystemConfiguration.cpp#L74 + */ +bool BuildCdmAccessConfigurations(const MediaFoundationCdmConfig& cdmConfig, + winrt::com_ptr& properties) +{ + winrt::com_ptr temp_configurations; + if (FAILED(PSCreateMemoryPropertyStore(IID_PPV_ARGS(&temp_configurations)))) + { + Log(MFCDM::MFLOG_ERROR, "Failed to create property store for cdm access."); + return false; + } + + // Add an empty audio capability. + ScopedPropVariant audio_capabilities; + audio_capabilities->vt = VT_VARIANT | VT_VECTOR; + audio_capabilities->capropvar.cElems = 0; + if (FAILED(temp_configurations->SetValue(MF_EME_AUDIOCAPABILITIES, + audio_capabilities.get()))) + { + Log(MFCDM::MFLOG_ERROR, "Failed to set audio capabilities."); + return false; + } + + // Add a video capability. + winrt::com_ptr video_capability; + if (!CreateVideoCapability(cdmConfig, video_capability)) + return false; + + ScopedPropVariant videoConfig; + videoConfig->vt = VT_UNKNOWN; + videoConfig->punkVal = video_capability.detach(); + + ScopedPropVariant videoCapabilities; + videoCapabilities->vt = VT_VARIANT | VT_VECTOR; + videoCapabilities->capropvar.cElems = 1; + videoCapabilities->capropvar.pElems = static_cast(CoTaskMemAlloc(sizeof(PROPVARIANT))); + if (!videoCapabilities->capropvar.pElems) + { + Log(MFCDM::MFLOG_ERROR, "Failed to allocate video capability array."); + return false; + } + + if (FAILED(PropVariantCopy(videoCapabilities->capropvar.pElems, videoConfig.ptr()))) + { + Log(MFCDM::MFLOG_ERROR, "Failed to set copy video config into video capabilities."); + return false; + } + + if (FAILED(temp_configurations->SetValue(MF_EME_VIDEOCAPABILITIES, videoCapabilities.get()))) + { + Log(MFCDM::MFLOG_ERROR, "Failed to set persisted state."); + return false; + } + + // Persistent state + ScopedPropVariant persisted_state; + if (FAILED(InitPropVariantFromUInt32(cdmConfig.allow_persistent_state + ? MF_MEDIAKEYS_REQUIREMENT_OPTIONAL + : MF_MEDIAKEYS_REQUIREMENT_NOT_ALLOWED, + persisted_state.ptr()))) + { + Log(MFCDM::MFLOG_ERROR, "Failed to create prop variant for persistent state."); + return false; + } + + if (FAILED(temp_configurations->SetValue(MF_EME_PERSISTEDSTATE, persisted_state.get()))) + { + Log(MFCDM::MFLOG_ERROR, "Failed to set persisted state."); + return false; + } + + // Distinctive ID + ScopedPropVariant allow_distinctive_identifier; + if (FAILED(InitPropVariantFromUInt32(cdmConfig.allow_distinctive_identifier + ? MF_MEDIAKEYS_REQUIREMENT_OPTIONAL + : MF_MEDIAKEYS_REQUIREMENT_NOT_ALLOWED, + allow_distinctive_identifier.ptr()))) + { + Log(MFCDM::MFLOG_ERROR, "Failed to create prop variant for distinctive identifier."); + return false; + } + + if (FAILED(temp_configurations->SetValue(MF_EME_DISTINCTIVEID, + allow_distinctive_identifier.get()))) + { + Log(MFCDM::MFLOG_ERROR, "Failed to set distinctive identifier."); + return false; + } + + properties = temp_configurations; + return true; +} + +bool BuildCdmProperties(const std::filesystem::path& storePath, + winrt::com_ptr& properties) +{ + winrt::com_ptr temp_properties; + if (FAILED(PSCreateMemoryPropertyStore(IID_PPV_ARGS(&temp_properties)))) + { + Log(MFCDM::MFLOG_ERROR, "Failed to create property store for cdm properties."); + return false; + } + + ScopedPropVariant storePathVar; + InitPropVariantFromBSTR(storePath.wstring().c_str(), storePathVar.ptr()); + + if (FAILED(temp_properties->SetValue(MF_EME_CDM_STOREPATH, storePathVar.get()))) + { + Log(MFCDM::MFLOG_ERROR, "Failed to set CDM Storage Path."); + return false; + } + + properties = temp_properties; + return true; +} + +bool MediaFoundationCdmFactory::CreateMfCdm(const MediaFoundationCdmConfig& cdmConfig, + const std::filesystem::path& cdmPath, + std::unique_ptr& mfCdm) const +{ + const auto key_system_str = WIDE::ConvertUtf8ToWide(m_keySystem); + if (!m_cdmFactory->IsTypeSupported(key_system_str.c_str(), nullptr)) + { + Log(MFCDM::MFLOG_ERROR, "%s is not supported by MF CdmFactory", m_keySystem); + return false; + } + + winrt::com_ptr cdmConfigProp; + if (!BuildCdmAccessConfigurations(cdmConfig, cdmConfigProp)) + { + Log(MFCDM::MFLOG_ERROR, "Failed to build cdm access configuration."); + return false; + } + + winrt::com_ptr cdmAccess; + IPropertyStore* configurations[] = {cdmConfigProp.get()}; + if (FAILED(m_cdmFactory->CreateContentDecryptionModuleAccess( + key_system_str.c_str(), configurations, ARRAYSIZE(configurations), cdmAccess.put()))) + { + Log(MFCDM::MFLOG_ERROR, "Failed to create module access."); + return false; + } + + // Ensure path exists to the cdm path. + if (!std::filesystem::create_directory(cdmPath) && !std::filesystem::exists(cdmPath)) + { + Log(MFCDM::MFLOG_ERROR, "CDM Path %s doesn't exist.", cdmPath.string()); + return false; + } + + winrt::com_ptr cdmProperties; + if (!BuildCdmProperties(cdmPath, cdmProperties)) + { + Log(MFCDM::MFLOG_ERROR, "Failed to build cdm properties."); + return false; + } + + winrt::com_ptr cdm; + if (FAILED(cdmAccess->CreateContentDecryptionModule(cdmProperties.get(), cdm.put()))) + { + Log(MFCDM::MFLOG_ERROR, "Failed to create cdm module."); + return false; + } + + mfCdm = std::make_unique(cdm); + return true; +} diff --git a/lib/mfcdm/mfcdm/MediaFoundationCdmFactory.h b/lib/mfcdm/mfcdm/MediaFoundationCdmFactory.h new file mode 100644 index 000000000..eaab6b2cc --- /dev/null +++ b/lib/mfcdm/mfcdm/MediaFoundationCdmFactory.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 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 "MediaFoundationCdmConfig.h" + +#include +#include +#include + +#include +#include + +#include +#include + +class MediaFoundationCdmModule; + +class MediaFoundationCdmFactory { +public: + explicit MediaFoundationCdmFactory(std::string_view keySystem); + bool Initialize(); + + bool IsTypeSupported(std::string_view keySystem) const; + + bool CreateMfCdm(const MediaFoundationCdmConfig& cdmConfig, + const std::filesystem::path& cdmPath, + std::unique_ptr& mfCdm) const; + +private: + std::string m_keySystem; + + winrt::com_ptr m_cdmFactory; +}; diff --git a/lib/mfcdm/mfcdm/MediaFoundationCdmModule.h b/lib/mfcdm/mfcdm/MediaFoundationCdmModule.h new file mode 100644 index 000000000..c36143bfb --- /dev/null +++ b/lib/mfcdm/mfcdm/MediaFoundationCdmModule.h @@ -0,0 +1,46 @@ +#include +#include +#include + +/*! + * \brief Wrapper around winrt::com_ptr of IMFContentDecryptionModule + * + * This is to prevent imports to Windows API & MF in MediaFoundationCdm.h + */ +class MediaFoundationCdmModule +{ +public: + ~MediaFoundationCdmModule() = default; + + MediaFoundationCdmModule(winrt::com_ptr& cdmModule) + { + std::swap(m_mfCdm, cdmModule); + } + + inline HRESULT SetServerCertificate(const uint8_t* server_certificate_data, + uint32_t server_certificate_data_size) const + { + return m_mfCdm->SetServerCertificate(server_certificate_data, server_certificate_data_size); + } + + inline HRESULT SetPMPHostApp(IMFPMPHostApp* pmpHostApp) const + { + return m_mfCdm->SetPMPHostApp(pmpHostApp); + } + + inline HRESULT CreateSession(MF_MEDIAKEYSESSION_TYPE sessionType, + IMFContentDecryptionModuleSessionCallbacks* callbacks, + IMFContentDecryptionModuleSession** session) const + { + return m_mfCdm->CreateSession(sessionType, callbacks, session); + } + + template + inline winrt::com_ptr As() const + { + return m_mfCdm.as(); + }; + +private: + winrt::com_ptr m_mfCdm; +}; diff --git a/lib/mfcdm/mfcdm/MediaFoundationCdmSession.cpp b/lib/mfcdm/mfcdm/MediaFoundationCdmSession.cpp new file mode 100644 index 000000000..343ea2240 --- /dev/null +++ b/lib/mfcdm/mfcdm/MediaFoundationCdmSession.cpp @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2023 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 "MediaFoundationCdmSession.h" + +#include "MediaFoundationCdmModule.h" +#include "utils/ScopedCoMem.h" +#include "utils/Wide.h" +#include "Log.h" + +#include +#include + +#include +#include +#include +#include + +using namespace UTILS; + +MF_MEDIAKEYSESSION_TYPE ToMFSessionType(SessionType session_type) +{ + switch (session_type) + { + case MFPersistentLicense: + return MF_MEDIAKEYSESSION_TYPE_PERSISTENT_LICENSE; + case MFTemporary: + default: + return MF_MEDIAKEYSESSION_TYPE_TEMPORARY; + } +} + +/*! + * \link https://www.w3.org/TR/eme-initdata-registry/ + */ +LPCWSTR InitDataTypeToString(InitDataType init_data_type) +{ + switch (init_data_type) + { + case MFWebM: + return L"webm"; + case MFCenc: + return L"cenc"; + case MFKeyIds: + return L"keyids"; + default: + return L"unknown"; + } +} + +KeyStatus ToCdmKeyStatus(MF_MEDIAKEY_STATUS status) +{ + switch (status) + { + case MF_MEDIAKEY_STATUS_USABLE: + return MFKeyUsable; + case MF_MEDIAKEY_STATUS_EXPIRED: + return MFKeyExpired; + case MF_MEDIAKEY_STATUS_OUTPUT_DOWNSCALED: + return MFKeyDownScaled; + case MF_MEDIAKEY_STATUS_INTERNAL_ERROR: + // Output not allowed is legacy use? Should not happen in normal cases + case MF_MEDIAKEY_STATUS_OUTPUT_NOT_ALLOWED: + return MFKeyError; + case MF_MEDIAKEY_STATUS_STATUS_PENDING: + return MFKeyPending; + case MF_MEDIAKEY_STATUS_RELEASED: + return MFKeyReleased; + case MF_MEDIAKEY_STATUS_OUTPUT_RESTRICTED: + return MFKeyRestricted; + } +} + +std::vector> ToCdmKeysInfo(const MFMediaKeyStatus* key_statuses, + int count) +{ + std::vector> keys_info; + keys_info.reserve(count); + for (int i = 0; i < count; ++i) + { + const auto& key_status = key_statuses[i]; + keys_info.push_back(std::make_unique( + std::vector(key_status.pbKeyId, key_status.pbKeyId + key_status.cbKeyId), + ToCdmKeyStatus(key_status.eMediaKeyStatus)) + ); + } + return keys_info; +} + +class SessionCallbacks : public winrt::implements< + SessionCallbacks, IMFContentDecryptionModuleSessionCallbacks> +{ + public: + using SessionMessage = + std::function& message, std::string_view destinationUrl)>; + using KeyChanged = + std::function; + + SessionCallbacks(SessionMessage sessionMessage, KeyChanged keyChanged) + : m_sessionMessage(std::move(sessionMessage)), m_keyChanged(keyChanged){}; + + IFACEMETHODIMP KeyMessage(MF_MEDIAKEYSESSION_MESSAGETYPE message_type, + const BYTE* message, + DWORD message_size, + LPCWSTR destination_url) final + { + Log(MFCDM::MFLOG_DEBUG, "Message size: %i Destination Url: %S", + message_size, destination_url); + m_sessionMessage(std::vector(message, message + message_size), + WIDE::ConvertWideToUTF8(destination_url)); + return S_OK; + } + + IFACEMETHODIMP KeyStatusChanged() final + { + Log(MFCDM::MFLOG_DEBUG, "KeyStatusChanged"); + m_keyChanged(); + return S_OK; + } +private: + SessionMessage m_sessionMessage; + KeyChanged m_keyChanged; +}; + +MediaFoundationCdmSession::MediaFoundationCdmSession(SessionClient* client) + : m_client(client) +{ + assert(m_client != nullptr); +} + +bool MediaFoundationCdmSession::Initialize(MediaFoundationCdmModule* mfCdm, + SessionType sessionType) +{ + const auto session_callbacks = winrt::make( + std::bind(&MediaFoundationCdmSession::OnSessionMessage, this, std::placeholders::_1, std::placeholders::_2), + std::bind(&MediaFoundationCdmSession::OnKeyChange, this) + ); + // |mf_cdm_session_| holds a ref count to |session_callbacks|. + if (FAILED(mfCdm->CreateSession(ToMFSessionType(sessionType), session_callbacks.get(), + mfCdmSession.put()))) + { + Log(MFCDM::MFLOG_ERROR, "Failed to create MF CDM session."); + return false; + } + return true; +} + +bool MediaFoundationCdmSession::GenerateRequest(InitDataType initDataType, + const std::vector& initData, + SessionCreatedFunc created) +{ + m_sessionCreated = std::move(created); + + if (FAILED(mfCdmSession->GenerateRequest(InitDataTypeToString(initDataType), initData.data(), + static_cast(initData.size())))) + { + Log(MFCDM::MFLOG_ERROR, "Failed to generate MF CDM request."); + return false; + } + return true; +} + +bool MediaFoundationCdmSession::Update(const std::vector& response) +{ + if (FAILED(mfCdmSession->Update(response.data(), static_cast(response.size())))) + { + Log(MFCDM::MFLOG_ERROR, "Failed to update MF CDM with response."); + return false; + } + return true; +} + +void MediaFoundationCdmSession::OnSessionMessage(const std::vector& message, + std::string_view destinationUrl) +{ + if (!m_client) + return; + + if (m_sessionCreated) + { + m_sessionCreated(GetSessionId()); + m_sessionCreated = SessionCreatedFunc(); + } + + m_client->OnSessionMessage(GetSessionId(), message, destinationUrl); +} + +void MediaFoundationCdmSession::OnKeyChange() const +{ + if (!m_client || !mfCdmSession) + return; + + ScopedCoMem keyStatuses; + + UINT count = 0; + if (FAILED(mfCdmSession->GetKeyStatuses(&keyStatuses, &count))) + { + Log(MFCDM::MFLOG_ERROR, "Failed to get key statuses."); + return; + } + + m_client->OnKeyChange(GetSessionId(), ToCdmKeysInfo(keyStatuses.get(), count)); + + for (UINT i = 0; i < count; ++i) + { + const auto& key_status = keyStatuses.get()[i]; + if (key_status.pbKeyId) + CoTaskMemFree(key_status.pbKeyId); + } +} + +std::string MediaFoundationCdmSession::GetSessionId() const +{ + if (!mfCdmSession) + return ""; + + ScopedCoMem sessionId; + + if (FAILED(mfCdmSession->GetSessionId(&sessionId))) + { + Log(MFCDM::MFLOG_ERROR, "Failed to grab MF session's id."); + return ""; + } + + return WIDE::ConvertWideToUTF8(sessionId.get()); +} diff --git a/lib/mfcdm/mfcdm/MediaFoundationCdmSession.h b/lib/mfcdm/mfcdm/MediaFoundationCdmSession.h new file mode 100644 index 000000000..c9628de8a --- /dev/null +++ b/lib/mfcdm/mfcdm/MediaFoundationCdmSession.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2023 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 "MediaFoundationCdmTypes.h" + +#include + +#include +#include + +#include +#include + +class MediaFoundationCdmModule; + +class MediaFoundationCdmSession { +public: + using SessionCreatedFunc = std::function; + + MediaFoundationCdmSession(SessionClient* client); + + bool Initialize(MediaFoundationCdmModule* mfCdm, SessionType sessionType); + + bool GenerateRequest(InitDataType initDataType, + const std::vector& initData, + SessionCreatedFunc created); + bool Update(const std::vector& response); + + std::string GetSessionId() const; + +private: + + void OnSessionMessage(const std::vector& message, std::string_view destinationUrl); + void OnKeyChange() const; + + winrt::com_ptr mfCdmSession; + SessionClient* m_client; + SessionCreatedFunc m_sessionCreated; +}; diff --git a/lib/mfcdm/mfcdm/MediaFoundationCdmTypes.h b/lib/mfcdm/mfcdm/MediaFoundationCdmTypes.h new file mode 100644 index 000000000..38490e638 --- /dev/null +++ b/lib/mfcdm/mfcdm/MediaFoundationCdmTypes.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2023 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 +#include +#include +#include + +enum SessionType : uint32_t +{ + MFTemporary = 0, + MFPersistentLicense = 1 +}; + +enum InitDataType : uint32_t +{ + MFCenc = 0, + MFKeyIds = 1, + MFWebM = 2 +}; + +enum KeyStatus : uint32_t +{ + MFKeyUsable = 0, + MFKeyDownScaled = 1, + MFKeyPending = 2, + MFKeyExpired = 3, + MFKeyReleased = 4, + MFKeyRestricted = 5, + MFKeyError = 6 +}; + +struct KeyInfo +{ + KeyInfo(std::vector keyId, KeyStatus status) + : keyId(std::move(keyId)), + status(status) + { + + } + std::vector keyId; + KeyStatus status; + + bool operator==(KeyInfo const& other) const { return keyId == other.keyId; } +}; + +class SessionClient +{ +public: + virtual ~SessionClient() = default; + + virtual void OnSessionMessage(std::string_view sessionId, + const std::vector& message, + std::string_view destinationUrl) = 0; + + virtual void OnKeyChange(std::string_view sessionId, + std::vector> keys) = 0; +}; diff --git a/lib/mfcdm/mfcdm/MediaFoundationSession.cpp b/lib/mfcdm/mfcdm/MediaFoundationSession.cpp new file mode 100644 index 000000000..ac7b50c9c --- /dev/null +++ b/lib/mfcdm/mfcdm/MediaFoundationSession.cpp @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2023 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 "MediaFoundationSession.h" + +#include +#include +#include + +MediaFoundationSession::~MediaFoundationSession() { + Shutdown(); +} + +void MediaFoundationSession::Startup() { + winrt::init_apartment(); + + const auto hr = MFStartup(MF_VERSION, MFSTARTUP_LITE); + hasMediaFoundation = hr == S_OK; +} + +void MediaFoundationSession::Shutdown() { + if (hasMediaFoundation) + MFShutdown(); + hasMediaFoundation = false; + + winrt::uninit_apartment(); +} diff --git a/lib/mfcdm/mfcdm/MediaFoundationSession.h b/lib/mfcdm/mfcdm/MediaFoundationSession.h new file mode 100644 index 000000000..ce3eb4639 --- /dev/null +++ b/lib/mfcdm/mfcdm/MediaFoundationSession.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2023 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 + +class MediaFoundationSession { +public: + ~MediaFoundationSession(); + + void Startup(); + void Shutdown(); + + [[nodiscard]] bool HasMediaFoundation() const { return hasMediaFoundation; } +private: + bool hasMediaFoundation = false; +}; diff --git a/lib/mfcdm/mfcdm/utils/PMPHostWrapper.h b/lib/mfcdm/mfcdm/utils/PMPHostWrapper.h new file mode 100644 index 000000000..3ce2a8d7d --- /dev/null +++ b/lib/mfcdm/mfcdm/utils/PMPHostWrapper.h @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2023 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 +#include +#include +#include + +EXTERN_GUID(GUID_ObjectStream, 0x3e73735c, 0xe6c0, 0x481d, 0x82, 0x60, 0xee, 0x5d, 0xb1, 0x34, 0x3b, 0x5f); +EXTERN_GUID(GUID_ClassName, 0x77631a31, 0xe5e7, 0x4785, 0xbf, 0x17, 0x20, 0xf5, 0x7b, 0x22, 0x48, 0x02); +EXTERN_GUID(CLSID_EMEStoreActivate, 0x2df7b51e, 0x797b, 0x4d06, 0xbe, 0x71, 0xd1, 0x4a, 0x52, 0xcf, 0x84, 0x21); + +class PMPHostWrapper : public winrt::implements { +public: + explicit PMPHostWrapper(winrt::com_ptr& host) { + std::swap(host, m_spIMFPMPHost); + } + ~PMPHostWrapper() override = default; + + IFACEMETHODIMP LockProcess() override { + return m_spIMFPMPHost->LockProcess(); + } + + IFACEMETHODIMP UnlockProcess() override { + return m_spIMFPMPHost->UnlockProcess(); + } + + IFACEMETHODIMP ActivateClassById(LPCWSTR id, IStream* stream, REFIID riid, void** activated_class) override { + HRESULT ret = S_OK; + + wchar_t guid[MAX_PATH] = {}; + StringFromGUID2(riid, guid, MAX_PATH); + + winrt::com_ptr creation_attributes; + ret = MFCreateAttributes(creation_attributes.put(), 3); + if (FAILED(ret)) + return ret; + + ret = creation_attributes->SetString(GUID_ClassName, id); + if (FAILED(ret)) + return ret; + + if (stream) { + STATSTG statStg; + + ret = stream->Stat(&statStg, STATFLAG_NOOPEN | STATFLAG_NONAME); + if (FAILED(ret)) + return ret; + + std::vector stream_blob(statStg.cbSize.LowPart); + ULONG read_size = 0; + + ret = stream->Read(&stream_blob[0], static_cast(stream_blob.size()), &read_size); + if (FAILED(ret)) + return ret; + + ret = creation_attributes->SetBlob(GUID_ObjectStream, &stream_blob[0], read_size); + if (FAILED(ret)) + return ret; + } + + // Serialize attributes + winrt::com_ptr output_stream; + ret = CreateStreamOnHGlobal(nullptr, TRUE, output_stream.put()); + if (FAILED(ret)) + return ret; + + ret = MFSerializeAttributesToStream(creation_attributes.get(), 0, output_stream.get()); + if (FAILED(ret)) + return ret; + + ret = output_stream->Seek({}, STREAM_SEEK_SET, nullptr); + if (FAILED(ret)) + return ret; + + winrt::com_ptr activator; + ret = m_spIMFPMPHost->CreateObjectByCLSID(CLSID_EMEStoreActivate, output_stream.get(), + IID_PPV_ARGS(&activator)); + if (FAILED(ret)) + return ret; + + ret = activator->ActivateObject(riid, activated_class); + if (FAILED(ret)) + return ret; + + return S_OK; + } +private: + winrt::com_ptr m_spIMFPMPHost; +}; diff --git a/lib/mfcdm/mfcdm/utils/ScopedCoMem.h b/lib/mfcdm/mfcdm/utils/ScopedCoMem.h new file mode 100644 index 000000000..1ea5f9d47 --- /dev/null +++ b/lib/mfcdm/mfcdm/utils/ScopedCoMem.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2023 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 + +namespace UTILS +{ + +/*! + * \brief Class to automatically release memory allocated in COM. + */ +template +class ScopedCoMem +{ +public: + ScopedCoMem() : m_ptr(nullptr) {} + + ~ScopedCoMem() { Reset(); } + + ScopedCoMem(const ScopedCoMem&) = delete; + ScopedCoMem& operator=(const ScopedCoMem&) = delete; + + inline T* operator->() { return m_ptr; } + inline T** operator&() { return &m_ptr; } + + void Reset() + { + if (m_ptr) + CoTaskMemFree(m_ptr); + m_ptr = nullptr; + } + + T* get() const { return m_ptr; } + +private: + T* m_ptr; +}; + +} // namespace UTILS diff --git a/lib/mfcdm/mfcdm/utils/ScopedPropVariant.h b/lib/mfcdm/mfcdm/utils/ScopedPropVariant.h new file mode 100644 index 000000000..ca64cc958 --- /dev/null +++ b/lib/mfcdm/mfcdm/utils/ScopedPropVariant.h @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2023 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 +#include + +namespace UTILS +{ + +/*! + * \brief A MS PROPVARIANT that is automatically initialized and cleared + * upon respective construction and destruction of this class. +*/ +class ScopedPropVariant +{ +public: + ScopedPropVariant() { PropVariantInit(&pv_); } + + ScopedPropVariant(const ScopedPropVariant&) = delete; + ScopedPropVariant& operator=(const ScopedPropVariant&) = delete; + + ~ScopedPropVariant() { Reset(); } + + /*! + * \brief Clears the instance & prepares it for re-use (e.g., via Receive). + */ + void Reset() + { + if (pv_.vt == VT_EMPTY) + return; + + HRESULT result = PropVariantClear(&pv_); + assert(result == S_OK); + } + + inline PROPVARIANT* operator->() { return &pv_; } + + const PROPVARIANT& get() const + { + assert(pv_.vt == VT_EMPTY); + return pv_; + } + + /*! + * \brief Returns a pointer to the underlying PROPVARIANT. + * Example: Use as an out param in a function call. + */ + PROPVARIANT* ptr() + { + assert(pv_.vt == VT_EMPTY); + return &pv_; + } + + PROPVARIANT release() noexcept + { + PROPVARIANT value(pv_); + PropVariantInit(&pv_); + return value; + } + + const PROPVARIANT* ptr() const + { + assert(pv_.vt == VT_EMPTY); + return &pv_; + } + +private: + PROPVARIANT pv_; +}; + +} // namespace UTILS diff --git a/lib/mfcdm/mfcdm/utils/Wide.h b/lib/mfcdm/mfcdm/utils/Wide.h new file mode 100644 index 000000000..ad8866a13 --- /dev/null +++ b/lib/mfcdm/mfcdm/utils/Wide.h @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2023 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 + +#define NOGDI // Ignore useless header that creates useless macros +#include + +namespace UTILS +{ + + namespace WIDE + { + + static std::wstring ConvertUtf8ToWide(std::string_view str) + { + const int charCount = + MultiByteToWideChar(CP_UTF8, 0, str.data(), static_cast(str.length()), nullptr, 0); + if (charCount <= 0) + return {}; + + std::wstring wide(charCount, 0); + MultiByteToWideChar(CP_UTF8, 0, str.data(), static_cast(str.length()), wide.data(), + charCount); + return wide; + } + + static std::string ConvertWideToUTF8(std::wstring_view wstr) + { + const int charCount = WideCharToMultiByte( + CP_UTF8, 0, wstr.data(), static_cast(wstr.length()), nullptr, 0, nullptr, nullptr); + if (charCount <= 0) + return {}; + + std::string str(charCount, 0); + WideCharToMultiByte(CP_UTF8, 0, wstr.data(), static_cast(wstr.length()), str.data(), + charCount, nullptr, nullptr); + return str; + } + + } //namespace WIDE + +} //namespace UTILS diff --git a/src/decrypters/CMakeLists.txt b/src/decrypters/CMakeLists.txt index e2606bd39..4533bbac7 100644 --- a/src/decrypters/CMakeLists.txt +++ b/src/decrypters/CMakeLists.txt @@ -18,3 +18,17 @@ if(NOT CORE_SYSTEM_NAME STREQUAL ios AND NOT CORE_SYSTEM_NAME STREQUAL darwin_em add_subdirectory(widevine) endif() endif() + +if(CORE_SYSTEM_NAME STREQUAL windows) + include(CheckCXXSourceCompiles) + check_cxx_source_compiles([=[ + #include + static_assert(WDK_NTDDI_VERSION >= NTDDI_WIN10_VB, "Inspecting WDK_NTDDI_VERSION, the Windows SDK version."); + int main() {}]=] WINDOWS_SDK_VERSION_CHECK) + + if(NOT WINDOWS_SDK_VERSION_CHECK) + message(WARNING "MediaFoundation (for Playready) is only available with Windows 10 SDK (10.0.19041) or later.") + else() + add_subdirectory(mediafoundation) + endif() +endif() diff --git a/src/decrypters/DrmFactory.cpp b/src/decrypters/DrmFactory.cpp index 7ee3b2388..0f4bf6075 100644 --- a/src/decrypters/DrmFactory.cpp +++ b/src/decrypters/DrmFactory.cpp @@ -17,6 +17,13 @@ #endif #endif +#if _WIN32 +#include +#if WDK_NTDDI_VERSION >= NTDDI_WIN10_VB // Windows SDK higher than Windows 20H2 +#include "mediafoundation/MFDecrypter.h" +#endif +#endif + using namespace DRM; IDecrypter* DRM::FACTORY::GetDecrypter(STREAM_CRYPTO_KEY_SYSTEM keySystem) @@ -32,8 +39,15 @@ IDecrypter* DRM::FACTORY::GetDecrypter(STREAM_CRYPTO_KEY_SYSTEM keySystem) #endif #endif } - else if (keySystem == STREAM_CRYPTO_KEY_SYSTEM_PLAYREADY || - keySystem == STREAM_CRYPTO_KEY_SYSTEM_WISEPLAY) + else if (keySystem == STREAM_CRYPTO_KEY_SYSTEM_PLAYREADY) + { +#if ANDROID + return new CWVDecrypterA(); +#elif _WIN32 && WDK_NTDDI_VERSION >= NTDDI_WIN10_VB + return new CMFDecrypter(); +#endif + } + else if (keySystem == STREAM_CRYPTO_KEY_SYSTEM_WISEPLAY) { #if ANDROID return new CWVDecrypterA(); diff --git a/src/decrypters/mediafoundation/CMakeLists.txt b/src/decrypters/mediafoundation/CMakeLists.txt new file mode 100644 index 000000000..0db992993 --- /dev/null +++ b/src/decrypters/mediafoundation/CMakeLists.txt @@ -0,0 +1,13 @@ +set(SOURCES + MFDecrypter.cpp + MFCencSingleSampleDecrypter.cpp +) + +set(HEADERS + MFDecrypter.h + MFCencSingleSampleDecrypter.h +) + +add_dir_sources(SOURCES HEADERS) + +add_dependency(mfcdm_library lib/mfcdm) diff --git a/src/decrypters/mediafoundation/MFCencSingleSampleDecrypter.cpp b/src/decrypters/mediafoundation/MFCencSingleSampleDecrypter.cpp new file mode 100644 index 000000000..c563557f1 --- /dev/null +++ b/src/decrypters/mediafoundation/MFCencSingleSampleDecrypter.cpp @@ -0,0 +1,465 @@ +/* + * Copyright (C) 2023 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 "MFCencSingleSampleDecrypter.h" + +#include "MFDecrypter.h" +#include "utils/Base64Utils.h" +#include "utils/CurlUtils.h" +#include "utils/DigestMD5Utils.h" +#include "utils/FileUtils.h" +#include "utils/StringUtils.h" +#include "utils/Utils.h" +#include "utils/log.h" +#include "utils/XMLUtils.h" +#include "pugixml.hpp" + +#include + +#include +#include + +using namespace pugi; +using namespace kodi::tools; +using namespace UTILS; + +CMFCencSingleSampleDecrypter::CMFCencSingleSampleDecrypter(CMFDecrypter& host, + std::vector& pssh, + std::string_view defaultKeyId, + bool skipSessionMessage, + CryptoMode cryptoMode) + : m_host(host), + m_pssh(pssh), + m_hdcpVersion(99), + m_hdcpLimit(0), + m_resolutionLimit(0), + m_promiseId(1), + m_isDrained(true), + m_defaultKeyId(defaultKeyId), + m_EncryptionMode(cryptoMode) +{ + SetParentIsOwner(false); + + if (pssh.size() > 4096) + { + LOG::LogF(LOGERROR, "PSSH init data with length %u seems not to be cenc init data", + pssh.size()); + return; + } + + if (m_host.IsDebugSaveLicense()) + { + const std::string debugFilePath = + FILESYS::PathCombine(m_host.GetProfilePath(), "9A04F079-9840-4286-AB92-E65BE0885F95.init"); + + std::string data{reinterpret_cast(pssh.data()), pssh.size()}; + FILESYS::SaveFile(debugFilePath, data, true); + } + + // No cenc init data with PSSH box format, create one + if (memcmp(pssh.data() + 4, "pssh", 4) != 0) + { + // PSSH box version 0 (no kid's) + static const uint8_t atomHeader[12] = {0x00, 0x00, 0x00, 0x00, 0x70, 0x73, + 0x73, 0x68, 0x00, 0x00, 0x00, 0x00}; + + static const uint8_t playReadySystemId[16] = {0x9A, 0x04, 0xF0, 0x79, 0x98, 0x40, 0x42, 0x86, + 0xAB, 0x92, 0xE6, 0x5B, 0xE0, 0x88, 0x5F, 0x95}; + + std::vector psshAtom; + psshAtom.assign(atomHeader, atomHeader + 12); // PSSH Box header + psshAtom.insert(psshAtom.end(), playReadySystemId, playReadySystemId + 16); // System ID + // Add data size bytes + psshAtom.resize(30, 0); // 2 zero bytes + psshAtom.emplace_back(static_cast((pssh.size()) >> 8)); + psshAtom.emplace_back(static_cast(pssh.size())); + + psshAtom.insert(psshAtom.end(), pssh.begin(), pssh.end()); // Data + // Update box size + psshAtom[2] = static_cast(psshAtom.size() >> 8); + psshAtom[3] = static_cast(psshAtom.size()); + m_pssh = psshAtom; + } + + m_host.GetCdm()->CreateSessionAndGenerateRequest(MFTemporary, MFCenc, m_pssh, this); + + if (sessionId.empty()) + { + LOG::LogF(LOGERROR, "Cannot perform License update, no session available"); + return; + } + +} + +CMFCencSingleSampleDecrypter::~CMFCencSingleSampleDecrypter() +{ +} + +void CMFCencSingleSampleDecrypter::ParsePlayReadyMessage(const std::vector& message, + std::string& challenge, + std::map& headers) +{ + xml_document doc; + + // Load wide string XML + xml_parse_result parseRes = doc.load_buffer(message.data(), message.size()); + if (parseRes.status != status_ok) + { + LOG::LogF(LOGERROR, "Failed to parse PlayReady session message %i", parseRes.status); + return; + } + + if (m_host.IsDebugSaveLicense()) + { + const std::string debugFilePath = FILESYS::PathCombine( + m_host.GetProfilePath(), "9A04F079-9840-4286-AB92-E65BE0885F95.message"); + + doc.save_file(debugFilePath.c_str()); + } + + xml_node nodeAcquisition = doc.first_element_by_path("PlayReadyKeyMessage/LicenseAcquisition"); + if (!nodeAcquisition) + { + LOG::LogF(LOGERROR, "Failed to get Playready's tag element."); + return; + } + + xml_node nodeChallenge = nodeAcquisition.child("Challenge"); + if (!nodeChallenge) + { + LOG::LogF(LOGERROR, "Failed to get Playready's tag element."); + return; + } + + std::string encodingType; + encodingType = XML::GetAttrib(nodeChallenge, "encoding"); + if (encodingType != "base64encoded") + { + LOG::LogF(LOGERROR, "Unknown challenge encoding %s", encodingType); + return; + } + + challenge = BASE64::DecodeToStr(nodeChallenge.child_value()); + + LOG::LogF(LOGDEBUG, "Challenge: encoding %s size %i", encodingType, challenge.size()); + + if (xml_node nodeHeaders = nodeAcquisition.child("HttpHeaders")) + { + for (xml_node nodeHeader : nodeHeaders.children("HttpHeader")) + { + std::string name = nodeHeader.child_value("name"); + std::string value = nodeHeader.child_value("value"); + headers.insert({name, value}); + } + } + + LOG::LogF(LOGDEBUG, "HttpHeaders: size %i", headers.size()); +} + +void CMFCencSingleSampleDecrypter::OnSessionMessage(std::string_view session, + const std::vector& message, + std::string_view messageDestinationUrl) +{ + std::string challenge; + std::map playReadyHeaders; + + ParsePlayReadyMessage(message, challenge, + playReadyHeaders); + + sessionId = session; + m_challenge.SetData(reinterpret_cast(challenge.data()), + static_cast(challenge.size())); + + LOG::LogF(LOGDEBUG, "Playready message session ID: %s", sessionId.c_str()); + + if (m_host.IsDebugSaveLicense()) + { + std::string debugFilePath = FILESYS::PathCombine( + m_host.GetProfilePath(), "9A04F079-9840-4286-AB92-E65BE0885F95.challenge"); + + FILESYS::SaveFile(debugFilePath, challenge, true); + } + + std::vector blocks; + if (!m_host.GetLicenseKey().empty()) + { + blocks = StringUtils::Split(m_host.GetLicenseKey(), '|'); + if (blocks.size() != 4) + { + LOG::LogF(LOGERROR, "Wrong \"|\" blocks in license URL. Four blocks (req | header | body | " + "response) are expected in license URL"); + return; + } + } + + std::string destinationUrl; + if (!blocks.empty()) + { + destinationUrl = blocks[0]; + } + else + { + destinationUrl = messageDestinationUrl; + } + + CURL::CUrl file(destinationUrl); + file.AddHeader("Expect", ""); + + for (const auto& header: playReadyHeaders) + { + file.AddHeader(header.first, header.second); + } + + //Process headers + if(!blocks.empty()) + { + std::vector headers{StringUtils::Split(blocks[1], '&')}; + for (std::string& headerStr : headers) + { + std::vector header{StringUtils::Split(headerStr, '=')}; + if (!header.empty()) + { + StringUtils::Trim(header[0]); + std::string value; + if (header.size() > 1) + { + StringUtils::Trim(header[1]); + value = STRING::URLDecode(header[1]); + } + file.AddHeader(header[0].c_str(), value.c_str()); + } + } + } + + std::string encData{BASE64::Encode(challenge)}; + file.AddHeader("postdata", encData); + + int statusCode = file.Open(); + if (statusCode == -1 || statusCode >= 400) + { + LOG::Log(LOGERROR, "License server returned failure"); + return; + } + + std::string response; + + CURL::ReadStatus downloadStatus = CURL::ReadStatus::CHUNK_READ; + while (downloadStatus == CURL::ReadStatus::CHUNK_READ) + { + downloadStatus = file.Read(response); + } + + if (downloadStatus == CURL::ReadStatus::ERROR) + { + LOG::LogF(LOGERROR, "Could not read full SessionMessage response"); + return; + } + + if (m_host.IsDebugSaveLicense()) + { + std::string debugFilePath = FILESYS::PathCombine( + m_host.GetProfilePath(), "9A04F079-9840-4286-AB92-E65BE0885F95.response"); + FILESYS::SaveFile(debugFilePath, response, true); + } + + m_host.GetCdm()->UpdateSession( + sessionId, std::vector(response.data(), response.data() + response.size())); +} + +void CMFCencSingleSampleDecrypter::OnKeyChange(std::string_view sessionId, + std::vector> keys) +{ + LOG::LogF(LOGDEBUG, "Received %i keys", keys.size()); + for (const auto& key : keys) + { + char buf[36]; + buf[32] = 0; + AP4_FormatHex(key->keyId.data(), key->keyId.size(), buf); + + LOG::LogF(LOGDEBUG, "Key: %s status: %i", buf, key->status); + } + m_keys = std::move(keys); +} + +void CMFCencSingleSampleDecrypter::GetCapabilities(std::string_view key, + uint32_t media, + IDecrypter::DecrypterCapabilites& caps) +{ + caps = {IDecrypter::DecrypterCapabilites::SSD_SECURE_PATH | + IDecrypter::DecrypterCapabilites::SSD_ANNEXB_REQUIRED, + 0, m_hdcpLimit}; + + if (sessionId.empty()) + { + LOG::LogF(LOGDEBUG, "Session empty"); + return; + } + + if (m_keys.empty()) + { + LOG::LogF(LOGDEBUG, "Keys empty"); + return; + } + + if (!caps.hdcpLimit) + caps.hdcpLimit = m_resolutionLimit; +} + +const char* CMFCencSingleSampleDecrypter::GetSessionId() +{ + return sessionId.empty() ? nullptr : sessionId.c_str(); +} + +void CMFCencSingleSampleDecrypter::CloseSessionId() +{ + if (!sessionId.empty()) + { + LOG::LogF(LOGDEBUG, "Closing MF session ID: %s", sessionId.c_str()); + //m_wvCdmAdapter.GetCdmAdapter()->CloseSession(++m_promiseId, sessionId.data(), + // sessionId.size()); + + LOG::LogF(LOGDEBUG, "MF session ID %s closed", sessionId.c_str()); + sessionId.clear(); + } +} + +AP4_DataBuffer CMFCencSingleSampleDecrypter::GetChallengeData() +{ + return m_challenge; +} + +bool CMFCencSingleSampleDecrypter::HasKeyId(std::string_view keyId) +{ + if (!keyId.empty()) + { + for (const std::unique_ptr& key : m_keys) + { + if (key->keyId == STRING::ToVecUint8(keyId)) + return true; + } + } + return false; +} + +AP4_Result CMFCencSingleSampleDecrypter::SetFragmentInfo(AP4_UI32 poolId, + const std::vector& keyId, + const AP4_UI08 nalLengthSize, + AP4_DataBuffer& annexbSpsPps, + AP4_UI32 flags, + CryptoInfo cryptoInfo) +{ + if (poolId >= m_fragmentPool.size()) + return AP4_ERROR_OUT_OF_RANGE; + + m_fragmentPool[poolId].m_key = keyId; + m_fragmentPool[poolId].m_nalLengthSize = nalLengthSize; + m_fragmentPool[poolId].m_annexbSpsPps.SetData(annexbSpsPps.GetData(), annexbSpsPps.GetDataSize()); + m_fragmentPool[poolId].m_decrypterFlags = flags; + m_fragmentPool[poolId].m_cryptoInfo = cryptoInfo; + + return AP4_SUCCESS; +} + +AP4_UI32 CMFCencSingleSampleDecrypter::AddPool() +{ + for (size_t i(0); i < m_fragmentPool.size(); ++i) + if (m_fragmentPool[i].m_nalLengthSize == 99) + { + m_fragmentPool[i].m_nalLengthSize = 0; + return i; + } + m_fragmentPool.push_back(FINFO()); + m_fragmentPool.back().m_nalLengthSize = 0; + return static_cast(m_fragmentPool.size() - 1); +} + + +void CMFCencSingleSampleDecrypter::RemovePool(AP4_UI32 poolId) +{ + m_fragmentPool[poolId].m_nalLengthSize = 99; + m_fragmentPool[poolId].m_key.clear(); +} + +void CMFCencSingleSampleDecrypter::RepackSubsampleData(AP4_DataBuffer& dataIn, + AP4_DataBuffer& dataOut, + size_t& pos, + size_t& cipherPos, + const unsigned int subsamplePos, + const AP4_UI16* bytesOfCleartextData, + const AP4_UI32* bytesOfEncryptedData) +{ + dataOut.AppendData(dataIn.GetData() + pos, bytesOfCleartextData[subsamplePos]); + pos += bytesOfCleartextData[subsamplePos]; + dataOut.AppendData(m_decryptOut.GetData() + cipherPos, bytesOfEncryptedData[subsamplePos]); + pos += bytesOfEncryptedData[subsamplePos]; + cipherPos += bytesOfEncryptedData[subsamplePos]; +} + +void CMFCencSingleSampleDecrypter::UnpackSubsampleData(AP4_DataBuffer& dataIn, + size_t& pos, + const unsigned int subsamplePos, + const AP4_UI16* bytesOfCleartextData, + const AP4_UI32* bytesOfEncryptedData) +{ + pos += bytesOfCleartextData[subsamplePos]; + m_decryptIn.AppendData(dataIn.GetData() + pos, bytesOfEncryptedData[subsamplePos]); + pos += bytesOfEncryptedData[subsamplePos]; +} + +/*---------------------------------------------------------------------- +| CWVCencSingleSampleDecrypter::DecryptSampleData ++---------------------------------------------------------------------*/ +AP4_Result CMFCencSingleSampleDecrypter::DecryptSampleData(AP4_UI32 poolId, + AP4_DataBuffer& dataIn, + AP4_DataBuffer& dataOut, + const AP4_UI08* iv, + unsigned int subsampleCount, + const AP4_UI16* bytesOfCleartextData, + const AP4_UI32* bytesOfEncryptedData) +{ + return AP4_ERROR_INVALID_PARAMETERS; +} + +bool CMFCencSingleSampleDecrypter::OpenVideoDecoder(const VIDEOCODEC_INITDATA* initData) +{ + return false; +} + +VIDEOCODEC_RETVAL CMFCencSingleSampleDecrypter::DecryptAndDecodeVideo( + kodi::addon::CInstanceVideoCodec* codecInstance, const DEMUX_PACKET* sample) +{ + return VC_ERROR; +} + +VIDEOCODEC_RETVAL CMFCencSingleSampleDecrypter::VideoFrameDataToPicture( + kodi::addon::CInstanceVideoCodec* codecInstance, VIDEOCODEC_PICTURE* picture) +{ + return VC_BUFFER; +} + +void CMFCencSingleSampleDecrypter::ResetVideo() +{ + //m_wvCdmAdapter.GetCdmAdapter()->ResetDecoder(cdm::kStreamTypeVideo); + m_isDrained = true; +} + +void CMFCencSingleSampleDecrypter::SetDefaultKeyId(std::string_view keyId) +{ + m_defaultKeyId = keyId; +} + +void CMFCencSingleSampleDecrypter::AddKeyId(std::string_view keyId) +{ + std::unique_ptr key = std::make_unique( + std::vector(keyId.data(), keyId.data() + keyId.size()), MFKeyUsable); + + if (std::find(m_keys.begin(), m_keys.end(), key) == m_keys.end()) + { + m_keys.push_back(std::move(key)); + } +} diff --git a/src/decrypters/mediafoundation/MFCencSingleSampleDecrypter.h b/src/decrypters/mediafoundation/MFCencSingleSampleDecrypter.h new file mode 100644 index 000000000..f19365eba --- /dev/null +++ b/src/decrypters/mediafoundation/MFCencSingleSampleDecrypter.h @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2023 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 "../../common/AdaptiveCencSampleDecrypter.h" +#include "../IDecrypter.h" + +#include + +#include +#include +#include +#include + +class CMFDecrypter; +class CWVCdmAdapter; + +using namespace DRM; + +class ATTR_DLL_LOCAL CMFCencSingleSampleDecrypter : public Adaptive_CencSingleSampleDecrypter, + public SessionClient +{ +public: + // methods + CMFCencSingleSampleDecrypter(CMFDecrypter& host, + std::vector& pssh, + std::string_view defaultKeyId, + bool skipSessionMessage, + CryptoMode cryptoMode); + ~CMFCencSingleSampleDecrypter() override; + + void GetCapabilities(std::string_view key, + uint32_t media, + IDecrypter::DecrypterCapabilites& caps); + + virtual const char* GetSessionId() override; + void CloseSessionId(); + AP4_DataBuffer GetChallengeData(); + + void OnSessionMessage(std::string_view session, + const std::vector& message, + std::string_view destinationUrl) override; + void OnKeyChange(std::string_view sessionId, std::vector> keys) override; + + bool HasKeyId(std::string_view keyId); + + virtual AP4_Result SetFragmentInfo(AP4_UI32 poolId, + const std::vector& keyId, + const AP4_UI08 nalLengthSize, + AP4_DataBuffer& annexbSpsPps, + AP4_UI32 flags, + CryptoInfo cryptoInfo) override; + + virtual AP4_UI32 AddPool() override; + virtual void RemovePool(AP4_UI32 poolId) override; + + virtual AP4_Result DecryptSampleData( + AP4_UI32 poolId, + AP4_DataBuffer& dataIn, + AP4_DataBuffer& dataOut, + + // always 16 bytes + const AP4_UI08* iv, + + // pass 0 for full decryption + unsigned int subsampleCount, + + // array of integers. NULL if subsample_count is 0 + const AP4_UI16* bytesOfCleartextData, + + // array of integers. NULL if subsample_count is 0 + const AP4_UI32* bytesOfEncryptedData) override; + + bool OpenVideoDecoder(const VIDEOCODEC_INITDATA* initData); + VIDEOCODEC_RETVAL DecryptAndDecodeVideo(kodi::addon::CInstanceVideoCodec* codecInstance, + const DEMUX_PACKET* sample); + VIDEOCODEC_RETVAL VideoFrameDataToPicture(kodi::addon::CInstanceVideoCodec* codecInstance, + VIDEOCODEC_PICTURE* picture); + void ResetVideo(); + void SetDefaultKeyId(std::string_view keyId) override; + void AddKeyId(std::string_view keyId) override; + +private: + void ParsePlayReadyMessage(const std::vector& message, + std::string& challenge, + std::map& headers); + + CMFDecrypter& m_host; + + std::string sessionId; + std::vector m_pssh; + AP4_DataBuffer m_challenge; + std::string m_defaultKeyId; + std::vector> m_keys; + + AP4_UI16 m_hdcpVersion; + int m_hdcpLimit; + int m_resolutionLimit; + + AP4_DataBuffer m_decryptIn; + AP4_DataBuffer m_decryptOut; + + struct FINFO + { + std::vector m_key; + AP4_UI08 m_nalLengthSize; + AP4_UI16 m_decrypterFlags; + AP4_DataBuffer m_annexbSpsPps; + CryptoInfo m_cryptoInfo; + }; + std::vector m_fragmentPool; + //void LogDecryptError(const cdm::Status status, const AP4_UI08* key); + //void SetCdmSubsamples(std::vector& subsamples, bool isCbc); + void RepackSubsampleData(AP4_DataBuffer& dataIn, + AP4_DataBuffer& dataOut, + size_t& startPos, + size_t& cipherPos, + const unsigned int subsamplePos, + const AP4_UI16* bytesOfCleartextData, + const AP4_UI32* bytesOfEncryptedData); + void UnpackSubsampleData(AP4_DataBuffer& dataIn, + size_t& startPos, + const unsigned int subsamplePos, + const AP4_UI16* bytesOfCleartextData, + const AP4_UI32* bytesOfEncryptedData); + //void SetInput(cdm::InputBuffer_2& cdmInputBuffer, + // const AP4_DataBuffer& inputData, + // const unsigned int subsampleCount, + // const uint8_t* iv, + // const FINFO& fragInfo, + // const std::vector& subsamples); + uint32_t m_promiseId; + bool m_isDrained; + + //std::list m_videoFrames; + CryptoMode m_EncryptionMode; + + //std::optional m_currentVideoDecConfig; + +}; diff --git a/src/decrypters/mediafoundation/MFDecrypter.cpp b/src/decrypters/mediafoundation/MFDecrypter.cpp new file mode 100644 index 000000000..bad909cbb --- /dev/null +++ b/src/decrypters/mediafoundation/MFDecrypter.cpp @@ -0,0 +1,200 @@ +#include "MFDecrypter.h" +#include "MFCencSingleSampleDecrypter.h" +#include "../../utils/Base64Utils.h" +#include "../../utils/log.h" + +#include + +#include +#include + +using namespace UTILS; +using namespace DRM; +using namespace kodi::tools; + +namespace +{ +void MFLog(int level, char* msg) +{ + if (msg[std::strlen(msg) - 1] == '\n') + msg[std::strlen(msg) - 1] = '\0'; + + switch (level) + { + case MFCDM::MFLOG_ERROR: + LOG::Log(LOGERROR, msg); + break; + case MFCDM::MFLOG_WARN: + LOG::Log(LOGWARNING, msg); + break; + case MFCDM::MFLOG_INFO: + LOG::Log(LOGINFO, msg); + break; + case MFCDM::MFLOG_DEBUG: + LOG::Log(LOGDEBUG, msg); + break; + default: + break; + } +} +} // unnamed namespace + + +CMFDecrypter::CMFDecrypter() + : m_cdm(nullptr) +{ + MFCDM::LogAll(); + MFCDM::SetMFMsgCallback(MFLog); +} + +CMFDecrypter::~CMFDecrypter() +{ + delete m_cdm; +} + +bool CMFDecrypter::Initialize() +{ + m_cdm = new MediaFoundationCdm(); + return m_cdm != nullptr; +} + +std::string CMFDecrypter::SelectKeySytem(std::string_view keySystem) +{ + if (keySystem == "com.microsoft.playready") + return "urn:uuid:9A04F079-9840-4286-AB92-E65BE0885F95"; + return ""; +} + +bool CMFDecrypter::OpenDRMSystem(std::string_view licenseURL, + const std::vector& serverCertificate, + const uint8_t config) +{ + if (!m_cdm) + return false; + + if (!(config & DRM::IDecrypter::CONFIG_PERSISTENTSTORAGE)) + { + LOG::Log(LOGERROR, "MF PlayReady requires persistent storage to be optionally on or required."); + return false; + } + + m_strLicenseKey = licenseURL; + + return m_cdm->Initialize({true, true}, "com.microsoft.playready.recommendation", + m_strProfilePath); +} + +Adaptive_CencSingleSampleDecrypter* CMFDecrypter::CreateSingleSampleDecrypter( + std::vector& pssh, + std::string_view optionalKeyParameter, + std::string_view defaultKeyId, + bool skipSessionMessage, + CryptoMode cryptoMode) +{ + CMFCencSingleSampleDecrypter* decrypter = new CMFCencSingleSampleDecrypter( + *this, pssh, defaultKeyId, skipSessionMessage, cryptoMode); + if (!decrypter->GetSessionId()) + { + delete decrypter; + decrypter = nullptr; + } + return decrypter; +} + +void CMFDecrypter::DestroySingleSampleDecrypter(Adaptive_CencSingleSampleDecrypter* decrypter) +{ + if (decrypter) + { + // close session before dispose + dynamic_cast(decrypter)->CloseSessionId(); + delete dynamic_cast(decrypter); + } +} + +void CMFDecrypter::GetCapabilities(Adaptive_CencSingleSampleDecrypter* decrypter, + std::string_view keyId, + uint32_t media, + IDecrypter::DecrypterCapabilites& caps) +{ + if (!decrypter) + { + caps = {0, 0, 0}; + return; + } + + dynamic_cast(decrypter)->GetCapabilities(keyId, media, caps); +} + +bool CMFDecrypter::HasLicenseKey(Adaptive_CencSingleSampleDecrypter* decrypter, + std::string_view keyId) +{ + if (decrypter) + return dynamic_cast(decrypter)->HasKeyId(keyId); + return false; +} + +std::string CMFDecrypter::GetChallengeB64Data(Adaptive_CencSingleSampleDecrypter* decrypter) +{ + if (!decrypter) + return ""; + + AP4_DataBuffer challengeData = + dynamic_cast(decrypter)->GetChallengeData(); + return BASE64::Encode(challengeData.GetData(), challengeData.GetDataSize()); +} + +bool CMFDecrypter::OpenVideoDecoder(Adaptive_CencSingleSampleDecrypter* decrypter, + const VIDEOCODEC_INITDATA* initData) +{ + if (!decrypter || !initData) + return false; + + m_decodingDecrypter = dynamic_cast(decrypter); + return m_decodingDecrypter->OpenVideoDecoder(initData); +} + +VIDEOCODEC_RETVAL CMFDecrypter::DecryptAndDecodeVideo( + kodi::addon::CInstanceVideoCodec* codecInstance, const DEMUX_PACKET* sample) +{ + if (!m_decodingDecrypter) + return VC_ERROR; + + return m_decodingDecrypter->DecryptAndDecodeVideo(codecInstance, sample); +} + +VIDEOCODEC_RETVAL CMFDecrypter::VideoFrameDataToPicture( + kodi::addon::CInstanceVideoCodec* codecInstance, VIDEOCODEC_PICTURE* picture) +{ + if (!m_decodingDecrypter) + return VC_ERROR; + + return m_decodingDecrypter->VideoFrameDataToPicture(codecInstance, picture); +} + +void CMFDecrypter::ResetVideo() +{ + if (m_decodingDecrypter) + m_decodingDecrypter->ResetVideo(); +} + +void CMFDecrypter::SetProfilePath(const std::string& profilePath) +{ + m_strProfilePath = profilePath; + + const char* pathSep{profilePath[0] && profilePath[1] == ':' && isalpha(profilePath[0]) ? "\\" + : "/"}; + + if (!m_strProfilePath.empty() && m_strProfilePath.back() != pathSep[0]) + m_strProfilePath += pathSep; + + //let us make cdm userdata out of the addonpath and share them between addons + m_strProfilePath.resize(m_strProfilePath.find_last_of(pathSep[0], m_strProfilePath.length() - 2)); + m_strProfilePath.resize(m_strProfilePath.find_last_of(pathSep[0], m_strProfilePath.length() - 1)); + m_strProfilePath.resize(m_strProfilePath.find_last_of(pathSep[0], m_strProfilePath.length() - 1) + + 1); + + kodi::vfs::CreateDirectory(m_strProfilePath); + m_strProfilePath += "cdm"; + m_strProfilePath += pathSep; + kodi::vfs::CreateDirectory(m_strProfilePath); +} diff --git a/src/decrypters/mediafoundation/MFDecrypter.h b/src/decrypters/mediafoundation/MFDecrypter.h new file mode 100644 index 000000000..0adc8f270 --- /dev/null +++ b/src/decrypters/mediafoundation/MFDecrypter.h @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2023 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 "../IDecrypter.h" +#include "mfcdm/MediaFoundationCdm.h" + +using namespace DRM; +using namespace kodi::tools; + +class MediaFoundationCdm; +class CMFCencSingleSampleDecrypter; + +/*********************************************************************************************/ + +class ATTR_DLL_LOCAL CMFDecrypter : public IDecrypter +{ +public: + CMFDecrypter(); + ~CMFDecrypter() override; + + bool Initialize() override; + + std::string SelectKeySytem(std::string_view keySystem) override; + + bool OpenDRMSystem(std::string_view licenseURL, + const std::vector& serverCertificate, + const uint8_t config) override; + + Adaptive_CencSingleSampleDecrypter* CreateSingleSampleDecrypter( + std::vector& pssh, + std::string_view optionalKeyParameter, + std::string_view defaultKeyId, + bool skipSessionMessage, + CryptoMode cryptoMode) override; + + void DestroySingleSampleDecrypter(Adaptive_CencSingleSampleDecrypter* decrypter) override; + + void GetCapabilities(Adaptive_CencSingleSampleDecrypter* decrypter, + std::string_view keyId, + uint32_t media, + IDecrypter::DecrypterCapabilites& caps) override; + + bool HasLicenseKey(Adaptive_CencSingleSampleDecrypter* decrypter, + std::string_view keyId) override; + + std::string GetChallengeB64Data(Adaptive_CencSingleSampleDecrypter* decrypter) override; + + virtual bool OpenVideoDecoder(Adaptive_CencSingleSampleDecrypter* decrypter, + const VIDEOCODEC_INITDATA* initData) override; + + virtual VIDEOCODEC_RETVAL DecryptAndDecodeVideo(kodi::addon::CInstanceVideoCodec* codecInstance, + const DEMUX_PACKET* sample) override; + virtual VIDEOCODEC_RETVAL VideoFrameDataToPicture(kodi::addon::CInstanceVideoCodec* codecInstance, + VIDEOCODEC_PICTURE* picture) override; + virtual void ResetVideo() override; + + void SetLibraryPath(const char* libraryPath) override {}; + void SetProfilePath(const std::string& profilePath) override; + bool IsInitialised() override + { + if (!m_cdm) + return false; + return m_cdm->IsInitialized(); + } + + void SetDebugSaveLicense(bool isDebugSaveLicense) override + { + m_isDebugSaveLicense = isDebugSaveLicense; + } + + const bool IsDebugSaveLicense() const override { return m_isDebugSaveLicense; } + const char* GetLibraryPath() const override { return m_strLibraryPath.c_str(); } + const char* GetProfilePath() const override { return m_strProfilePath.c_str(); } + + MediaFoundationCdm* GetCdm() const { return m_cdm; } + + std::string GetLicenseKey() const { return m_strLicenseKey; } + +private: + MediaFoundationCdm* m_cdm; + CMFCencSingleSampleDecrypter* m_decodingDecrypter; + std::string m_strProfilePath; + std::string m_strLibraryPath; + std::string m_strLicenseKey; + bool m_isDebugSaveLicense; +}; diff --git a/src/parser/DASHTree.cpp b/src/parser/DASHTree.cpp index 365ae900a..0e454f8a1 100644 --- a/src/parser/DASHTree.cpp +++ b/src/parser/DASHTree.cpp @@ -1298,6 +1298,7 @@ bool adaptive::CDashTree::ParseTagContentProtection(pugi::xml_node nodeParent, PRProtectionParser parser; if (parser.ParseHeader(node.child_value())) protScheme.kid = parser.GetKID(); + protScheme.pssh = node.child_value(); } } protectionSchemes.emplace_back(protScheme);