From 4320230a6fe402ac70e1ea4a97c0feeb6bbaa1dd Mon Sep 17 00:00:00 2001 From: TheDaChicken Date: Wed, 18 Oct 2023 21:49:12 -0700 Subject: [PATCH] Properly compiles --- lib/mfcdm/CMakeLists.txt | 15 + lib/mfcdm/mfcdm/Log.cpp | 47 ++ lib/mfcdm/mfcdm/Log.h | 17 + lib/mfcdm/mfcdm/MediaFoundationCdm.cpp | 123 ++++ lib/mfcdm/mfcdm/MediaFoundationCdm.h | 60 ++ lib/mfcdm/mfcdm/MediaFoundationCdmConfig.h | 32 + lib/mfcdm/mfcdm/MediaFoundationCdmFactory.cpp | 244 +++++++ lib/mfcdm/mfcdm/MediaFoundationCdmFactory.h | 40 ++ lib/mfcdm/mfcdm/MediaFoundationCdmModule.h | 46 ++ lib/mfcdm/mfcdm/MediaFoundationCdmSession.cpp | 101 +++ lib/mfcdm/mfcdm/MediaFoundationCdmSession.h | 28 + lib/mfcdm/mfcdm/MediaFoundationSession.cpp | 32 + lib/mfcdm/mfcdm/MediaFoundationSession.h | 21 + lib/mfcdm/mfcdm/utils/PMPHostWrapper.h | 90 +++ lib/mfcdm/mfcdm/utils/ScopedPropVariant.h | 47 ++ src/decrypters/CMakeLists.txt | 14 + src/decrypters/DrmFactory.cpp | 18 +- src/decrypters/mediafoundation/CMakeLists.txt | 13 + .../MFCencSingleSampleDecrypter.cpp | 628 ++++++++++++++++++ .../MFCencSingleSampleDecrypter.h | 152 +++++ .../mediafoundation/MFDecrypter.cpp | 200 ++++++ src/decrypters/mediafoundation/MFDecrypter.h | 90 +++ 22 files changed, 2056 insertions(+), 2 deletions(-) create mode 100644 lib/mfcdm/CMakeLists.txt create mode 100644 lib/mfcdm/mfcdm/Log.cpp create mode 100644 lib/mfcdm/mfcdm/Log.h create mode 100644 lib/mfcdm/mfcdm/MediaFoundationCdm.cpp create mode 100644 lib/mfcdm/mfcdm/MediaFoundationCdm.h create mode 100644 lib/mfcdm/mfcdm/MediaFoundationCdmConfig.h create mode 100644 lib/mfcdm/mfcdm/MediaFoundationCdmFactory.cpp create mode 100644 lib/mfcdm/mfcdm/MediaFoundationCdmFactory.h create mode 100644 lib/mfcdm/mfcdm/MediaFoundationCdmModule.h create mode 100644 lib/mfcdm/mfcdm/MediaFoundationCdmSession.cpp create mode 100644 lib/mfcdm/mfcdm/MediaFoundationCdmSession.h create mode 100644 lib/mfcdm/mfcdm/MediaFoundationSession.cpp create mode 100644 lib/mfcdm/mfcdm/MediaFoundationSession.h create mode 100644 lib/mfcdm/mfcdm/utils/PMPHostWrapper.h create mode 100644 lib/mfcdm/mfcdm/utils/ScopedPropVariant.h create mode 100644 src/decrypters/mediafoundation/CMakeLists.txt create mode 100644 src/decrypters/mediafoundation/MFCencSingleSampleDecrypter.cpp create mode 100644 src/decrypters/mediafoundation/MFCencSingleSampleDecrypter.h create mode 100644 src/decrypters/mediafoundation/MFDecrypter.cpp create mode 100644 src/decrypters/mediafoundation/MFDecrypter.h diff --git a/lib/mfcdm/CMakeLists.txt b/lib/mfcdm/CMakeLists.txt new file mode 100644 index 000000000..b317c50bb --- /dev/null +++ b/lib/mfcdm/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 3.10) +project(mfcdm_library) + +add_library(mfcdm_library STATIC + mfcdm/MediaFoundationCdm.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..b0e008e4b --- /dev/null +++ b/lib/mfcdm/mfcdm/MediaFoundationCdm.cpp @@ -0,0 +1,123 @@ +/* + * 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 "Log.h" +#include "MediaFoundationCdmFactory.h" +#include "MediaFoundationCdmModule.h" +#include "MediaFoundationCdmSession.h" + +#include "utils/PMPHostWrapper.h" + +MediaFoundationCdm::~MediaFoundationCdm() = default; + +bool MediaFoundationCdm::Initialize(const std::string &keySystem, + const std::string &basePath, + const media::CdmConfig &cdmConfig, + media::CdmAdapterClient* client) +{ + 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(); + + m_client = client; + 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()); +} + +void MediaFoundationCdm::SetServerCertificate(uint32_t promise_id, + const uint8_t* serverCertificateData, + uint32_t serverCertificateDataSize) const +{ + m_module->SetServerCertificate(serverCertificateData, serverCertificateDataSize); +} + +void MediaFoundationCdm::CreateSessionAndGenerateRequest(uint32_t promise_id, cdm::SessionType sessionType, + cdm::InitDataType initDataType, const uint8_t *init_data, + uint32_t init_data_size) +{ + auto session = std::make_unique(); + + if (!session->Initialize(sessionType, m_module.get())) + { + Log(MFCDM::MFLOG_ERROR, "Failed to create session."); + return; + } + + int session_token = next_session_token_++; + session->GenerateRequest(initDataType, init_data, init_data_size); + + m_cdm_sessions.emplace(session_token, std::move(session)); +} + +void MediaFoundationCdm::LoadSession(cdm::SessionType session_type, + const std::string &session_id) +{ + +} + +void MediaFoundationCdm::UpdateSession(const std::string &session_id) +{ + +} + + + + diff --git a/lib/mfcdm/mfcdm/MediaFoundationCdm.h b/lib/mfcdm/mfcdm/MediaFoundationCdm.h new file mode 100644 index 000000000..8e94af382 --- /dev/null +++ b/lib/mfcdm/mfcdm/MediaFoundationCdm.h @@ -0,0 +1,60 @@ +/* + * 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 +#include +#include + +#include "cdm/media/base/cdm_config.h" +#include "cdm/media/cdm/api/content_decryption_module.h" +#include "cdm/media/cdm/cdm_adapter.h" + +class MediaFoundationCdmSession; +class MediaFoundationCdmFactory; +class MediaFoundationCdmModule; + +class MediaFoundationCdm { +public: + ~MediaFoundationCdm(); + + bool IsInitialized() const { return m_module != nullptr; } + + bool Initialize(const std::string& keySystem, + const std::string &basePath, + const media::CdmConfig &cdmConfig, + media::CdmAdapterClient* client); + + void SetServerCertificate(uint32_t promise_id, + const uint8_t* serverCertificateData, + uint32_t serverCertificateDataSize) const; + + void CreateSessionAndGenerateRequest(uint32_t promise_id, + cdm::SessionType sessionType, + cdm::InitDataType initDataType, + const uint8_t* init_data, + uint32_t init_data_size); + + void LoadSession(cdm::SessionType session_type, const std::string& session_id); + + void UpdateSession(const std::string& session_id); +private: + void SetupPMPServer() const; + + MediaFoundationSession m_session; + + std::unique_ptr m_module{nullptr}; + + int next_session_token_{0}; + std::map> m_cdm_sessions; + + media::CdmAdapterClient* m_client{nullptr}; +}; 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..2ef9c4c94 --- /dev/null +++ b/lib/mfcdm/mfcdm/MediaFoundationCdmFactory.cpp @@ -0,0 +1,244 @@ +/* + * 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 "Log.h" + +#include "utils/ScopedPropVariant.h" + +#include +#include + +#include +#include +#include + +static void InitPropVariantFromBSTR(const wchar_t* str, PROPVARIANT* propvariant) { + propvariant->vt = VT_BSTR; + propvariant->bstrVal = SysAllocString(str); +} + +static std::wstring ConvertUtf8ToWide(const std::string& str) +{ + const int count = + MultiByteToWideChar(CP_UTF8, 0, str.c_str(), static_cast(str.length()), nullptr, 0); + std::wstring wstr(count, 0); + MultiByteToWideChar(CP_UTF8, 0, str.c_str(), static_cast(str.length()), wstr.data(), + count); + return wstr; +} + +MediaFoundationCdmFactory::MediaFoundationCdmFactory(std::string key_system) + : m_keySystem(std::move(key_system)) +{ +} + +bool MediaFoundationCdmFactory::Initialize() +{ + const winrt::com_ptr class_factory = winrt::create_instance( + CLSID_MFMediaEngineClassFactory, CLSCTX_INPROC_SERVER); + const std::wstring key_system_str = ConvertUtf8ToWide(m_keySystem); + + return SUCCEEDED(class_factory->CreateContentDecryptionModuleFactory( + key_system_str.c_str(), IID_PPV_ARGS(&m_cdmFactory))); +} + +bool MediaFoundationCdmFactory::IsTypeSupported(const std::string &key_system) const { + return m_cdmFactory->IsTypeSupported(ConvertUtf8ToWide(key_system).c_str(), nullptr); +} + +// Returns a property store similar to EME MediaKeySystemMediaCapability. +bool CreateVideoCapability(const media::CdmConfig& 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; + } + + ScopedPropVariant robustness; + if (cdm_config.use_hw_secure_codecs) { + PROPVARIANT* ptr = robustness.Receive(); + ptr->vt = VT_BSTR; + ptr->bstrVal = SysAllocString(L"HW_SECURE_ALL"); + temp_video_capability->SetValue(MF_EME_ROBUSTNESS, robustness.get()); + } + video_capability = temp_video_capability; +} + +/*! + * \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 media::CdmConfig& cdm_config, + 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; + } + + PROPVARIANT* receive; + + // Add an empty audio capability. + ScopedPropVariant audio_capabilities; + PROPVARIANT* var_to_set = audio_capabilities.Receive(); + var_to_set->vt = VT_VARIANT | VT_VECTOR; + var_to_set->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(cdm_config, video_capability)) + return false; + + ScopedPropVariant video_config; + auto* video_config_ptr = video_config.Receive(); + video_config_ptr->vt = VT_UNKNOWN; + video_config_ptr->punkVal = video_capability.detach(); + + ScopedPropVariant video_capabilities; + var_to_set = video_capabilities.Receive(); + var_to_set->vt = VT_VARIANT | VT_VECTOR; + var_to_set->capropvar.cElems = 1; + var_to_set->capropvar.pElems = + static_cast(CoTaskMemAlloc(sizeof(PROPVARIANT))); + if (!var_to_set->capropvar.pElems) + throw std::bad_alloc(); + + if (FAILED(PropVariantCopy(var_to_set->capropvar.pElems, video_config.ptr()))) + { + Log(MFCDM::MFLOG_ERROR, "Failed to set copy video config into video capabilities."); + return false; + } + + if (FAILED( + temp_configurations->SetValue(MF_EME_VIDEOCAPABILITIES, video_capabilities.get()))) + { + Log(MFCDM::MFLOG_ERROR, "Failed to set persisted state."); + return false; + } + + // Persistent state + ScopedPropVariant persisted_state; + + if (FAILED(InitPropVariantFromUInt32(cdm_config.allow_persistent_state + ? MF_MEDIAKEYS_REQUIREMENT_REQUIRED + : MF_MEDIAKEYS_REQUIREMENT_NOT_ALLOWED, + persisted_state.Receive()))) + { + 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(cdm_config.allow_distinctive_identifier + ? MF_MEDIAKEYS_REQUIREMENT_REQUIRED + : MF_MEDIAKEYS_REQUIREMENT_NOT_ALLOWED, + allow_distinctive_identifier.Receive()))) + { + 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 id."); + return false; + } + + properties = temp_configurations; + return true; +} + +bool BuildCdmProperties(const std::filesystem::path& store_path, + 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 store_path_var; + InitPropVariantFromBSTR(store_path.wstring().c_str(), store_path_var.Receive()); + + if (FAILED(temp_properties->SetValue(MF_EME_CDM_STOREPATH, store_path_var.get()))) + { + Log(MFCDM::MFLOG_ERROR, "Failed to set CDM Storage Path."); + return false; + } + + properties = temp_properties; + return true; +} + +bool MediaFoundationCdmFactory::CreateMfCdm(const media::CdmConfig& cdm_config, + const std::filesystem::path& cdm_path, + std::unique_ptr& mf_cdm) const +{ + const auto key_system_str = ConvertUtf8ToWide(m_keySystem); + if (!m_cdmFactory->IsTypeSupported(key_system_str.c_str(), nullptr)) + { + Log(MFCDM::MFLOG_ERROR, "%s not supported by MF CdmFactory", m_keySystem); + return false; + } + + if (!std::filesystem::create_directory(cdm_path) && !std::filesystem::exists(cdm_path)) + return false; + + winrt::com_ptr cdm_access; + + winrt::com_ptr property_store; + if (!BuildCdmAccessConfigurations(cdm_config, property_store)) + return false; + + IPropertyStore* configurations[] = {property_store.get()}; + if (FAILED(m_cdmFactory->CreateContentDecryptionModuleAccess( + key_system_str.c_str(), configurations, ARRAYSIZE(configurations), cdm_access.put()))) + { + Log(MFCDM::MFLOG_ERROR, "Failed to create module access."); + return false; + } + + winrt::com_ptr cdm_properties; + if (!BuildCdmProperties(cdm_path, cdm_properties)) + return false; + + winrt::com_ptr cdm; + if (FAILED(cdm_access->CreateContentDecryptionModule(cdm_properties.get(), cdm.put()))) + { + Log(MFCDM::MFLOG_ERROR, "Failed to create cdm module."); + return false; + } + + mf_cdm = 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..5e127dbce --- /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 +#include +#include + +#include +#include + +#include +#include + +#include + +class MediaFoundationCdmModule; + +class MediaFoundationCdmFactory { +public: + explicit MediaFoundationCdmFactory(std::string key_system); + bool Initialize(); + + bool IsTypeSupported(const std::string& key_system) const; + + bool CreateMfCdm(const media::CdmConfig& cdm_config, + const std::filesystem::path& cdm_path, + std::unique_ptr& mf_cdm) 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..ee494c77d --- /dev/null +++ b/lib/mfcdm/mfcdm/MediaFoundationCdmSession.cpp @@ -0,0 +1,101 @@ +/* + * 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 "Log.h" + +#include +#include + +#include +#include +#include +#include + +MF_MEDIAKEYSESSION_TYPE ToMFSessionType(cdm::SessionType session_type) +{ + switch (session_type) + { + case cdm::SessionType::kPersistentLicense: + return MF_MEDIAKEYSESSION_TYPE_PERSISTENT_LICENSE; + case cdm::SessionType::kTemporary: + default: + return MF_MEDIAKEYSESSION_TYPE_TEMPORARY; + } +} + +/*! + * \link https://www.w3.org/TR/eme-initdata-registry/ + */ +LPCWSTR InitDataTypeToString(cdm::InitDataType init_data_type) +{ + switch (init_data_type) + { + case cdm::InitDataType::kWebM: + return L"webm"; + case cdm::InitDataType::kCenc: + return L"cenc"; + case cdm::InitDataType::kKeyIds: + return L"keyids"; + default: + return L"unknown"; + } +} + +class SessionCallbacks : public winrt::implements< + SessionCallbacks, IMFContentDecryptionModuleSessionCallbacks> +{ +public: + SessionCallbacks() = default; + + IFACEMETHODIMP KeyMessage(MF_MEDIAKEYSESSION_MESSAGETYPE message_type, + const BYTE* message, + DWORD message_size, + LPCWSTR destination_url) final + { + std::wstring messageStr = std::wstring(reinterpret_cast(message), message_size); + //std::wcout << "KeyMessage: " << messageStr << std::endl; + Log(MFCDM::MFLOG_DEBUG, "Destination Url %S", destination_url); + return S_OK; + } + + IFACEMETHODIMP KeyStatusChanged() final + { + std::cout << "KeyStatusChanged" << std::endl; + return S_OK; + } +}; + +bool MediaFoundationCdmSession::Initialize(cdm::SessionType session_type, + MediaFoundationCdmModule* mf_cdm) +{ + const winrt::com_ptr + session_callbacks = winrt::make(); + // |mf_cdm_session_| holds a ref count to |session_callbacks|. + if (FAILED(mf_cdm->CreateSession(ToMFSessionType(session_type), session_callbacks.get(), + mfCdmSession.put()))) + { + Log(MFCDM::MFLOG_ERROR, "Failed to create MF CDM session."); + return false; + } + return true; +} + +void MediaFoundationCdmSession::GenerateRequest(cdm::InitDataType init_data_type, const uint8_t *init_data, + uint32_t init_data_size) +{ + if (FAILED(mfCdmSession->GenerateRequest(InitDataTypeToString(init_data_type), init_data, + init_data_size))) + { + Log(MFCDM::MFLOG_ERROR, "Failed to generate MF CDM request."); + return; + } + +} diff --git a/lib/mfcdm/mfcdm/MediaFoundationCdmSession.h b/lib/mfcdm/mfcdm/MediaFoundationCdmSession.h new file mode 100644 index 000000000..90e8fc07a --- /dev/null +++ b/lib/mfcdm/mfcdm/MediaFoundationCdmSession.h @@ -0,0 +1,28 @@ +/* + * 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 + +#include + +class MediaFoundationCdmModule; + +class MediaFoundationCdmSession { +public: + bool Initialize(cdm::SessionType session_type, MediaFoundationCdmModule* mf_cdm); + void GenerateRequest(cdm::InitDataType init_data_type, + const uint8_t* init_data, uint32_t init_data_size); +private: + winrt::com_ptr mfCdmSession; +}; 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..3b0bc66f0 --- /dev/null +++ b/lib/mfcdm/mfcdm/utils/PMPHostWrapper.h @@ -0,0 +1,90 @@ +/* + * 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; + + wchar_t guid[MAX_PATH] = {}; + StringFromGUID2(riid, guid, std::size(guid)); + + 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); + unsigned long read_size = 0; + + ret = stream->Read(&stream_blob[0], 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/ScopedPropVariant.h b/lib/mfcdm/mfcdm/utils/ScopedPropVariant.h new file mode 100644 index 000000000..075cd000d --- /dev/null +++ b/lib/mfcdm/mfcdm/utils/ScopedPropVariant.h @@ -0,0 +1,47 @@ +// Copyright (c) 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#pragma once + +#include +#include + +/*! + * \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 Returns a pointer to the underlying PROPVARIANT. + * Example: Use as an out param in a function call. + */ + PROPVARIANT* Receive() { + assert(pv_.vt == VT_EMPTY); + return &pv_; + } + + /*! + * \brief Clears the instance to prepare it for re-use (e.g., via Receive). + */ + void Reset() { + if (pv_.vt != VT_EMPTY) { + HRESULT result = PropVariantClear(&pv_); + assert(result == S_OK); + } + } + + [[nodiscard]] const PROPVARIANT& get() const { return pv_; } + [[nodiscard]] const PROPVARIANT* ptr() const { return &pv_; } + +private: + PROPVARIANT pv_; +}; diff --git a/src/decrypters/CMakeLists.txt b/src/decrypters/CMakeLists.txt index fbd013a59..9fdf1b0d1 100644 --- a/src/decrypters/CMakeLists.txt +++ b/src/decrypters/CMakeLists.txt @@ -16,3 +16,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 11 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..40636f0af --- /dev/null +++ b/src/decrypters/mediafoundation/MFCencSingleSampleDecrypter.cpp @@ -0,0 +1,628 @@ +/* + * 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 "../../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 "MFDecrypter.h" +#include "mfcdm/MediaFoundationCdm.h" + +#include +#include + +using namespace kodi::tools; +using namespace UTILS; + +void CMFCencSingleSampleDecrypter::SetSession(const char* session, + uint32_t sessionSize, + const uint8_t* data, + size_t dataSize) +{ + std::lock_guard lock(m_renewalLock); + + m_strSession = std::string(session, sessionSize); + m_challenge.SetData(data, dataSize); + LOG::LogF(LOGDEBUG, "Opened widevine session ID: %s", m_strSession.c_str()); +} + +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; + } + + //m_wvCdmAdapter.insertssd(this); + + if (m_host.IsDebugSaveLicense()) + { + std::string debugFilePath = + FILESYS::PathCombine(m_host.GetProfilePath(), "9A04F079-9840-4286-AB92-E65BE0885F95.init"); + + std::string data{reinterpret_cast(pssh.data()), pssh.size()}; + UTILS::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( + m_promiseId++, cdm::SessionType::kTemporary, cdm::InitDataType::kCenc, + m_pssh.data(), m_pssh.size()); + + int retryCount = 0; + while (m_strSession.empty() && ++retryCount < 100) + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + if (m_strSession.empty()) + { + LOG::LogF(LOGERROR, "Cannot perform License update, no session available"); + return; + } + + if (skipSessionMessage) + return; + + while (m_challenge.GetDataSize() > 0 && SendSessionMessage()) + ; +} + +CMFCencSingleSampleDecrypter::~CMFCencSingleSampleDecrypter() +{ + //m_wvCdmAdapter.removessd(this); +} + +void CMFCencSingleSampleDecrypter::GetCapabilities(std::string_view key, + uint32_t media, + IDecrypter::DecrypterCapabilites& caps) +{ + caps = {0, m_hdcpVersion, m_hdcpLimit}; + + if (m_strSession.empty()) + { + LOG::LogF(LOGDEBUG, "Session empty"); + return; + } + + caps.flags = IDecrypter::DecrypterCapabilites::SSD_SUPPORTS_DECODING; + + if (m_keys.empty()) + { + LOG::LogF(LOGDEBUG, "Keys empty"); + return; + } + + if (!caps.hdcpLimit) + caps.hdcpLimit = m_resolutionLimit; + +} + +const char* CMFCencSingleSampleDecrypter::GetSessionId() +{ + return m_strSession.empty() ? nullptr : m_strSession.c_str(); +} + +void CMFCencSingleSampleDecrypter::CloseSessionId() +{ + if (!m_strSession.empty()) + { + LOG::LogF(LOGDEBUG, "Closing widevine session ID: %s", m_strSession.c_str()); + //m_wvCdmAdapter.GetCdmAdapter()->CloseSession(++m_promiseId, m_strSession.data(), + // m_strSession.size()); + + LOG::LogF(LOGDEBUG, "MF session ID %s closed", m_strSession.c_str()); + m_strSession.clear(); + } +} + +AP4_DataBuffer CMFCencSingleSampleDecrypter::GetChallengeData() +{ + return m_challenge; +} + +void CMFCencSingleSampleDecrypter::CheckLicenseRenewal() +{ + { + std::lock_guard lock(m_renewalLock); + if (!m_challenge.GetDataSize()) + return; + } + SendSessionMessage(); +} + +bool CMFCencSingleSampleDecrypter::SendSessionMessage() +{ + // StringUtils::Split(m_wvCdmAdapter.GetLicenseURL(), '|') + std::vector blocks{}; + + if (blocks.size() != 4) + { + LOG::LogF(LOGERROR, "Wrong \"|\" blocks in license URL. Four blocks (req | header | body | " + "response) are expected in license URL"); + return false; + } + + if (m_host.IsDebugSaveLicense()) + { + std::string debugFilePath = FILESYS::PathCombine( + m_host.GetProfilePath(), "9A04F079-9840-4286-AB92-E65BE0885F95.challenge"); + std::string data{reinterpret_cast(m_challenge.GetData()), + m_challenge.GetDataSize()}; + UTILS::FILESYS::SaveFile(debugFilePath, data, true); + } + + //Process placeholder in GET String + std::string::size_type insPos(blocks[0].find("{SSM}")); + if (insPos != std::string::npos) + { + if (insPos > 0 && blocks[0][insPos - 1] == 'B') + { + std::string msgEncoded{BASE64::Encode(m_challenge.GetData(), m_challenge.GetDataSize())}; + msgEncoded = STRING::URLEncode(msgEncoded); + blocks[0].replace(insPos - 1, 6, msgEncoded); + } + else + { + LOG::Log(LOGERROR, "Unsupported License request template (command)"); + return false; + } + } + + insPos = blocks[0].find("{HASH}"); + if (insPos != std::string::npos) + { + DIGEST::MD5 md5; + md5.Update(m_challenge.GetData(), m_challenge.GetDataSize()); + md5.Finalize(); + blocks[0].replace(insPos, 6, md5.HexDigest()); + } + + CURL::CUrl file{blocks[0].c_str()}; + file.AddHeader("Expect", ""); + + std::string response; + std::string resLimit; + std::string contentType; + char buf[2048]; + bool serverCertRequest; + + //Process headers + 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()); + } + } + + //Process body + if (!blocks[2].empty()) + { + if (blocks[2][0] == '%') + blocks[2] = STRING::URLDecode(blocks[2]); + + insPos = blocks[2].find("{SSM}"); + if (insPos != std::string::npos) + { + std::string::size_type sidPos(blocks[2].find("{SID}")); + std::string::size_type kidPos(blocks[2].find("{KID}")); + + char fullDecode = 0; + if (insPos > 1 && sidPos > 1 && kidPos > 1 && (blocks[2][0] == 'b' || blocks[2][0] == 'B') && + blocks[2][1] == '{') + { + fullDecode = blocks[2][0]; + blocks[2] = blocks[2].substr(2, blocks[2].size() - 3); + insPos -= 2; + if (kidPos != std::string::npos) + kidPos -= 2; + if (sidPos != std::string::npos) + sidPos -= 2; + } + + size_t size_written(0); + + if (insPos > 0) + { + if (blocks[2][insPos - 1] == 'B' || blocks[2][insPos - 1] == 'b') + { + std::string msgEncoded{BASE64::Encode(m_challenge.GetData(), m_challenge.GetDataSize())}; + if (blocks[2][insPos - 1] == 'B') + { + msgEncoded = STRING::URLEncode(msgEncoded); + } + blocks[2].replace(insPos - 1, 6, msgEncoded); + size_written = msgEncoded.size(); + } + else if (blocks[2][insPos - 1] == 'D') + { + std::string msgEncoded{ + STRING::ToDecimal(m_challenge.GetData(), m_challenge.GetDataSize())}; + blocks[2].replace(insPos - 1, 6, msgEncoded); + size_written = msgEncoded.size(); + } + else + { + blocks[2].replace(insPos - 1, 6, reinterpret_cast(m_challenge.GetData()), + m_challenge.GetDataSize()); + size_written = m_challenge.GetDataSize(); + } + } + else + { + LOG::Log(LOGERROR, "Unsupported License request template (body / ?{SSM})"); + return false; + } + + if (sidPos != std::string::npos && insPos < sidPos) + sidPos += size_written, sidPos -= 6; + + if (kidPos != std::string::npos && insPos < kidPos) + kidPos += size_written, kidPos -= 6; + + size_written = 0; + + if (sidPos != std::string::npos) + { + if (sidPos > 0) + { + if (blocks[2][sidPos - 1] == 'B' || blocks[2][sidPos - 1] == 'b') + { + std::string msgEncoded{BASE64::Encode(m_strSession)}; + + if (blocks[2][sidPos - 1] == 'B') + { + msgEncoded = STRING::URLEncode(msgEncoded); + } + + blocks[2].replace(sidPos - 1, 6, msgEncoded); + size_written = msgEncoded.size(); + } + else + { + blocks[2].replace(sidPos - 1, 6, m_strSession.data(), m_strSession.size()); + size_written = m_strSession.size(); + } + } + else + { + LOG::LogF(LOGERROR, "Unsupported License request template (body / ?{SID})"); + return false; + } + } + + if (kidPos != std::string::npos) + { + if (sidPos < kidPos) + kidPos += size_written, kidPos -= 6; + + if (blocks[2][kidPos - 1] == 'H') + { + std::string keyIdUUID{StringUtils::ToHexadecimal(m_defaultKeyId)}; + blocks[2].replace(kidPos - 1, 6, keyIdUUID.c_str(), 32); + } + else + { + std::string kidUUID{ConvertKIDtoUUID(m_defaultKeyId)}; + blocks[2].replace(kidPos, 5, kidUUID.c_str(), 36); + } + } + + if (fullDecode) + { + std::string msgEncoded{BASE64::Encode(blocks[2])}; + if (fullDecode == 'B') + { + msgEncoded = STRING::URLEncode(msgEncoded); + } + blocks[2] = msgEncoded; + } + } + + std::string encData{BASE64::Encode(blocks[2])}; + file.AddHeader("postdata", encData.c_str()); + } + + serverCertRequest = m_challenge.GetDataSize() == 2; + m_challenge.SetDataSize(0); + + if (!file.Open()) + { + LOG::Log(LOGERROR, "License server returned failure"); + return false; + } + + CURL::ReadStatus downloadStatus = CURL::ReadStatus::CHUNK_READ; + while (downloadStatus == CURL::ReadStatus::CHUNK_READ) + { + downloadStatus = file.Read(response); + } + + resLimit = file.GetResponseHeader("X-Limit-Video"); + contentType = file.GetResponseHeader("Content-Type"); + + if (!resLimit.empty()) + { + std::string::size_type posMax = resLimit.find("max="); // log/check this + if (posMax != std::string::npos) + m_resolutionLimit = std::atoi(resLimit.data() + (posMax + 4)); + } + + if (downloadStatus == CURL::ReadStatus::ERROR) + { + LOG::LogF(LOGERROR, "Could not read full SessionMessage response"); + return false; + } + + if (m_host.IsDebugSaveLicense()) + { + std::string debugFilePath = FILESYS::PathCombine( + m_host.GetProfilePath(), "9A04F079-9840-4286-AB92-E65BE0885F95.response"); + FILESYS::SaveFile(debugFilePath, response, true); + } + + if (serverCertRequest && contentType.find("application/octet-stream") == std::string::npos) + serverCertRequest = false; + + //m_wvCdmAdapter.GetCdmAdapter()->UpdateSession( + // ++m_promiseId, m_strSession.data(), m_strSession.size(), + // reinterpret_cast(response.data()), response.size()); + + if (m_keys.empty()) + { + LOG::LogF(LOGERROR, "License update not successful (no keys)"); + CloseSessionId(); + return false; + } + + LOG::Log(LOGDEBUG, "License update successful"); + return true; +} + +void CMFCencSingleSampleDecrypter::AddSessionKey(const uint8_t* data, + size_t dataSize, + uint32_t status) +{ + WVSKEY key; + std::vector::iterator res; + + key.m_keyId = std::string((const char*)data, dataSize); + if ((res = std::find(m_keys.begin(), m_keys.end(), key)) == m_keys.end()) + res = m_keys.insert(res, key); + res->status = static_cast(status); +} + +bool CMFCencSingleSampleDecrypter::HasKeyId(std::string_view keyid) +{ + if (!keyid.empty()) + { + for (const WVSKEY& key : m_keys) + { + if (key.m_keyId == 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::LogDecryptError(const cdm::Status status, const AP4_UI08* key) +{ + char buf[36]; + buf[32] = 0; + AP4_FormatHex(key, 16, buf); + LOG::LogF(LOGDEBUG, "Decrypt failed with error: %d and key: %s", status, buf); +} + +void CMFCencSingleSampleDecrypter::SetCdmSubsamples(std::vector& subsamples, + bool isCbc) +{ + if (isCbc) + { + subsamples.resize(1); + subsamples[0] = {0, m_decryptIn.GetDataSize()}; + } + else + { + subsamples.push_back({0, m_decryptIn.GetDataSize()}); + } +} + +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]; +} + +void CMFCencSingleSampleDecrypter::SetInput(cdm::InputBuffer_2& cdmInputBuffer, + const AP4_DataBuffer& inputData, + const unsigned int subsampleCount, + const uint8_t* iv, + const FINFO& fragInfo, + const std::vector& subsamples) +{ + cdmInputBuffer.data = inputData.GetData(); + cdmInputBuffer.data_size = inputData.GetDataSize(); + cdmInputBuffer.num_subsamples = subsampleCount; + cdmInputBuffer.iv = iv; + cdmInputBuffer.iv_size = 16; //Always 16, see AP4_CencSingleSampleDecrypter declaration. + cdmInputBuffer.key_id = fragInfo.m_key.data(); + cdmInputBuffer.key_id_size = 16; + cdmInputBuffer.subsamples = subsamples.data(); + //cdmInputBuffer.encryption_scheme = media::ToCdmEncryptionScheme(fragInfo.m_cryptoInfo.m_mode); + cdmInputBuffer.timestamp = 0; + cdmInputBuffer.pattern = {fragInfo.m_cryptoInfo.m_cryptBlocks, + fragInfo.m_cryptoInfo.m_skipBlocks}; +} + +/*---------------------------------------------------------------------- +| 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) +{ + WVSKEY key; + key.m_keyId = keyId; + key.status = cdm::KeyStatus::kUsable; + + if (std::find(m_keys.begin(), m_keys.end(), key) == m_keys.end()) + { + m_keys.push_back(key); + } +} diff --git a/src/decrypters/mediafoundation/MFCencSingleSampleDecrypter.h b/src/decrypters/mediafoundation/MFCencSingleSampleDecrypter.h new file mode 100644 index 000000000..9537bb04b --- /dev/null +++ b/src/decrypters/mediafoundation/MFCencSingleSampleDecrypter.h @@ -0,0 +1,152 @@ +/* + * 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 "cdm/media/cdm/api/content_decryption_module.h" + +#include +#include +#include + +class CMFDecrypter; +class CWVCdmAdapter; + +namespace media +{ +class CdmVideoFrame; +} + +using namespace DRM; + +class ATTR_DLL_LOCAL CMFCencSingleSampleDecrypter : public Adaptive_CencSingleSampleDecrypter +{ +public: + // methods + CMFCencSingleSampleDecrypter(CMFDecrypter& host, + std::vector& pssh, + std::string_view defaultKeyId, + bool skipSessionMessage, + CryptoMode cryptoMode); + virtual ~CMFCencSingleSampleDecrypter(); + + void GetCapabilities(std::string_view key, + uint32_t media, + IDecrypter::DecrypterCapabilites& caps); + + virtual const char* GetSessionId() override; + void CloseSessionId(); + AP4_DataBuffer GetChallengeData(); + + void SetSession(const char* session, uint32_t sessionSize, const uint8_t* data, size_t dataSize); + + void AddSessionKey(const uint8_t* data, size_t dataSize, uint32_t status); + 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 CheckLicenseRenewal(); + bool SendSessionMessage(); + + CMFDecrypter& m_host; + + std::string m_strSession; + std::vector m_pssh; + AP4_DataBuffer m_challenge; + std::string m_defaultKeyId; + struct WVSKEY + { + bool operator==(WVSKEY const& other) const { return m_keyId == other.m_keyId; }; + std::string m_keyId; + cdm::KeyStatus status; + }; + 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; + std::mutex m_renewalLock; + 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..c417ace8a --- /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 + +#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 != NULL; +} + +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; + } + + return m_cdm->Initialize("com.microsoft.playready.recommendation", m_strProfilePath, {true, true}, + nullptr); +} + +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..17071bc4d --- /dev/null +++ b/src/decrypters/mediafoundation/MFDecrypter.h @@ -0,0 +1,90 @@ +/* + * 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; } + +private: + MediaFoundationCdm* m_cdm; + CMFCencSingleSampleDecrypter* m_decodingDecrypter; + + std::string m_strProfilePath; + std::string m_strLibraryPath; + + bool m_isDebugSaveLicense; +};