diff --git a/.gitignore b/.gitignore index fb476d8f0..e914eb1ce 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # build artifacts -build/ +!*/build/ +/build/ inputstream.*/addon.xml # Debian build files diff --git a/CMakeLists.txt b/CMakeLists.txt index cb6256ad8..e767c2ac3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,12 @@ cmake_minimum_required(VERSION 3.10) + +if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") + # require at least gcc 4.8 + if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 4.8) + message(FATAL_ERROR "GCC version must be at least 4.8!") + endif() +endif() + project(inputstream.adaptive) option(BUILD_TESTING "Build the testing tree." ON) @@ -6,6 +14,32 @@ set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PROJECT_SOURCE_DIR}) find_package(Kodi REQUIRED) +# Function to add source files to the main file list +function(add_dir_sources source_files header_files) + foreach(_source ${${source_files}}) + if (IS_ABSOLUTE "${_source}") + set(_source_abs "${_source}") + else() + get_filename_component(_source_abs "${_source}" ABSOLUTE) + endif() + set_property(GLOBAL APPEND PROPERTY GlobalSourceList "${_source_abs}") + endforeach() + foreach(_header ${${header_files}}) + if (IS_ABSOLUTE "${_header}") + set(_header_abs "${_header}") + else() + get_filename_component(_header_abs "${_header}" ABSOLUTE) + endif() + set_property(GLOBAL APPEND PROPERTY GlobalHeaderList "${_header_abs}") + endforeach() +endfunction(add_dir_sources) + +# Function to add an additional dependency +function(add_dependency project_name folder) + set_property(GLOBAL APPEND PROPERTY GlobalDepsNamesList "${project_name}") + set_property(GLOBAL APPEND PROPERTY GlobalDepsFoldersList "${folder}") +endfunction(add_dependency) + set(ADP_SOURCES src/AdaptiveByteStream.cpp src/main.cpp @@ -61,7 +95,6 @@ set(ADP_SOURCES src/utils/UrlUtils.cpp src/utils/Utils.cpp src/utils/XMLUtils.cpp - src/KodiHost.cpp src/oscompat.cpp src/Session.cpp src/Stream.cpp @@ -75,7 +108,6 @@ set(ADP_HEADERS src/AdaptiveByteStream.h src/main.h src/oscompat.h - src/SSD_dll.h src/codechandler/CodecHandler.h src/codechandler/AudioCodecHandler.h src/codechandler/AV1CodecHandler.h @@ -132,7 +164,6 @@ set(ADP_HEADERS src/utils/UrlUtils.h src/utils/Utils.h src/utils/XMLUtils.h - src/KodiHost.h src/Session.h src/Stream.h src/TSReader.h @@ -142,6 +173,8 @@ set(ADP_HEADERS src/WebmReader.h ) +add_subdirectory(src/decrypters) + if(WIN32) find_package(dlfcn-win32 REQUIRED) list(APPEND DEPLIBS ${dlfcn-win32_LIBRARIES}) @@ -169,20 +202,11 @@ add_definitions(-DUNICODE -D_UNICODE) include_directories(${PUGIXML_INCLUDE_DIRS}) -if(CORE_SYSTEM_NAME STREQUAL ios OR CORE_SYSTEM_NAME STREQUAL darwin_embedded) - set(BENTOUSESTCFS 1) - include_directories(${BENTO4_INCLUDE_DIRS}) -else() - add_subdirectory(wvdecrypter) - set(ADP_ADDITIONAL_BINARY $) -endif() - add_subdirectory(lib/mpegts) add_subdirectory(lib/webm_parser) if(ENABLE_INTERNAL_BENTO4) include_directories(${BENTO4_INCLUDE_DIRS}) - add_dependencies(ssd_wv bento4) add_dependencies(mpegts bento4) add_dependencies(webm_parser bento4) endif() @@ -194,6 +218,20 @@ list(APPEND DEPLIBS mpegts) list(APPEND DEPLIBS webm_parser) list(APPEND DEPLIBS ${PUGIXML_LIBRARIES}) +# Add sources from CMakeLists on subfolders +get_property(SOURCES_LIST GLOBAL PROPERTY GlobalSourceList) +list(APPEND ADP_SOURCES ${SOURCES_LIST}) +get_property(HEADERS_LIST GLOBAL PROPERTY GlobalHeaderList) +list(APPEND ADP_HEADERS ${HEADERS_LIST}) + +# Add additional dependencies +get_property(DEPS_FOLDERS_LIST GLOBAL PROPERTY GlobalDepsFoldersList) +foreach(DEP_FOLDER ${DEPS_FOLDERS_LIST}) + add_subdirectory(${DEP_FOLDER}) +endforeach() +get_property(DEPS_NAMES_LIST GLOBAL PROPERTY GlobalDepsNamesList) +list(APPEND DEPLIBS ${DEPS_NAMES_LIST}) + build_addon(inputstream.adaptive ADP DEPLIBS) include(CPack) diff --git a/lib/cdm/CMakeLists.txt b/lib/cdm/CMakeLists.txt new file mode 100644 index 000000000..6e67a89f4 --- /dev/null +++ b/lib/cdm/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.10) +project(cdm_library) + +if(WIN32) + set(CDMTYPE "win.cc") +elseif(${CMAKE_SYSTEM_NAME} MATCHES "Darwin") + set(CDMTYPE "mac.mm") +else() + set(CDMTYPE "posix.cc") +endif() + +add_library(cdm_library STATIC + cdm/base/native_library.cc + cdm/base/native_library_${CDMTYPE} + cdm/media/cdm/cdm_adapter.cc + cdm/media/cdm/cdm_adapter.h +) + +target_include_directories(cdm_library PUBLIC ${PROJECT_SOURCE_DIR}) + +set_target_properties(cdm_library PROPERTIES POSITION_INDEPENDENT_CODE True) + +if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin") + target_link_libraries(cdm_library "-framework CoreFoundation") +endif() diff --git a/wvdecrypter/cdm/base/base_export.h b/lib/cdm/cdm/base/base_export.h similarity index 100% rename from wvdecrypter/cdm/base/base_export.h rename to lib/cdm/cdm/base/base_export.h diff --git a/wvdecrypter/cdm/base/basictypes.h b/lib/cdm/cdm/base/basictypes.h similarity index 100% rename from wvdecrypter/cdm/base/basictypes.h rename to lib/cdm/cdm/base/basictypes.h diff --git a/wvdecrypter/cdm/base/compiler_specific.h b/lib/cdm/cdm/base/compiler_specific.h similarity index 100% rename from wvdecrypter/cdm/base/compiler_specific.h rename to lib/cdm/cdm/base/compiler_specific.h diff --git a/wvdecrypter/cdm/base/macros.h b/lib/cdm/cdm/base/macros.h similarity index 100% rename from wvdecrypter/cdm/base/macros.h rename to lib/cdm/cdm/base/macros.h diff --git a/wvdecrypter/cdm/base/native_library.cc b/lib/cdm/cdm/base/native_library.cc similarity index 100% rename from wvdecrypter/cdm/base/native_library.cc rename to lib/cdm/cdm/base/native_library.cc diff --git a/wvdecrypter/cdm/base/native_library.h b/lib/cdm/cdm/base/native_library.h similarity index 96% rename from wvdecrypter/cdm/base/native_library.h rename to lib/cdm/cdm/base/native_library.h index d32bdefea..61a0f0f27 100644 --- a/wvdecrypter/cdm/base/native_library.h +++ b/lib/cdm/cdm/base/native_library.h @@ -21,6 +21,8 @@ #if defined(OS_WIN) #include +// Windows "ERROR" define can conflicts with definitions of projects that use this name, so remove it +#undef ERROR #elif defined(OS_MACOSX) #import #endif // OS_* diff --git a/wvdecrypter/cdm/base/native_library_ios.mm b/lib/cdm/cdm/base/native_library_ios.mm similarity index 100% rename from wvdecrypter/cdm/base/native_library_ios.mm rename to lib/cdm/cdm/base/native_library_ios.mm diff --git a/wvdecrypter/cdm/base/native_library_mac.mm b/lib/cdm/cdm/base/native_library_mac.mm similarity index 100% rename from wvdecrypter/cdm/base/native_library_mac.mm rename to lib/cdm/cdm/base/native_library_mac.mm diff --git a/wvdecrypter/cdm/base/native_library_posix.cc b/lib/cdm/cdm/base/native_library_posix.cc similarity index 100% rename from wvdecrypter/cdm/base/native_library_posix.cc rename to lib/cdm/cdm/base/native_library_posix.cc diff --git a/wvdecrypter/cdm/base/native_library_unittest.cc b/lib/cdm/cdm/base/native_library_unittest.cc similarity index 100% rename from wvdecrypter/cdm/base/native_library_unittest.cc rename to lib/cdm/cdm/base/native_library_unittest.cc diff --git a/wvdecrypter/cdm/base/native_library_win.cc b/lib/cdm/cdm/base/native_library_win.cc similarity index 100% rename from wvdecrypter/cdm/base/native_library_win.cc rename to lib/cdm/cdm/base/native_library_win.cc diff --git a/wvdecrypter/cdm/build/build_config.h b/lib/cdm/cdm/build/build_config.h similarity index 100% rename from wvdecrypter/cdm/build/build_config.h rename to lib/cdm/cdm/build/build_config.h diff --git a/wvdecrypter/cdm/media/base/cdm_config.h b/lib/cdm/cdm/media/base/cdm_config.h similarity index 100% rename from wvdecrypter/cdm/media/base/cdm_config.h rename to lib/cdm/cdm/media/base/cdm_config.h diff --git a/wvdecrypter/cdm/media/base/limits.h b/lib/cdm/cdm/media/base/limits.h similarity index 100% rename from wvdecrypter/cdm/media/base/limits.h rename to lib/cdm/cdm/media/base/limits.h diff --git a/wvdecrypter/cdm/media/cdm/api/content_decryption_module.h b/lib/cdm/cdm/media/cdm/api/content_decryption_module.h similarity index 100% rename from wvdecrypter/cdm/media/cdm/api/content_decryption_module.h rename to lib/cdm/cdm/media/cdm/api/content_decryption_module.h diff --git a/wvdecrypter/cdm/media/cdm/api/content_decryption_module_export.h b/lib/cdm/cdm/media/cdm/api/content_decryption_module_export.h similarity index 100% rename from wvdecrypter/cdm/media/cdm/api/content_decryption_module_export.h rename to lib/cdm/cdm/media/cdm/api/content_decryption_module_export.h diff --git a/wvdecrypter/cdm/media/cdm/api/content_decryption_module_proxy.h b/lib/cdm/cdm/media/cdm/api/content_decryption_module_proxy.h similarity index 100% rename from wvdecrypter/cdm/media/cdm/api/content_decryption_module_proxy.h rename to lib/cdm/cdm/media/cdm/api/content_decryption_module_proxy.h diff --git a/wvdecrypter/cdm/media/cdm/cdm_adapter.cc b/lib/cdm/cdm/media/cdm/cdm_adapter.cc similarity index 98% rename from wvdecrypter/cdm/media/cdm/cdm_adapter.cc rename to lib/cdm/cdm/media/cdm/cdm_adapter.cc index 9fcf559e1..d33d2bd13 100644 --- a/wvdecrypter/cdm/media/cdm/cdm_adapter.cc +++ b/lib/cdm/cdm/media/cdm/cdm_adapter.cc @@ -8,7 +8,8 @@ #include "cdm_adapter.h" -#include "../../../Helper.h" +//! @todo: provide an appropriate interface for log output +#include "../../../src/utils/log.h" #include #include @@ -163,7 +164,7 @@ void CdmAdapter::Initialize() if (!library_) { - LOG::Log(SSD::SSDERROR, "%s: Failed to load library: %s", __FUNCTION__, + LOG::LogF(LOGERROR, "%s: Failed to load library: %s", __FUNCTION__, error.ToString().c_str()); return; } @@ -181,7 +182,7 @@ void CdmAdapter::Initialize() } std::string version{get_cdm_verion_func()}; - LOG::Log(SSD::SSDDEBUG, "CDM version: %s", version.c_str()); + LOG::LogF(LOGDEBUG, "CDM version: %s", version.c_str()); #if defined(OS_WIN) // Load DXVA before sandbox lockdown to give CDM access to Output Protection @@ -547,7 +548,7 @@ void CdmAdapter::OnSessionKeysChange(const char* session_id, char* bufferPtr{buffer}; for (uint32_t j{0}; j < keys_info[i].key_id_size; ++j) bufferPtr += sprintf(bufferPtr, "%02X", (int)keys_info[i].key_id[j]); - LOG::Log(SSD::SSDDEBUG, "%s: Sessionkey %s status: %d syscode: %u", __func__, buffer, + LOG::Log(LOGDEBUG, "%s: Sessionkey %s status: %d syscode: %u", __func__, buffer, keys_info[i].status, keys_info[i].system_code); SendClientMessage(session_id, session_id_size, CdmAdapterClient::kSessionKeysChange, @@ -623,7 +624,7 @@ void CdmAdapter::RequestStorageId(uint32_t version) void CdmAdapter::OnInitialized(bool success) { - LOG::Log(SSD::SSDDEBUG, "CDM is initialized: %s", success ? "true" : "false"); + LOG::LogF(LOGDEBUG, "CDM is initialized: %s", success ? "true" : "false"); } diff --git a/wvdecrypter/cdm/media/cdm/cdm_adapter.h b/lib/cdm/cdm/media/cdm/cdm_adapter.h similarity index 100% rename from wvdecrypter/cdm/media/cdm/cdm_adapter.h rename to lib/cdm/cdm/media/cdm/cdm_adapter.h diff --git a/lib/jni/CMakeLists.txt b/lib/jni/CMakeLists.txt new file mode 100644 index 000000000..2a247db12 --- /dev/null +++ b/lib/jni/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.5) +project(jni) + +add_library(jni STATIC + jni/jutils/jutils-details.hpp + jni/jutils/jutils.cpp + jni/jutils/jutils.hpp + jni/src/ClassLoader.cpp + jni/src/ClassLoader.h + jni/src/ClassLoader.cpp + jni/src/HashMap.cpp + jni/src/HashMap.h + jni/src/JNIBase.cpp + jni/src/JNIBase.h + jni/src/Log.h + jni/src/MediaDrm.cpp + jni/src/MediaDrm.h + jni/src/MediaDrmKeyRequest.cpp + jni/src/MediaDrmKeyRequest.h + jni/src/MediaDrmOnEventListener.cpp + jni/src/MediaDrmOnEventListener.h + jni/src/MediaDrmProvisionRequest.cpp + jni/src/MediaDrmProvisionRequest.h + jni/src/UUID.cpp + jni/src/UUID.h +) + +target_include_directories(jni PUBLIC ${PROJECT_SOURCE_DIR} PRIVATE ${PROJECT_SOURCE_DIR}/jni/jutils) diff --git a/wvdecrypter/jni/jutils/jni.inc b/lib/jni/jni/jutils/jni.inc similarity index 100% rename from wvdecrypter/jni/jutils/jni.inc rename to lib/jni/jni/jutils/jni.inc diff --git a/wvdecrypter/jni/jutils/jutils-details.hpp b/lib/jni/jni/jutils/jutils-details.hpp similarity index 100% rename from wvdecrypter/jni/jutils/jutils-details.hpp rename to lib/jni/jni/jutils/jutils-details.hpp diff --git a/wvdecrypter/jni/jutils/jutils.cpp b/lib/jni/jni/jutils/jutils.cpp similarity index 100% rename from wvdecrypter/jni/jutils/jutils.cpp rename to lib/jni/jni/jutils/jutils.cpp diff --git a/wvdecrypter/jni/jutils/jutils.hpp b/lib/jni/jni/jutils/jutils.hpp similarity index 100% rename from wvdecrypter/jni/jutils/jutils.hpp rename to lib/jni/jni/jutils/jutils.hpp diff --git a/wvdecrypter/jni/src/ClassLoader.cpp b/lib/jni/jni/src/ClassLoader.cpp similarity index 100% rename from wvdecrypter/jni/src/ClassLoader.cpp rename to lib/jni/jni/src/ClassLoader.cpp diff --git a/wvdecrypter/jni/src/ClassLoader.h b/lib/jni/jni/src/ClassLoader.h similarity index 100% rename from wvdecrypter/jni/src/ClassLoader.h rename to lib/jni/jni/src/ClassLoader.h diff --git a/wvdecrypter/jni/src/HashMap.cpp b/lib/jni/jni/src/HashMap.cpp similarity index 100% rename from wvdecrypter/jni/src/HashMap.cpp rename to lib/jni/jni/src/HashMap.cpp diff --git a/wvdecrypter/jni/src/HashMap.h b/lib/jni/jni/src/HashMap.h similarity index 100% rename from wvdecrypter/jni/src/HashMap.h rename to lib/jni/jni/src/HashMap.h diff --git a/wvdecrypter/jni/src/JNIBase.cpp b/lib/jni/jni/src/JNIBase.cpp similarity index 100% rename from wvdecrypter/jni/src/JNIBase.cpp rename to lib/jni/jni/src/JNIBase.cpp diff --git a/wvdecrypter/jni/src/JNIBase.h b/lib/jni/jni/src/JNIBase.h similarity index 98% rename from wvdecrypter/jni/src/JNIBase.h rename to lib/jni/jni/src/JNIBase.h index 20809211c..3a820a334 100644 --- a/wvdecrypter/jni/src/JNIBase.h +++ b/lib/jni/jni/src/JNIBase.h @@ -8,7 +8,7 @@ #pragma once -#include "jutils.hpp" +#include "../jutils/jutils.hpp" #include diff --git a/wvdecrypter/jni/src/Log.h b/lib/jni/jni/src/Log.h similarity index 100% rename from wvdecrypter/jni/src/Log.h rename to lib/jni/jni/src/Log.h diff --git a/wvdecrypter/jni/src/MediaDrm.cpp b/lib/jni/jni/src/MediaDrm.cpp similarity index 100% rename from wvdecrypter/jni/src/MediaDrm.cpp rename to lib/jni/jni/src/MediaDrm.cpp diff --git a/wvdecrypter/jni/src/MediaDrm.h b/lib/jni/jni/src/MediaDrm.h similarity index 100% rename from wvdecrypter/jni/src/MediaDrm.h rename to lib/jni/jni/src/MediaDrm.h diff --git a/wvdecrypter/jni/src/MediaDrmKeyRequest.cpp b/lib/jni/jni/src/MediaDrmKeyRequest.cpp similarity index 100% rename from wvdecrypter/jni/src/MediaDrmKeyRequest.cpp rename to lib/jni/jni/src/MediaDrmKeyRequest.cpp diff --git a/wvdecrypter/jni/src/MediaDrmKeyRequest.h b/lib/jni/jni/src/MediaDrmKeyRequest.h similarity index 100% rename from wvdecrypter/jni/src/MediaDrmKeyRequest.h rename to lib/jni/jni/src/MediaDrmKeyRequest.h diff --git a/wvdecrypter/jni/src/MediaDrmOnEventListener.cpp b/lib/jni/jni/src/MediaDrmOnEventListener.cpp similarity index 100% rename from wvdecrypter/jni/src/MediaDrmOnEventListener.cpp rename to lib/jni/jni/src/MediaDrmOnEventListener.cpp diff --git a/wvdecrypter/jni/src/MediaDrmOnEventListener.h b/lib/jni/jni/src/MediaDrmOnEventListener.h similarity index 100% rename from wvdecrypter/jni/src/MediaDrmOnEventListener.h rename to lib/jni/jni/src/MediaDrmOnEventListener.h diff --git a/wvdecrypter/jni/src/MediaDrmProvisionRequest.cpp b/lib/jni/jni/src/MediaDrmProvisionRequest.cpp similarity index 100% rename from wvdecrypter/jni/src/MediaDrmProvisionRequest.cpp rename to lib/jni/jni/src/MediaDrmProvisionRequest.cpp diff --git a/wvdecrypter/jni/src/MediaDrmProvisionRequest.h b/lib/jni/jni/src/MediaDrmProvisionRequest.h similarity index 100% rename from wvdecrypter/jni/src/MediaDrmProvisionRequest.h rename to lib/jni/jni/src/MediaDrmProvisionRequest.h diff --git a/wvdecrypter/jni/src/UUID.cpp b/lib/jni/jni/src/UUID.cpp similarity index 100% rename from wvdecrypter/jni/src/UUID.cpp rename to lib/jni/jni/src/UUID.cpp diff --git a/wvdecrypter/jni/src/UUID.h b/lib/jni/jni/src/UUID.h similarity index 100% rename from wvdecrypter/jni/src/UUID.h rename to lib/jni/jni/src/UUID.h diff --git a/src/KodiHost.cpp b/src/KodiHost.cpp deleted file mode 100644 index 16bb2b664..000000000 --- a/src/KodiHost.cpp +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright (C) 2022 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 "KodiHost.h" - -#include "utils/log.h" - -void* CKodiHost::CURLCreate(const char* strURL) -{ - kodi::vfs::CFile* file{new kodi::vfs::CFile}; - if (!file->CURLCreate(strURL)) - { - delete file; - return nullptr; - } - return file; -} - -bool CKodiHost::CURLAddOption(void* file, CURLOPTIONS opt, const char* name, const char* value) -{ - const CURLOptiontype xbmcmap[]{ADDON_CURL_OPTION_PROTOCOL, ADDON_CURL_OPTION_HEADER}; - return static_cast(file)->CURLAddOption(xbmcmap[opt], name, value); -} - -const char* CKodiHost::CURLGetProperty(void* file, CURLPROPERTY prop, const char* name) -{ - const FilePropertyTypes xbmcmap[]{ADDON_FILE_PROPERTY_RESPONSE_HEADER}; - m_strPropertyValue = static_cast(file)->GetPropertyValue(xbmcmap[prop], name); - return m_strPropertyValue.c_str(); -} - -bool CKodiHost::CURLOpen(void* file) -{ - return static_cast(file)->CURLOpen(ADDON_READ_NO_CACHE); -} - -size_t CKodiHost::ReadFile(void* file, void* lpBuf, size_t uiBufSize) -{ - return static_cast(file)->Read(lpBuf, uiBufSize); -} - -void CKodiHost::CloseFile(void* file) -{ - return static_cast(file)->Close(); -} - -bool CKodiHost::CreateDir(const char* dir) -{ - return kodi::vfs::CreateDirectory(dir); -} - -void CKodiHost::LogVA(const SSD::SSDLogLevel level, const char* format, va_list args) -{ - std::vector data; - data.resize(256); - - va_list argsStart; - va_copy(argsStart, args); - - int ret; - while (static_cast(ret = vsnprintf(data.data(), data.size(), format, args)) > data.size()) - { - data.resize(data.size() * 2); - args = argsStart; - } - LOG::Log(static_cast(level), data.data()); - va_end(argsStart); -} - -void CKodiHost::SetLibraryPath(const char* libraryPath) -{ - m_strLibraryPath = libraryPath; - - const char* pathSep{libraryPath[0] && libraryPath[1] == ':' && isalpha(libraryPath[0]) ? "\\" - : "/"}; - - if (m_strLibraryPath.size() && m_strLibraryPath.back() != pathSep[0]) - m_strLibraryPath += pathSep; -} - -void CKodiHost::SetProfilePath(const std::string& profilePath) -{ - m_strProfilePath = profilePath; - - const char* pathSep{profilePath[0] && profilePath[1] == ':' && isalpha(profilePath[0]) ? "\\" - : "/"}; - - if (m_strProfilePath.size() && 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.c_str()); - m_strProfilePath += "cdm"; - m_strProfilePath += pathSep; - kodi::vfs::CreateDirectory(m_strProfilePath.c_str()); -} - -bool CKodiHost::GetBuffer(void* instance, SSD::SSD_PICTURE& picture) -{ - return instance ? static_cast(instance)->GetFrameBuffer( - *reinterpret_cast(&picture)) - : false; -} - -void CKodiHost::ReleaseBuffer(void* instance, void* buffer) -{ - if (instance) - static_cast(instance)->ReleaseFrameBuffer(buffer); -} diff --git a/src/KodiHost.h b/src/KodiHost.h deleted file mode 100644 index 11ca1919c..000000000 --- a/src/KodiHost.h +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2022 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 "SSD_dll.h" - -#include -#include -#include - -#if defined(ANDROID) -#include -#endif - -class ATTR_DLL_LOCAL CKodiHost : public SSD::SSD_HOST -{ -public: -#if defined(ANDROID) - virtual void* GetJNIEnv() override { return m_androidSystem.GetJNIEnv(); }; - - virtual int GetSDKVersion() override { return m_androidSystem.GetSDKVersion(); }; - - virtual const char* GetClassName() override - { - m_retvalHelper = m_androidSystem.GetClassName(); - return m_retvalHelper.c_str(); - }; - -#endif - virtual const char* GetLibraryPath() const override { return m_strLibraryPath.c_str(); }; - - virtual const char* GetProfilePath() const override { return m_strProfilePath.c_str(); }; - - virtual void* CURLCreate(const char* strURL) override; - - virtual bool CURLAddOption(void* file, - CURLOPTIONS opt, - const char* name, - const char* value) override; - - virtual const char* CURLGetProperty(void* file, CURLPROPERTY prop, const char* name) override; - - virtual bool CURLOpen(void* file) override; - - virtual size_t ReadFile(void* file, void* lpBuf, size_t uiBufSize) override; - - virtual void CloseFile(void* file) override; - - virtual bool CreateDir(const char* dir) override; - - void LogVA(const SSD::SSDLogLevel level, const char* format, va_list args) override; - - void SetLibraryPath(const char* libraryPath); - - void SetProfilePath(const std::string& profilePath); - - virtual bool GetBuffer(void* instance, SSD::SSD_PICTURE& picture) override; - - virtual void ReleaseBuffer(void* instance, void* buffer) override; - - void SetDebugSaveLicense(bool isDebugSaveLicense) override - { - m_isDebugSaveLicense = isDebugSaveLicense; - } - - bool IsDebugSaveLicense() override { return m_isDebugSaveLicense; } - -private: - std::string m_strProfilePath; - std::string m_strLibraryPath; - std::string m_strPropertyValue; - bool m_isDebugSaveLicense; - -#if defined(ANDROID) - kodi::platform::CInterfaceAndroidSystem m_androidSystem; - std::string m_retvalHelper; -#endif -}; diff --git a/src/SSD_dll.h b/src/SSD_dll.h deleted file mode 100644 index d23fcbe45..000000000 --- a/src/SSD_dll.h +++ /dev/null @@ -1,283 +0,0 @@ -/* - * Copyright (C) 2017 peak3d (http://www.peak3d.de) - * 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 // va_list, va_start, va_arg, va_end -#include - -//Functionality wich is supported by the Decrypter -class Adaptive_CencSingleSampleDecrypter; -class AP4_DataBuffer; -enum class CryptoMode; - -namespace SSD -{ - // Must match to LogLevel on utils/log.h - enum SSDLogLevel - { - SSDDEBUG, - SSDINFO, - SSDWARNING, - SSDERROR, - SSDFATAL - }; - - struct SSD_PICTURE; - - //Functionality wich is supported by the Addon - class SSD_HOST - { - public: - enum CURLOPTIONS - { - OPTION_PROTOCOL, - OPTION_HEADER - }; - enum CURLPROPERTY - { - PROPERTY_HEADER - }; - static const uint32_t version = 22; -#if defined(ANDROID) - virtual void* GetJNIEnv() = 0; - virtual int GetSDKVersion() = 0; - virtual const char *GetClassName() = 0; -#endif - virtual const char *GetLibraryPath() const = 0; - virtual const char *GetProfilePath() const = 0; - virtual void* CURLCreate(const char* strURL) = 0; - virtual bool CURLAddOption(void* file, CURLOPTIONS opt, const char* name, const char* value) = 0; - virtual const char* CURLGetProperty(void* file, CURLPROPERTY prop, const char *name) = 0; - virtual bool CURLOpen(void* file) = 0; - virtual size_t ReadFile(void* file, void* lpBuf, size_t uiBufSize) = 0; - virtual void CloseFile(void* file) = 0; - virtual bool CreateDir(const char* dir) = 0; - virtual bool GetBuffer(void* instance, SSD_PICTURE &picture) = 0; - virtual void ReleaseBuffer(void* instance, void *buffer) = 0; - - virtual void LogVA(const SSDLogLevel level, const char* format, va_list args) = 0; - - /*! - * \brief Set whether license data must be saved for debugging. - * \param isDebugSaveLicense Set true to save the license data. - */ - virtual void SetDebugSaveLicense(bool isDebugSaveLicense) = 0; - - /*! - * \brief Determine if license data must to be saved for debugging purpose. - */ - virtual bool IsDebugSaveLicense() = 0; - }; - - /* - * Enums: SSD_VIDEOFORMAT, Codec, CodecProfile must be kept in sync with: - * xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream/stream_codec.h - * xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/video_codec.h - */ - - enum SSD_VIDEOFORMAT // refer to VIDEOCODEC_FORMAT - { - UnknownVideoFormat = 0, - VideoFormatYV12, - VideoFormatI420, - VideoFormatYUV420P9, - VideoFormatYUV420P10, - VideoFormatYUV420P12, - VideoFormatYUV422P9, - VideoFormatYUV422P10, - VideoFormatYUV422P12, - VideoFormatYUV444P9, - VideoFormatYUV444P10, - VideoFormatYUV444P12, - MaxVideoFormats - }; - - enum Codec // refer to VIDEOCODEC_TYPE - { - CodecUnknown = 0, - CodecVp8, - CodecH264, - CodecVp9, - CodecAv1, - }; - - enum CodecProfile // refer to STREAMCODEC_PROFILE - { - CodecProfileUnknown = 0, - CodecProfileNotNeeded, - H264CodecProfileBaseline, - H264CodecProfileMain, - H264CodecProfileExtended, - H264CodecProfileHigh, - H264CodecProfileHigh10, - H264CodecProfileHigh422, - H264CodecProfileHigh444Predictive, - VP9CodecProfile0 = 20, - VP9CodecProfile1, - VP9CodecProfile2, - VP9CodecProfile3, - AV1CodecProfileMain, - AV1CodecProfileHigh, - AV1CodecProfileProfessional, - }; - - struct SSD_VIDEOINITDATA - { - Codec codec; - CodecProfile codecProfile; - - const SSD_VIDEOFORMAT *videoFormats; - - uint32_t width, height; - - const uint8_t *extraData; - unsigned int extraDataSize; - }; - - struct SSD_PICTURE - { - enum VideoPlane { - YPlane = 0, - UPlane, - VPlane, - MaxPlanes = 3, - }; - - enum Flags : uint32_t - { - FLAG_NONE = 0, - FLAG_DROP = (1 << 0), - FLAG_DRAIN = (1 << 1), - }; - - SSD_VIDEOFORMAT videoFormat; - uint32_t flags; - - uint32_t width, height; - - uint8_t *decodedData; - size_t decodedDataSize; - - uint32_t planeOffsets[VideoPlane::MaxPlanes]; - uint32_t stride[VideoPlane::MaxPlanes]; - - int64_t pts; - - void *buffer; - }; - - typedef struct SSD_SAMPLE - { - const uint8_t *data; - uint32_t dataSize; - - int64_t pts; - - struct CRYPTO_INFO - { - /// @brief Number of subsamples. - uint16_t numSubSamples; - - /// @brief Flags for later use. - uint16_t flags; - - /// @brief @ref numSubSamples uint16_t's which define the size of clear size - /// of a subsample. - uint16_t* clearBytes; - - /// @brief @ref numSubSamples uint32_t's which define the size of cipher size - /// of a subsample. - uint32_t* cipherBytes; - - /// @brief Initialization vector - uint8_t* iv; - uint32_t ivSize; - - /// @brief Key id - uint8_t* kid; - uint32_t kidSize; - - /// @brief Encryption mode - uint16_t mode; - - /// @brief Crypt blocks - number of blocks to encrypt in sample encryption pattern - uint8_t cryptBlocks; - - /// @brief Skip blocks - number of blocks to skip in sample encryption pattern - uint8_t skipBlocks; - - } cryptoInfo; - } SSD_SAMPLE; - - enum SSD_DECODE_RETVAL - { - VC_NONE = 0, //< noop - VC_ERROR, //< an error occured, no other messages will be returned - VC_BUFFER, //< the decoder needs more data - VC_PICTURE, //< the decoder got a picture - VC_EOF, //< the decoder signals EOF - }; - - class SSD_DECRYPTER - { - public: - struct SSD_CAPS - { - static const uint32_t SSD_SUPPORTS_DECODING = 1; - static const uint32_t SSD_SECURE_PATH = 2; - static const uint32_t SSD_ANNEXB_REQUIRED = 4; - static const uint32_t SSD_HDCP_RESTRICTED = 8; - static const uint32_t SSD_SINGLE_DECRYPT = 16; - static const uint32_t SSD_SECURE_DECODER = 32; - static const uint32_t SSD_INVALID = 64; - - static const uint32_t SSD_MEDIA_VIDEO = 1; - static const uint32_t SSD_MEDIA_AUDIO = 2; - - uint16_t flags; - - /* The following 2 fields are set as followed: - - If licenseresponse return hdcp information, hdcpversion is 0 and - hdcplimit either 0 (if hdcp is supported) or given value (if hdcpversion is not supported) - - if no hdcp information is passed in licenseresponse, we set hdcpversion to the value we support - manifest / representation have to check if they are allowed to be played. - */ - uint16_t hdcpVersion; //The HDCP version streams has to be restricted 0,10,20,21,22..... - int hdcpLimit; // If set (> 0) streams that are greater than the multiplication of "Width x Height" cannot be played. - }; - - static const uint8_t CONFIG_PERSISTENTSTORAGE = 1; - - // Return supported URN if type matches to capabilities, otherwise null - virtual const char *SelectKeySytem(const char* keySystem) = 0; - virtual bool OpenDRMSystem(const char *licenseURL, const AP4_DataBuffer &serverCertificate, const uint8_t config) = 0; - virtual Adaptive_CencSingleSampleDecrypter* CreateSingleSampleDecrypter( - AP4_DataBuffer& pssh, - const char* optionalKeyParameter, - std::string_view defaultkeyid, - bool skipSessionMessage, - CryptoMode cryptoMode) = 0; - virtual void DestroySingleSampleDecrypter(Adaptive_CencSingleSampleDecrypter* decrypter) = 0; - - virtual void GetCapabilities(Adaptive_CencSingleSampleDecrypter* decrypter, - const uint8_t* keyid, - uint32_t media, - SSD_DECRYPTER::SSD_CAPS& caps) = 0; - virtual bool HasLicenseKey(Adaptive_CencSingleSampleDecrypter* decrypter, const uint8_t *keyid) = 0; - virtual bool HasCdmSession() = 0; - virtual std::string GetChallengeB64Data(Adaptive_CencSingleSampleDecrypter* decrypter) = 0; - - virtual bool OpenVideoDecoder(Adaptive_CencSingleSampleDecrypter* decrypter, - const SSD_VIDEOINITDATA* initData) = 0; - virtual SSD_DECODE_RETVAL DecryptAndDecodeVideo(void* hostInstance, SSD_SAMPLE* sample) = 0; - virtual SSD_DECODE_RETVAL VideoFrameDataToPicture(void* hostInstance, SSD_PICTURE* picture) = 0; - virtual void ResetVideo() = 0; - }; -}; // namespace diff --git a/src/Session.cpp b/src/Session.cpp index 7cf740910..0a378f420 100644 --- a/src/Session.cpp +++ b/src/Session.cpp @@ -38,12 +38,9 @@ CSession::CSession(const PROPERTIES::KodiProperties& kodiProps, const std::string& profilePath) : m_kodiProps(kodiProps), m_manifestUrl(manifestUrl), - m_KodiHost(std::make_unique()), + m_profilePath(profilePath), m_reprChooser(CHOOSER::CreateRepresentationChooser(kodiProps)) { - m_KodiHost->SetProfilePath(profilePath); - m_KodiHost->SetDebugSaveLicense(kodi::addon::GetSettingBoolean("debug.save.license")); - m_settingNoSecureDecoder = kodi::addon::GetSettingBoolean("NOSECUREDECODER"); LOG::Log(LOGDEBUG, "Setting NOSECUREDECODER value: %d", m_settingNoSecureDecoder); @@ -88,81 +85,27 @@ CSession::~CSession() void CSession::SetSupportedDecrypterURN(std::string& key_system) { - typedef SSD::SSD_DECRYPTER* (*CreateDecryptorInstanceFunc)(SSD::SSD_HOST * host, - uint32_t version); - std::string specialpath = kodi::addon::GetSettingString("DECRYPTERPATH"); if (specialpath.empty()) { LOG::Log(LOGDEBUG, "DECRYPTERPATH not specified in settings.xml"); return; } - m_KodiHost->SetLibraryPath(kodi::vfs::TranslateSpecialProtocol(specialpath).c_str()); - std::array searchPaths = - { - kodi::vfs::TranslateSpecialProtocol("special://xbmcbinaddons/inputstream.adaptive/"), - kodi::vfs::TranslateSpecialProtocol("special://xbmcaltbinaddons/inputstream.adaptive/"), - kodi::addon::GetAddonInfo("path"), - }; - - std::vector items; + m_decrypter = m_factory.GetDecrypter(GetCryptoKeySystem()); + if (!m_decrypter) + return; - for (auto searchPath : searchPaths) + if (!m_decrypter->Initialize()) { - LOG::Log(LOGDEBUG, "Searching for decrypters in: %s", searchPath.c_str()); - - if (!kodi::vfs::GetDirectory(searchPath, "", items)) - continue; - - for (auto item : items) - { - if (item.Label().compare(0, 4, "ssd_") && item.Label().compare(0, 7, "libssd_")) - continue; - - bool success = false; - m_dllHelper = std::make_unique(); - if (m_dllHelper->LoadDll(item.Path())) - { -#if defined(__linux__) && defined(__aarch64__) && !defined(ANDROID) - // On linux arm64, libwidevinecdm.so depends on two dynamic symbols: - // __aarch64_ldadd4_acq_rel - // __aarch64_swp4_acq_rel - // These are defined in libssd_wv.so, but to make them available in the main binary's PLT, - // we need RTLD_GLOBAL. LoadDll() above uses RTLD_LOCAL, so we use RTLD_NOLOAD here to - // switch the flags from LOCAL to GLOBAL. - void *hdl = dlopen(item.Path().c_str(), RTLD_NOLOAD | RTLD_GLOBAL | RTLD_LAZY); - if (!hdl) - { - LOG::Log(LOGERROR, "Failed to reload dll in global mode: %s", dlerror()); - } -#endif - CreateDecryptorInstanceFunc startup; - if (m_dllHelper->RegisterSymbol(startup, "CreateDecryptorInstance")) - { - SSD::SSD_DECRYPTER* decrypter = startup(m_KodiHost.get(), SSD::SSD_HOST::version); - const char* suppUrn(0); - - if (decrypter && (suppUrn = decrypter->SelectKeySytem(m_kodiProps.m_licenseType.c_str()))) - { - LOG::Log(LOGDEBUG, "Found decrypter: %s", item.Path().c_str()); - success = true; - m_decrypter = decrypter; - key_system = suppUrn; - break; - } - } - } - else - { - LOG::Log(LOGDEBUG, "%s", dlerror()); - } - if (!success) - { - m_dllHelper.reset(); - } - } + LOG::Log(LOGERROR, "The decrypter library cannot be initialized."); + return; } + + key_system = m_decrypter->SelectKeySytem(m_kodiProps.m_licenseType.c_str()); + m_decrypter->SetLibraryPath(kodi::vfs::TranslateSpecialProtocol(specialpath).c_str()); + m_decrypter->SetProfilePath(m_profilePath); + m_decrypter->SetDebugSaveLicense(kodi::addon::GetSettingBoolean("debug.save.license")); } void CSession::DisposeSampleDecrypter() @@ -188,18 +131,8 @@ void CSession::DisposeSampleDecrypter() void CSession::DisposeDecrypter() { - if (!m_dllHelper) - return; - DisposeSampleDecrypter(); - - typedef void (*DeleteDecryptorInstanceFunc)(SSD::SSD_DECRYPTER*); - DeleteDecryptorInstanceFunc disposefn; - - if (m_dllHelper->RegisterSymbol(disposefn, "DeleteDecryptorInstance")) - disposefn(m_decrypter); - - m_decrypter = nullptr; + delete m_decrypter; } /*---------------------------------------------------------------------- @@ -299,7 +232,7 @@ void CSession::CheckHDCP() if (m_cdmSessions.empty()) return; - std::vector decrypterCaps; + std::vector decrypterCaps; for (const auto& cdmsession : m_cdmSessions) { @@ -319,7 +252,7 @@ void CSession::CheckHDCP() { CRepresentation* repr = (*itRepr).get(); - const SSD::SSD_DECRYPTER::SSD_CAPS& ssd_caps = decrypterCaps[repr->m_psshSetPos]; + const DRM::IDecrypter::DecrypterCapabilites& ssd_caps = decrypterCaps[repr->m_psshSetPos]; if (repr->GetHdcpVersion() > ssd_caps.hdcpVersion || (ssd_caps.hdcpLimit > 0 && repr->GetWidth() * repr->GetHeight() > ssd_caps.hdcpLimit)) @@ -371,7 +304,7 @@ bool CSession::PreInitializeDRM(std::string& challengeB64, return false; } - if (!m_decrypter->HasCdmSession()) + if (!m_decrypter->IsInitialised()) { if (!m_decrypter->OpenDRMSystem(m_kodiProps.m_licenseKey.c_str(), m_serverCertificate, m_drmConfig)) @@ -449,7 +382,7 @@ bool CSession::InitializeDRM(bool addDefaultKID /* = false */) return false; } - if (!m_decrypter->HasCdmSession()) + if (!m_decrypter->IsInitialised()) { if (!m_decrypter->OpenDRMSystem(licenseKey.c_str(), m_serverCertificate, m_drmConfig)) { @@ -652,18 +585,18 @@ bool CSession::InitializeDRM(bool addDefaultKID /* = false */) m_decrypter->GetCapabilities(session.m_cencSingleSampleDecrypter, defkid, sessionPsshset.media_, session.m_decrypterCaps); - if (session.m_decrypterCaps.flags & SSD::SSD_DECRYPTER::SSD_CAPS::SSD_INVALID) + if (session.m_decrypterCaps.flags & DRM::IDecrypter::DecrypterCapabilites::SSD_INVALID) { m_adaptiveTree->m_currentPeriod->RemovePSSHSet(static_cast(ses)); } - else if (session.m_decrypterCaps.flags & SSD::SSD_DECRYPTER::SSD_CAPS::SSD_SECURE_PATH) + else if (session.m_decrypterCaps.flags & DRM::IDecrypter::DecrypterCapabilites::SSD_SECURE_PATH) { session.m_cdmSessionStr = session.m_cencSingleSampleDecrypter->GetSessionId(); isSecureVideoSession = true; if (m_settingNoSecureDecoder && !m_kodiProps.m_isLicenseForceSecureDecoder && !m_adaptiveTree->m_currentPeriod->IsSecureDecodeNeeded()) - session.m_decrypterCaps.flags &= ~SSD::SSD_DECRYPTER::SSD_CAPS::SSD_SECURE_DECODER; + session.m_decrypterCaps.flags &= ~DRM::IDecrypter::DecrypterCapabilites::SSD_SECURE_DECODER; } } else @@ -862,9 +795,9 @@ void CSession::UpdateStream(CStream& stream) std::string annexb; const std::string* extraData(&annexb); - const SSD::SSD_DECRYPTER::SSD_CAPS& caps{GetDecrypterCaps(rep->m_psshSetPos)}; + const DRM::IDecrypter::DecrypterCapabilites& caps{GetDecrypterCaps(rep->m_psshSetPos)}; - if ((caps.flags & SSD::SSD_DECRYPTER::SSD_CAPS::SSD_ANNEXB_REQUIRED) && + if ((caps.flags & DRM::IDecrypter::DecrypterCapabilites::SSD_ANNEXB_REQUIRED) && stream.m_info.GetStreamType() == INPUTSTREAM_TYPE_VIDEO) { LOG::Log(LOGDEBUG, "UpdateStream: Convert avc -> annexb"); @@ -938,16 +871,16 @@ void CSession::UpdateStream(CStream& stream) switch (codecProfileNum) { case 0: - stream.m_info.SetCodecProfile(VP9CodecProfile0); + stream.m_info.SetCodecProfile(STREAMCODEC_PROFILE::VP9CodecProfile0); break; case 1: - stream.m_info.SetCodecProfile(VP9CodecProfile1); + stream.m_info.SetCodecProfile(STREAMCODEC_PROFILE::VP9CodecProfile1); break; case 2: - stream.m_info.SetCodecProfile(VP9CodecProfile2); + stream.m_info.SetCodecProfile(STREAMCODEC_PROFILE::VP9CodecProfile2); break; case 3: - stream.m_info.SetCodecProfile(VP9CodecProfile3); + stream.m_info.SetCodecProfile(STREAMCODEC_PROFILE::VP9CodecProfile3); break; default: LOG::LogF(LOGWARNING, "Unhandled video codec profile \"%i\" for codec string: %s", diff --git a/src/Session.h b/src/Session.h index 135cb744b..8cd00371a 100644 --- a/src/Session.h +++ b/src/Session.h @@ -8,14 +8,19 @@ #pragma once -#include "KodiHost.h" #include "Stream.h" #include "common/AdaptiveStream.h" +#include "decrypters/DrmFactory.h" +#include "decrypters/IDecrypter.h" #include "utils/PropertiesUtils.h" #include #include +#if defined(ANDROID) +#include +#endif + class Adaptive_CencSingleSampleDecrypter; namespace SESSION @@ -136,10 +141,10 @@ class ATTR_DLL_LOCAL CSession : public adaptive::AdaptiveStreamObserver return m_cdmSessions[index].m_cencSingleSampleDecrypter; } - /*! \brief Get the decrypter (SSD lib) + /*! \brief Get the decrypter (DRM lib) * \return The decrypter */ - SSD::SSD_DECRYPTER* GetDecrypter() { return m_decrypter; } + DRM::IDecrypter* GetDecrypter() { return m_decrypter; } /*! \brief Get a single sample decrypter matching the session id provided * \param sessionId The session id string to match @@ -151,7 +156,7 @@ class ATTR_DLL_LOCAL CSession : public adaptive::AdaptiveStreamObserver * \param index The index (psshSet number) of the cdm session * \return The single sample decrypter capabilities */ - const SSD::SSD_DECRYPTER::SSD_CAPS& GetDecrypterCaps(unsigned int index) const + const DRM::IDecrypter::DecrypterCapabilites& GetDecrypterCaps(unsigned int index) const { return m_cdmSessions[index].m_decrypterCaps; }; @@ -363,13 +368,15 @@ class ATTR_DLL_LOCAL CSession : public adaptive::AdaptiveStreamObserver private: const UTILS::PROPERTIES::KodiProperties m_kodiProps; std::string m_manifestUrl; + std::string m_profilePath; AP4_DataBuffer m_serverCertificate; std::unique_ptr m_dllHelper; - SSD::SSD_DECRYPTER* m_decrypter{nullptr}; + DRM::IDecrypter* m_decrypter{nullptr}; + DRM::CDrmFactory m_factory; struct CCdmSession { - SSD::SSD_DECRYPTER::SSD_CAPS m_decrypterCaps{}; + DRM::IDecrypter::DecrypterCapabilites m_decrypterCaps{}; Adaptive_CencSingleSampleDecrypter* m_cencSingleSampleDecrypter{nullptr}; const char* m_cdmSessionStr{nullptr}; bool m_sharedCencSsd{false}; @@ -390,6 +397,5 @@ class ATTR_DLL_LOCAL CSession : public adaptive::AdaptiveStreamObserver uint8_t m_drmConfig{0}; bool m_settingNoSecureDecoder{false}; bool m_settingIsHdcpOverride{false}; - std::unique_ptr m_KodiHost; }; } // namespace SESSION diff --git a/src/common/AdaptiveCencSampleDecrypter.h b/src/common/AdaptiveCencSampleDecrypter.h index eaeaedc04..b0d4e85fc 100644 --- a/src/common/AdaptiveCencSampleDecrypter.h +++ b/src/common/AdaptiveCencSampleDecrypter.h @@ -15,6 +15,7 @@ class CAdaptiveCencSampleDecrypter : public AP4_CencSampleDecrypter public: CAdaptiveCencSampleDecrypter(Adaptive_CencSingleSampleDecrypter* singleSampleDecrypter, AP4_CencSampleInfoTable* sampleInfoTable); + ~CAdaptiveCencSampleDecrypter() override {}; virtual AP4_Result DecryptSampleData(AP4_UI32 poolid, AP4_DataBuffer& data_in, diff --git a/src/common/AdaptiveDecrypter.h b/src/common/AdaptiveDecrypter.h index 4396d09e4..5dc2c8122 100644 --- a/src/common/AdaptiveDecrypter.h +++ b/src/common/AdaptiveDecrypter.h @@ -36,22 +36,22 @@ class Adaptive_CencSingleSampleDecrypter : public AP4_CencSingleSampleDecrypter throw std::logic_error("SetDefaultKeyId method not implemented."); }; - virtual AP4_Result SetFragmentInfo(AP4_UI32 pool_id, + virtual AP4_Result SetFragmentInfo(AP4_UI32 poolId, const AP4_UI08* key, - const AP4_UI08 nal_length_size, - AP4_DataBuffer& annexb_sps_pps, + const AP4_UI08 nalLengthSize, + AP4_DataBuffer& annexbSpsPps, AP4_UI32 flags, CryptoInfo cryptoInfo) = 0; - virtual AP4_Result DecryptSampleData(AP4_UI32 poolid, - AP4_DataBuffer& data_in, - AP4_DataBuffer& data_out, + virtual AP4_Result DecryptSampleData(AP4_UI32 poolId, + AP4_DataBuffer& dataIn, + AP4_DataBuffer& dataOut, const AP4_UI08* iv, - unsigned int subsample_count, - const AP4_UI16* bytes_of_cleartext_data, - const AP4_UI32* bytes_of_encrypted_data) = 0; + unsigned int subsampleCount, + const AP4_UI16* bytesOfCleartextData, + const AP4_UI32* bytesOfEncryptedData) = 0; virtual AP4_UI32 AddPool() { return 0; } - virtual void RemovePool(AP4_UI32 poolid) {} + virtual void RemovePool(AP4_UI32 poolId) {} virtual const char* GetSessionId() { return nullptr; } }; diff --git a/src/decrypters/CMakeLists.txt b/src/decrypters/CMakeLists.txt new file mode 100644 index 000000000..fbd013a59 --- /dev/null +++ b/src/decrypters/CMakeLists.txt @@ -0,0 +1,18 @@ +set(SOURCES + DrmFactory.cpp +) + +set(HEADERS + DrmFactory.h + IDecrypter.h +) + +add_dir_sources(SOURCES HEADERS) + +if(NOT CORE_SYSTEM_NAME STREQUAL ios AND NOT CORE_SYSTEM_NAME STREQUAL darwin_embedded) + if(CORE_SYSTEM_NAME STREQUAL android) + add_subdirectory(widevineandroid) + else() + add_subdirectory(widevine) + endif() +endif() diff --git a/src/decrypters/DrmFactory.cpp b/src/decrypters/DrmFactory.cpp new file mode 100644 index 000000000..4160fb2d0 --- /dev/null +++ b/src/decrypters/DrmFactory.cpp @@ -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 "DrmFactory.h" + +#include +#if ANDROID +#include "widevineandroid/WVDecrypter.h" +#else +#ifndef TARGET_DARWIN_EMBEDDED +#include "widevine/WVDecrypter.h" +#endif +#endif + +using namespace DRM; + +IDecrypter* CDrmFactory::GetDecrypter(STREAM_CRYPTO_KEY_SYSTEM keySystem) +{ + if (keySystem == STREAM_CRYPTO_KEY_SYSTEM_WIDEVINE) + { +#if ANDROID + return new CWVDecrypterA(); +#else +// Darwin embedded are apple platforms different than MacOS (e.g. IOS) +#ifndef TARGET_DARWIN_EMBEDDED + return new CWVDecrypter(); +#endif +#endif + } + else if (keySystem == STREAM_CRYPTO_KEY_SYSTEM_PLAYREADY || + keySystem == STREAM_CRYPTO_KEY_SYSTEM_WISEPLAY) + { +#if ANDROID + return new CWVDecrypterA(); +#endif + } + + return nullptr; +} diff --git a/src/decrypters/DrmFactory.h b/src/decrypters/DrmFactory.h new file mode 100644 index 000000000..b005a814d --- /dev/null +++ b/src/decrypters/DrmFactory.h @@ -0,0 +1,20 @@ +/* + * 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 "IDecrypter.h" + +namespace DRM +{ +class CDrmFactory +{ +public: + IDecrypter* GetDecrypter(STREAM_CRYPTO_KEY_SYSTEM keySystem); +}; +} // namespace DRM diff --git a/src/decrypters/IDecrypter.h b/src/decrypters/IDecrypter.h new file mode 100644 index 000000000..876359948 --- /dev/null +++ b/src/decrypters/IDecrypter.h @@ -0,0 +1,202 @@ +/* + * 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 + +class Adaptive_CencSingleSampleDecrypter; +class AP4_DataBuffer; +enum class CryptoMode; + +namespace DRM +{ +class IDecrypter +{ +public: + struct DecrypterCapabilites + { + static const uint32_t SSD_SUPPORTS_DECODING = 1; + static const uint32_t SSD_SECURE_PATH = 2; + static const uint32_t SSD_ANNEXB_REQUIRED = 4; + static const uint32_t SSD_HDCP_RESTRICTED = 8; + static const uint32_t SSD_SINGLE_DECRYPT = 16; + static const uint32_t SSD_SECURE_DECODER = 32; + static const uint32_t SSD_INVALID = 64; + + static const uint32_t SSD_MEDIA_VIDEO = 1; + static const uint32_t SSD_MEDIA_AUDIO = 2; + + uint16_t flags; + + /* The following 2 fields are set as followed: + - If licenseresponse return hdcp information, hdcpversion is 0 and + hdcplimit either 0 (if hdcp is supported) or given value (if hdcpversion is not supported) + - if no hdcp information is passed in licenseresponse, we set hdcpversion to the value we support + manifest / representation have to check if they are allowed to be played. + */ + uint16_t hdcpVersion; //The HDCP version streams has to be restricted 0,10,20,21,22..... + int hdcpLimit; // If set (> 0) streams that are greater than the multiplication of "Width x Height" cannot be played. + }; + + static const uint8_t CONFIG_PERSISTENTSTORAGE = 1; + + virtual ~IDecrypter(){}; + + /** + * \brief Initialize the decrypter library + * \return True if has success, otherwise false + */ + virtual bool Initialize() { return true; } + + /** + * \brief Used to ensure the correct key system is selected + * \param keySystem The URN to be matched + * \return Supported URN if type matches to capabilities, otherwise null + */ + virtual const char* SelectKeySytem(const char* keySystem) = 0; + + /** + * \brief Initialise the DRM system + * \param licenseURL The license URL to contact if applicable + * \param serverCertificate Server certificate to supply if applicable + * \param config Flags to be passed to the decrypter + * \return true on success + */ + virtual bool OpenDRMSystem(const char* licenseURL, + const AP4_DataBuffer& serverCertificate, + const uint8_t config) = 0; + + /** + * \brief Creates a Single Sample Decrypter for decrypting content + * \param pssh The PSSH for initialising the decrypter + * \param optionalKeyParameter License key data passed into IA as parameter + * \param defaultkeyid The default KeyID to initialise with + * \param skipSessionMessage False for preinitialisation case + * \param cryptoMode The crypto/cypher mode to initialise with + * \return The single sample decrypter if successfully created + */ + virtual Adaptive_CencSingleSampleDecrypter* CreateSingleSampleDecrypter( + AP4_DataBuffer& pssh, + const char* optionalKeyParameter, + std::string_view defaultKeyId, + bool skipSessionMessage, + CryptoMode cryptoMode) = 0; + + /** + * \brief Destroy the decrypter + * \param decrypter The single sample decrypter instance to destroy + */ + virtual void DestroySingleSampleDecrypter(Adaptive_CencSingleSampleDecrypter* decrypter) = 0; + + /** + * \brief Determine the capabilities of the decrypter against the supplied media type and KeyID + * \param decrypter The single sample decrypter to use for this check + * \param keyid The KeyID that will be used for this check + * \param media The type of media being decrypted (audio/video) + * \param caps The capabilities object to be populated + */ + virtual void GetCapabilities(Adaptive_CencSingleSampleDecrypter* decrypter, + const uint8_t* keyId, + uint32_t media, + IDecrypter::DecrypterCapabilites& caps) = 0; + + /** + * \brief Check if the supplied KeyID has a license in the decrypter + * \param decrypter The single sample decrypter to use for this check + * \param keyid The KeyID to check for a valid license + * \return True if the KeyID has a license otherwise false + */ + virtual bool HasLicenseKey(Adaptive_CencSingleSampleDecrypter* decrypter, + const uint8_t* keyId) = 0; + + /** + * \brief Check if the decrypter has been initialised (OpenDRMSystem called) + * \return True if decrypter has been initialised otherwise false + */ + virtual bool IsInitialised() = 0; + + /** + * \brief Retrieve license challenge data + * \param decrypter The single sample decrypter to use for license challenge + * \return The license data in Base64 format + */ + virtual std::string GetChallengeB64Data(Adaptive_CencSingleSampleDecrypter* decrypter) = 0; + + /** + * \brief Open VideoCodec for decoding video in a secure pathway to Kodi + * \param decrypter The single sample decrypter to use + * \param initData The data for initialising the codec + * \return True if the decoder was opened successfully otherwise false + */ + virtual bool OpenVideoDecoder(Adaptive_CencSingleSampleDecrypter* decrypter, + const VIDEOCODEC_INITDATA* initData) = 0; + + /** + * \brief Decrypt and decode the video packet with the supplied VideoCodec instance + * \param codecInstance The instance of VideoCodec to use + * \param sample The video sample/packet to decrypt and decode + * \return Return status of the decrypt/decode action + */ + virtual VIDEOCODEC_RETVAL DecryptAndDecodeVideo(kodi::addon::CInstanceVideoCodec* codecInstance, + const DEMUX_PACKET* sample) = 0; + + /** + * \brief Convert CDM video frame data to Kodi picture format + * \param codecInstance The instance of VideoCodec to use + * \param picture The picture object to populate + * \return status of the conversion + */ + virtual VIDEOCODEC_RETVAL VideoFrameDataToPicture(kodi::addon::CInstanceVideoCodec* codecInstance, + VIDEOCODEC_PICTURE* picture) = 0; + + /** + * \brief Reset the decoder + */ + virtual void ResetVideo() = 0; + + /** + * \brief Set the auxillary library path + * \param libraryPath Filesystem path for the decrypter to locate any needed files such as CDMs + */ + virtual void SetLibraryPath(const char* libraryPath) = 0; + + /** + * \brief Set the path to inputstream.adaptive's user/profile directory + * \param profilePath The path to inputstream.adaptive's user/profile directory + */ + virtual void SetProfilePath(const std::string& profilePath) = 0; + + /** + * \brief Set whether to enable saving of license challenge/response data for debugging + * \param isDebugSaveLicense True to save data, otherwise false + */ + virtual void SetDebugSaveLicense(bool isDebugSaveLicense) = 0; + + /** + * \brief Get the auxillary library path + * \return The auxillary library path + */ + virtual const char* GetLibraryPath() const = 0; + + /** + * \brief Get the path to inputstream.adaptive's user/profile directory + * \return The path to inputstream.adaptive's user/profile directory + */ + virtual const char* GetProfilePath() const = 0; + + /** + * \brief Get whether to enable saving of license challenge/response data for debugging + * \return True to save data, otherwise false + */ + virtual const bool IsDebugSaveLicense() const = 0; +}; +}; // namespace DRM diff --git a/src/decrypters/widevine/CMakeLists.txt b/src/decrypters/widevine/CMakeLists.txt new file mode 100644 index 000000000..2d27fdfb0 --- /dev/null +++ b/src/decrypters/widevine/CMakeLists.txt @@ -0,0 +1,22 @@ +set(SOURCES + WVCencSingleSampleDecrypter.cpp + WVCdmAdapter.cpp + CdmFixedBuffer.cpp + WVDecrypter.cpp + jsmn.c + CdmTypeConversion.cpp +) + +set(HEADERS + WVCencSingleSampleDecrypter.h + WVCdmAdapter.h + CdmFixedBuffer.h + WVDecrypter.h + jsmn.h + CdmTypeConversion.h +) + +add_dir_sources(SOURCES HEADERS) + +# Native CDM library +add_dependency(cdm_library lib/cdm) diff --git a/src/decrypters/widevine/CdmBuffer.h b/src/decrypters/widevine/CdmBuffer.h new file mode 100644 index 000000000..7ade69117 --- /dev/null +++ b/src/decrypters/widevine/CdmBuffer.h @@ -0,0 +1,30 @@ +/* + * 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 "cdm/media/cdm/cdm_adapter.h" + +#include +#include + +class ATTR_DLL_LOCAL CdmBuffer : public cdm::Buffer +{ +public: + CdmBuffer(AP4_DataBuffer* buffer) : m_buffer(buffer){}; + virtual ~CdmBuffer(){}; + + virtual void Destroy() override{}; + + virtual uint32_t Capacity() const override { return m_buffer->GetBufferSize(); }; + virtual uint8_t* Data() override { return (uint8_t*)m_buffer->GetData(); }; + virtual void SetSize(uint32_t size) override { m_buffer->SetDataSize(size); }; + virtual uint32_t Size() const override { return m_buffer->GetDataSize(); }; + +private: + AP4_DataBuffer* m_buffer; +}; diff --git a/src/decrypters/widevine/CdmDecryptedBlock.h b/src/decrypters/widevine/CdmDecryptedBlock.h new file mode 100644 index 000000000..69f76568c --- /dev/null +++ b/src/decrypters/widevine/CdmDecryptedBlock.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 "cdm/media/cdm/cdm_adapter.h" + +class ATTR_DLL_LOCAL CdmDecryptedBlock : public cdm::DecryptedBlock +{ +public: + CdmDecryptedBlock() : m_buffer(0), m_timeStamp(0){}; + virtual ~CdmDecryptedBlock(){}; + + virtual void SetDecryptedBuffer(cdm::Buffer* buffer) override { m_buffer = buffer; }; + virtual cdm::Buffer* DecryptedBuffer() override { return m_buffer; }; + + virtual void SetTimestamp(int64_t timestamp) override { m_timeStamp = timestamp; }; + virtual int64_t Timestamp() const override { return m_timeStamp; }; + +private: + cdm::Buffer* m_buffer; + int64_t m_timeStamp; +}; diff --git a/src/decrypters/widevine/CdmFixedBuffer.cpp b/src/decrypters/widevine/CdmFixedBuffer.cpp new file mode 100644 index 000000000..1115dde9a --- /dev/null +++ b/src/decrypters/widevine/CdmFixedBuffer.cpp @@ -0,0 +1,27 @@ +/* + * 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 "CdmFixedBuffer.h" + +#include "WVDecrypter.h" + +void CdmFixedBuffer::Destroy() +{ + m_host->ReleaseBuffer(m_instance, m_buffer); + delete this; +} + +void CdmFixedBuffer::initialize(void* instance, uint8_t* data, size_t dataSize, void* buffer, CWVDecrypter* host) +{ + m_instance = instance; + m_data = data; + m_dataSize = 0; + m_capacity = dataSize; + m_buffer = buffer; + m_host = host; +} diff --git a/src/decrypters/widevine/CdmFixedBuffer.h b/src/decrypters/widevine/CdmFixedBuffer.h new file mode 100644 index 000000000..2e76b9a92 --- /dev/null +++ b/src/decrypters/widevine/CdmFixedBuffer.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. + */ + +#pragma once + +#include "CdmBuffer.h" + +class CWVDecrypter; + +class ATTR_DLL_LOCAL CdmFixedBuffer : public cdm::Buffer +{ +public: + CdmFixedBuffer() + : m_data(nullptr), + m_dataSize(0), + m_capacity(0), + m_buffer(nullptr), + m_instance(nullptr), + m_host(nullptr){}; + virtual ~CdmFixedBuffer(){}; + + virtual void Destroy() override; + + virtual uint32_t Capacity() const override { return m_capacity; }; + + virtual uint8_t* Data() override { return m_data; }; + + virtual void SetSize(uint32_t size) override { m_dataSize = size; }; + + virtual uint32_t Size() const override { return m_dataSize; }; + + void initialize(void* instance, uint8_t* data, size_t dataSize, void* buffer, CWVDecrypter* host); + + void* Buffer() const { return m_buffer; }; + +private: + uint8_t* m_data; + size_t m_dataSize; + size_t m_capacity; + void* m_buffer; + void* m_instance; + CWVDecrypter* m_host; +}; diff --git a/wvdecrypter/cdm/media/cdm/cdm_type_conversion.cc b/src/decrypters/widevine/CdmTypeConversion.cpp similarity index 56% rename from wvdecrypter/cdm/media/cdm/cdm_type_conversion.cc rename to src/decrypters/widevine/CdmTypeConversion.cpp index d09ea7e8e..206fb6326 100644 --- a/wvdecrypter/cdm/media/cdm/cdm_type_conversion.cc +++ b/src/decrypters/widevine/CdmTypeConversion.cpp @@ -6,10 +6,12 @@ * See LICENSES/README.md for more information. */ -#include "cdm_type_conversion.h" -#include "../../../Helper.h" +#include "CdmTypeConversion.h" -using namespace SSD; +#include "../../../src/utils/CryptoUtils.h" +#include "../../../src/utils/log.h" + +using namespace media; std::string media::CdmStatusToString(const cdm::Status status) { @@ -49,132 +51,132 @@ cdm::EncryptionScheme media::ToCdmEncryptionScheme(const CryptoMode cryptoMode) } } -cdm::VideoCodec media::ToCdmVideoCodec(const SSD::Codec codec) +cdm::VideoCodec media::ToCdmVideoCodec(const VIDEOCODEC_TYPE codec) { switch (codec) { - case SSD::Codec::CodecH264: + case VIDEOCODEC_H264: return cdm::VideoCodec::kCodecH264; - case SSD::Codec::CodecVp8: + case VIDEOCODEC_VP8: return cdm::VideoCodec::kCodecVp8; - case SSD::Codec::CodecVp9: + case VIDEOCODEC_VP9: return cdm::VideoCodec::kCodecVp9; - case SSD::Codec::CodecAv1: + case VIDEOCODEC_AV1: return cdm::VideoCodec::kCodecAv1; default: - LOG::LogF(SSDWARNING, "Unknown video codec %i", codec); + LOG::LogF(LOGWARNING, "Unknown video codec %i", codec); return cdm::VideoCodec::kUnknownVideoCodec; } } -cdm::VideoCodecProfile media::ToCdmVideoCodecProfile(const SSD::CodecProfile profile) +cdm::VideoCodecProfile media::ToCdmVideoCodecProfile(const STREAMCODEC_PROFILE profile) { switch (profile) { - case SSD::CodecProfile::H264CodecProfileBaseline: + case STREAMCODEC_PROFILE::H264CodecProfileBaseline: return cdm::VideoCodecProfile::kH264ProfileBaseline; - case SSD::CodecProfile::H264CodecProfileMain: + case STREAMCODEC_PROFILE::H264CodecProfileMain: return cdm::VideoCodecProfile::kH264ProfileMain; - case SSD::CodecProfile::H264CodecProfileExtended: + case STREAMCODEC_PROFILE::H264CodecProfileExtended: return cdm::VideoCodecProfile::kH264ProfileExtended; - case SSD::CodecProfile::H264CodecProfileHigh: + case STREAMCODEC_PROFILE::H264CodecProfileHigh: return cdm::VideoCodecProfile::kH264ProfileHigh; - case SSD::CodecProfile::H264CodecProfileHigh10: + case STREAMCODEC_PROFILE::H264CodecProfileHigh10: return cdm::VideoCodecProfile::kH264ProfileHigh10; - case SSD::CodecProfile::H264CodecProfileHigh422: + case STREAMCODEC_PROFILE::H264CodecProfileHigh422: return cdm::VideoCodecProfile::kH264ProfileHigh422; - case SSD::CodecProfile::H264CodecProfileHigh444Predictive: + case STREAMCODEC_PROFILE::H264CodecProfileHigh444Predictive: return cdm::VideoCodecProfile::kH264ProfileHigh444Predictive; - case SSD::CodecProfile::VP9CodecProfile0: + case STREAMCODEC_PROFILE::VP9CodecProfile0: return cdm::VideoCodecProfile::kVP9Profile0; - case SSD::CodecProfile::VP9CodecProfile1: + case STREAMCODEC_PROFILE::VP9CodecProfile1: return cdm::VideoCodecProfile::kVP9Profile1; - case SSD::CodecProfile::VP9CodecProfile2: + case STREAMCODEC_PROFILE::VP9CodecProfile2: return cdm::VideoCodecProfile::kVP9Profile2; - case SSD::CodecProfile::VP9CodecProfile3: + case STREAMCODEC_PROFILE::VP9CodecProfile3: return cdm::VideoCodecProfile::kVP9Profile3; - case SSD::CodecProfile::AV1CodecProfileMain: + case STREAMCODEC_PROFILE::AV1CodecProfileMain: return cdm::VideoCodecProfile::kAv1ProfileMain; - case SSD::CodecProfile::AV1CodecProfileHigh: + case STREAMCODEC_PROFILE::AV1CodecProfileHigh: return cdm::VideoCodecProfile::kAv1ProfileHigh; - case SSD::CodecProfile::AV1CodecProfileProfessional: + case STREAMCODEC_PROFILE::AV1CodecProfileProfessional: return cdm::VideoCodecProfile::kAv1ProfilePro; - case SSD::CodecProfile::CodecProfileNotNeeded: + case STREAMCODEC_PROFILE::CodecProfileNotNeeded: return cdm::VideoCodecProfile::kProfileNotNeeded; default: - LOG::LogF(SSDWARNING, "Unknown codec profile %i", profile); + LOG::LogF(LOGWARNING, "Unknown codec profile %i", profile); return cdm::VideoCodecProfile::kUnknownVideoCodecProfile; } } -cdm::VideoFormat media::ToCdmVideoFormat(const SSD::SSD_VIDEOFORMAT format) +cdm::VideoFormat media::ToCdmVideoFormat(const VIDEOCODEC_FORMAT format) { switch (format) { - case SSD::SSD_VIDEOFORMAT::VideoFormatYV12: + case VIDEOCODEC_FORMAT::VIDEOCODEC_FORMAT_YV12: return cdm::VideoFormat::kYv12; - case SSD::SSD_VIDEOFORMAT::VideoFormatI420: + case VIDEOCODEC_FORMAT::VIDEOCODEC_FORMAT_I420: return cdm::VideoFormat::kI420; - case SSD::SSD_VIDEOFORMAT::VideoFormatYUV420P9: + case VIDEOCODEC_FORMAT::VIDEOCODEC_FORMAT_YUV420P9: return cdm::VideoFormat::kYUV420P9; - case SSD::SSD_VIDEOFORMAT::VideoFormatYUV420P10: + case VIDEOCODEC_FORMAT::VIDEOCODEC_FORMAT_YUV420P10: return cdm::VideoFormat::kYUV420P10; - case SSD::SSD_VIDEOFORMAT::VideoFormatYUV420P12: + case VIDEOCODEC_FORMAT::VIDEOCODEC_FORMAT_YUV420P12: return cdm::VideoFormat::kYUV420P12; - case SSD::SSD_VIDEOFORMAT::VideoFormatYUV422P9: + case VIDEOCODEC_FORMAT::VIDEOCODEC_FORMAT_YUV422P9: return cdm::VideoFormat::kYUV422P9; - case SSD::SSD_VIDEOFORMAT::VideoFormatYUV422P10: + case VIDEOCODEC_FORMAT::VIDEOCODEC_FORMAT_YUV422P10: return cdm::VideoFormat::kYUV420P10; - case SSD::SSD_VIDEOFORMAT::VideoFormatYUV422P12: + case VIDEOCODEC_FORMAT::VIDEOCODEC_FORMAT_YUV422P12: return cdm::VideoFormat::kYUV422P12; - case SSD::SSD_VIDEOFORMAT::VideoFormatYUV444P9: + case VIDEOCODEC_FORMAT::VIDEOCODEC_FORMAT_YUV444P9: return cdm::VideoFormat::kYUV444P9; - case SSD::SSD_VIDEOFORMAT::VideoFormatYUV444P10: + case VIDEOCODEC_FORMAT::VIDEOCODEC_FORMAT_YUV444P10: return cdm::VideoFormat::kYUV444P10; - case SSD::SSD_VIDEOFORMAT::VideoFormatYUV444P12: + case VIDEOCODEC_FORMAT::VIDEOCODEC_FORMAT_YUV444P12: return cdm::VideoFormat::kYUV444P12; default: - LOG::LogF(SSDWARNING, "Unknown video format %i", format); + LOG::LogF(LOGWARNING, "Unknown video format %i", format); return cdm::VideoFormat::kUnknownVideoFormat; } } -SSD::SSD_VIDEOFORMAT media::ToSSDVideoFormat(const cdm::VideoFormat format) +VIDEOCODEC_FORMAT media::ToSSDVideoFormat(const cdm::VideoFormat format) { switch (format) { case cdm::VideoFormat::kYv12: - return SSD::SSD_VIDEOFORMAT::VideoFormatYV12; + return VIDEOCODEC_FORMAT_YV12; case cdm::VideoFormat::kI420: - return SSD::SSD_VIDEOFORMAT::VideoFormatI420; + return VIDEOCODEC_FORMAT_I420; case cdm::VideoFormat::kYUV420P9: - return SSD::SSD_VIDEOFORMAT::VideoFormatYUV420P9; + return VIDEOCODEC_FORMAT_YUV420P9; case cdm::VideoFormat::kYUV420P10: - return SSD::SSD_VIDEOFORMAT::VideoFormatYUV420P10; + return VIDEOCODEC_FORMAT_YUV420P10; case cdm::VideoFormat::kYUV420P12: - return SSD::SSD_VIDEOFORMAT::VideoFormatYUV420P12; + return VIDEOCODEC_FORMAT_YUV420P12; case cdm::VideoFormat::kYUV422P9: - return SSD::SSD_VIDEOFORMAT::VideoFormatYUV422P9; + return VIDEOCODEC_FORMAT_YUV422P9; case cdm::VideoFormat::kYUV422P10: - return SSD::SSD_VIDEOFORMAT::VideoFormatYUV422P10; + return VIDEOCODEC_FORMAT_YUV422P10; case cdm::VideoFormat::kYUV422P12: - return SSD::SSD_VIDEOFORMAT::VideoFormatYUV422P12; + return VIDEOCODEC_FORMAT_YUV422P12; case cdm::VideoFormat::kYUV444P9: - return SSD::SSD_VIDEOFORMAT::VideoFormatYUV444P9; + return VIDEOCODEC_FORMAT_YUV444P9; case cdm::VideoFormat::kYUV444P10: - return SSD::SSD_VIDEOFORMAT::VideoFormatYUV444P10; + return VIDEOCODEC_FORMAT_YUV444P10; case cdm::VideoFormat::kYUV444P12: - return SSD::SSD_VIDEOFORMAT::VideoFormatYUV444P12; + return VIDEOCODEC_FORMAT_YUV444P12; default: - LOG::LogF(SSDWARNING, "Unknown video format %i", format); - return SSD::SSD_VIDEOFORMAT::UnknownVideoFormat; + LOG::LogF(LOGWARNING, "Unknown video format %i", format); + return VIDEOCODEC_FORMAT_UNKNOWN; } } // Warning: The returned config contains raw pointers to the extra data in the // input |config|. Hence, the caller must make sure the input |config| outlives // the returned config. -cdm::VideoDecoderConfig_3 media::ToCdmVideoDecoderConfig(const SSD::SSD_VIDEOINITDATA* initData, +cdm::VideoDecoderConfig_3 media::ToCdmVideoDecoderConfig(const VIDEOCODEC_INITDATA* initData, const CryptoMode cryptoMode) { cdm::VideoDecoderConfig_3 cdmConfig{}; @@ -194,39 +196,45 @@ cdm::VideoDecoderConfig_3 media::ToCdmVideoDecoderConfig(const SSD::SSD_VIDEOINI return cdmConfig; } -void media::ToCdmInputBuffer(const SSD::SSD_SAMPLE* encryptedBuffer, +void media::ToCdmInputBuffer(const DEMUX_PACKET* encryptedBuffer, std::vector* subsamples, cdm::InputBuffer_2* inputBuffer) { - inputBuffer->data = encryptedBuffer->data; - inputBuffer->data_size = encryptedBuffer->dataSize; + inputBuffer->data = encryptedBuffer->pData; + inputBuffer->data_size = encryptedBuffer->iSize; inputBuffer->timestamp = encryptedBuffer->pts; - inputBuffer->key_id = encryptedBuffer->cryptoInfo.kid; - inputBuffer->key_id_size = encryptedBuffer->cryptoInfo.kidSize; - inputBuffer->iv = encryptedBuffer->cryptoInfo.iv; - inputBuffer->iv_size = encryptedBuffer->cryptoInfo.ivSize; + inputBuffer->key_id = encryptedBuffer->cryptoInfo->kid; + inputBuffer->key_id_size = 16; + inputBuffer->iv = encryptedBuffer->cryptoInfo->iv; + inputBuffer->iv_size = 16; - const uint16_t numSubsamples = encryptedBuffer->cryptoInfo.numSubSamples; + const uint16_t numSubsamples = + encryptedBuffer->cryptoInfo ? encryptedBuffer->cryptoInfo->numSubSamples : 0; if (numSubsamples > 0) { subsamples->reserve(numSubsamples); for (uint16_t i = 0; i < numSubsamples; i++) { - subsamples->push_back( - {encryptedBuffer->cryptoInfo.clearBytes[i], encryptedBuffer->cryptoInfo.cipherBytes[i]}); + subsamples->push_back({encryptedBuffer->cryptoInfo->clearBytes[i], + encryptedBuffer->cryptoInfo->cipherBytes[i]}); } } inputBuffer->subsamples = subsamples->data(); inputBuffer->num_subsamples = numSubsamples; - inputBuffer->encryption_scheme = - ToCdmEncryptionScheme(static_cast(encryptedBuffer->cryptoInfo.mode)); + if (encryptedBuffer->cryptoInfo) + { + inputBuffer->encryption_scheme = + ToCdmEncryptionScheme(static_cast(encryptedBuffer->cryptoInfo->mode)); // ?? + } + else + inputBuffer->encryption_scheme = cdm::EncryptionScheme::kUnencrypted; if (inputBuffer->encryption_scheme != cdm::EncryptionScheme::kUnencrypted) { - inputBuffer->pattern = {encryptedBuffer->cryptoInfo.cryptBlocks, - encryptedBuffer->cryptoInfo.skipBlocks}; + inputBuffer->pattern = {encryptedBuffer->cryptoInfo->cryptBlocks, + encryptedBuffer->cryptoInfo->skipBlocks}; } } diff --git a/wvdecrypter/cdm/media/cdm/cdm_type_conversion.h b/src/decrypters/widevine/CdmTypeConversion.h similarity index 67% rename from wvdecrypter/cdm/media/cdm/cdm_type_conversion.h rename to src/decrypters/widevine/CdmTypeConversion.h index eb7fec10d..4a7330638 100644 --- a/wvdecrypter/cdm/media/cdm/cdm_type_conversion.h +++ b/src/decrypters/widevine/CdmTypeConversion.h @@ -8,13 +8,15 @@ #pragma once -#include "../../../../src/SSD_dll.h" -#include "../../../../src/common/AdaptiveDecrypter.h" -#include "api/content_decryption_module.h" +#include "cdm/media/cdm/api/content_decryption_module.h" #include #include +#include + +enum class CryptoMode; + namespace media { @@ -24,29 +26,29 @@ std::string CdmStatusToString(const cdm::Status status); cdm::EncryptionScheme ToCdmEncryptionScheme(const CryptoMode cryptoMode); -cdm::VideoCodec ToCdmVideoCodec(const SSD::Codec codec); +cdm::VideoCodec ToCdmVideoCodec(const VIDEOCODEC_TYPE codec); -cdm::VideoCodecProfile ToCdmVideoCodecProfile(const SSD::CodecProfile profile); +cdm::VideoCodecProfile ToCdmVideoCodecProfile(const STREAMCODEC_PROFILE profile); // Video Converters -cdm::VideoFormat ToCdmVideoFormat(const SSD::SSD_VIDEOFORMAT videoFormat); +cdm::VideoFormat ToCdmVideoFormat(const VIDEOCODEC_FORMAT videoFormat); -SSD::SSD_VIDEOFORMAT ToSSDVideoFormat(const cdm::VideoFormat format); +VIDEOCODEC_FORMAT ToSSDVideoFormat(const cdm::VideoFormat format); // Aggregated Types // Warning: The returned config contains raw pointers to the extra data in the // input |config|. Hence, the caller must make sure the input |config| outlives // the returned config. -cdm::VideoDecoderConfig_3 ToCdmVideoDecoderConfig(const SSD::SSD_VIDEOINITDATA* initData, +cdm::VideoDecoderConfig_3 ToCdmVideoDecoderConfig(const VIDEOCODEC_INITDATA* initData, const CryptoMode cryptoMode); // Fill |input_buffer| based on the values in |encrypted|. |subsamples| // is used to hold some of the data. |input_buffer| will contain pointers // to data contained in |encrypted| and |subsamples|, so the lifetime of // |input_buffer| must be <= the lifetime of |encrypted| and |subsamples|. -void ToCdmInputBuffer(const SSD::SSD_SAMPLE* encryptedBuffer, +void ToCdmInputBuffer(const DEMUX_PACKET* encryptedBuffer, std::vector* subsamples, cdm::InputBuffer_2* inputBuffer); diff --git a/src/decrypters/widevine/WVCdmAdapter.cpp b/src/decrypters/widevine/WVCdmAdapter.cpp new file mode 100644 index 000000000..96573ee0c --- /dev/null +++ b/src/decrypters/widevine/WVCdmAdapter.cpp @@ -0,0 +1,137 @@ +/* + * 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 "../../utils/log.h" +#include "CdmFixedBuffer.h" +#include "WVCencSingleSampleDecrypter.h" +#include "WVCdmAdapter.h" +#include "WVDecrypter.h" + +#include + +namespace +{ +#if WIN32 + constexpr const char* LIBRARY_FILENAME = "widevinecdm.dll"; +#elif TARGET_DARWIN_EMBEDDED + constexpr const char* LIBRARY_FILENAME = "libwidevinecdm.dylib"; +#else + constexpr const char* LIBRARY_FILENAME = "libwidevinecdm.so"; +#endif +} // unnamed namespace + +CWVCdmAdapter::CWVCdmAdapter(const char* licenseURL, + const AP4_DataBuffer& serverCert, + const uint8_t config, + CWVDecrypter* host) + : m_licenseUrl(licenseURL), m_host(host), m_codecInstance(nullptr) +{ + std::string strLibPath = m_host->GetLibraryPath(); + if (strLibPath.empty()) + { + LOG::LogF(LOGERROR, "No Widevine library path specified in settings"); + return; + } + strLibPath += LIBRARY_FILENAME; + + std::string strBasePath = m_host->GetProfilePath(); + char cSep = strBasePath.back(); + strBasePath += "widevine"; + strBasePath += cSep; + kodi::vfs::CreateDirectory(strBasePath.c_str()); + + //Build up a CDM path to store decrypter specific stuff. Each domain gets it own path + const char* bspos(strchr(m_licenseUrl.c_str(), ':')); + if (!bspos || bspos[1] != '/' || bspos[2] != '/' || !(bspos = strchr(bspos + 3, '/'))) + { + LOG::LogF(LOGERROR, "Unable to find protocol inside license URL"); + return; + } + if (bspos - m_licenseUrl.c_str() > 256) + { + LOG::Log(LOGERROR, "Length of license URL domain exeeds max. size of 256"); + return; + } + char buffer[1024]; + buffer[(bspos - m_licenseUrl.c_str()) * 2] = 0; + AP4_FormatHex(reinterpret_cast(m_licenseUrl.c_str()), + bspos - m_licenseUrl.c_str(), buffer); + + strBasePath += buffer; + strBasePath += cSep; + kodi::vfs::CreateDirectory(strBasePath.c_str()); + + wv_adapter = std::shared_ptr(new media::CdmAdapter( + "com.widevine.alpha", strLibPath, strBasePath, + media::CdmConfig(false, (config & DRM::IDecrypter::CONFIG_PERSISTENTSTORAGE) != 0), + dynamic_cast(this))); + if (!wv_adapter->valid()) + { + LOG::Log(LOGERROR, "Unable to load widevine shared library (%s)", strLibPath.c_str()); + wv_adapter = nullptr; + return; + } + + if (serverCert.GetDataSize()) + wv_adapter->SetServerCertificate(0, serverCert.GetData(), serverCert.GetDataSize()); + + // For backward compatibility: If no | is found in URL, use the most common working config + if (m_licenseUrl.find('|') == std::string::npos) + m_licenseUrl += "|Content-Type=application%2Foctet-stream|R{SSM}|"; + + //wv_adapter->GetStatusForPolicy(); + //wv_adapter->QueryOutputProtectionStatus(); +} + +CWVCdmAdapter::~CWVCdmAdapter() +{ + if (wv_adapter) + { + wv_adapter->RemoveClient(); + LOG::Log(LOGERROR, "Instances: %u", wv_adapter.use_count()); + wv_adapter = nullptr; + } +} + +void CWVCdmAdapter::OnCDMMessage(const char* session, + uint32_t session_size, + CDMADPMSG msg, + const uint8_t* data, + size_t data_size, + uint32_t status) +{ + LOG::Log(LOGDEBUG, "CDMMessage: %u arrived!", msg); + std::vector::iterator b(ssds.begin()), e(ssds.end()); + for (; b != e; ++b) + if (!(*b)->GetSessionId() || strncmp((*b)->GetSessionId(), session, session_size) == 0) + break; + + if (b == ssds.end()) + return; + + if (msg == CDMADPMSG::kSessionMessage) + { + (*b)->SetSession(session, session_size, data, data_size); + } + else if (msg == CDMADPMSG::kSessionKeysChange) + (*b)->AddSessionKey(data, data_size, status); +} + +cdm::Buffer* CWVCdmAdapter::AllocateBuffer(size_t sz) +{ + VIDEOCODEC_PICTURE pic; + pic.decodedDataSize = sz; + if (m_host->GetBuffer(m_codecInstance, pic)) + { + CdmFixedBuffer* buf = new CdmFixedBuffer; + buf->initialize(m_codecInstance, pic.decodedData, pic.decodedDataSize, pic.videoBufferHandle, m_host); + return buf; + } + return nullptr; + ; +} diff --git a/src/decrypters/widevine/WVCdmAdapter.h b/src/decrypters/widevine/WVCdmAdapter.h new file mode 100644 index 000000000..605d1d862 --- /dev/null +++ b/src/decrypters/widevine/WVCdmAdapter.h @@ -0,0 +1,69 @@ +/* + * 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 "CdmBuffer.h" + +#include "cdm/media/cdm/cdm_adapter.h" +#include +#include + +class CWVDecrypter; +class CWVCencSingleSampleDecrypter; + +class ATTR_DLL_LOCAL CWVCdmAdapter : public media::CdmAdapterClient +{ +public: + CWVCdmAdapter(const char* licenseURL, + const AP4_DataBuffer& serverCert, + const uint8_t config, + CWVDecrypter* host); + virtual ~CWVCdmAdapter(); + + virtual void OnCDMMessage(const char* session, + uint32_t session_size, + CDMADPMSG msg, + const uint8_t* data, + size_t data_size, + uint32_t status) override; + + virtual cdm::Buffer* AllocateBuffer(size_t sz) override; + + void insertssd(CWVCencSingleSampleDecrypter* ssd) { ssds.push_back(ssd); }; + void removessd(CWVCencSingleSampleDecrypter* ssd) + { + std::vector::iterator res( + std::find(ssds.begin(), ssds.end(), ssd)); + if (res != ssds.end()) + ssds.erase(res); + }; + + media::CdmAdapter* GetCdmAdapter() { return wv_adapter.get(); }; + const std::string& GetLicenseURL() { return m_licenseUrl; }; + + cdm::Status DecryptAndDecodeFrame(cdm::InputBuffer_2& cdm_in, + media::CdmVideoFrame* frame, + kodi::addon::CInstanceVideoCodec* codecInstance) + { + // DecryptAndDecodeFrame calls CdmAdapter::Allocate which calls Host->GetBuffer + // that cast hostInstance to CInstanceVideoCodec to get the frame buffer + // so we have temporary set the host instance + m_codecInstance = codecInstance; + cdm::Status ret = wv_adapter->DecryptAndDecodeFrame(cdm_in, frame); + m_codecInstance = nullptr; + return ret; + } + +private: + std::shared_ptr wv_adapter; + std::string m_licenseUrl; + kodi::addon::CInstanceVideoCodec* m_codecInstance; + CWVDecrypter* m_host; + std::vector ssds; +}; diff --git a/src/decrypters/widevine/WVCencSingleSampleDecrypter.cpp b/src/decrypters/widevine/WVCencSingleSampleDecrypter.cpp new file mode 100644 index 000000000..fe502b2ce --- /dev/null +++ b/src/decrypters/widevine/WVCencSingleSampleDecrypter.cpp @@ -0,0 +1,1156 @@ +/* + * 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 "WVCencSingleSampleDecrypter.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 "CdmDecryptedBlock.h" +#include "CdmFixedBuffer.h" +#include "WVCdmAdapter.h" +#include "WVDecrypter.h" +#include "CdmTypeConversion.h" +#include "cdm/media/cdm/cdm_adapter.h" +#include "jsmn.h" + +#include +#include + +using namespace UTILS; + +void CWVCencSingleSampleDecrypter::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()); +} + +CWVCencSingleSampleDecrypter::CWVCencSingleSampleDecrypter(CWVCdmAdapter& drm, + AP4_DataBuffer& pssh, + std::string_view defaultKeyId, + bool skipSessionMessage, + CryptoMode cryptoMode, + CWVDecrypter* host) + : m_wvCdmAdapter(drm), + m_pssh(pssh), + m_hdcpVersion(99), + m_hdcpLimit(0), + m_resolutionLimit(0), + m_promiseId(1), + m_isDrained(true), + m_defaultKeyId(defaultKeyId), + m_EncryptionMode(cryptoMode), + m_host(host) +{ + SetParentIsOwner(false); + + if (pssh.GetDataSize() > 4096) + { + LOG::LogF(LOGERROR, "PSSH init data with length %u seems not to be cenc init data", + pssh.GetDataSize()); + return; + } + + m_wvCdmAdapter.insertssd(this); + + if (m_host->IsDebugSaveLicense()) + { + std::string debugFilePath = + FILESYS::PathCombine(m_host->GetProfilePath(), "EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED.init"); + + std::string data{reinterpret_cast(pssh.GetData()), pssh.GetDataSize()}; + UTILS::FILESYS::SaveFile(debugFilePath, data, true); + } + + if (memcmp(pssh.GetData() + 4, "pssh", 4) != 0) + { + unsigned int buf_size = 32 + pssh.GetDataSize(); + uint8_t buf[4096 + 32]; + + // This will request a new session and initializes session_id and message members in cdm_adapter. + // message will be used to create a license request in the step after CreateSession call. + // Initialization data is the widevine cdm pssh code in google proto style found in mpd schemeIdUri + static uint8_t proto[] = {0x00, 0x00, 0x00, 0x63, 0x70, 0x73, 0x73, 0x68, 0x00, 0x00, 0x00, + 0x00, 0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, 0x4a, 0xce, 0xa3, 0xc8, + 0x27, 0xdc, 0xd5, 0x1d, 0x21, 0xed, 0x00, 0x00, 0x00, 0x00}; + + proto[2] = static_cast((buf_size >> 8) & 0xFF); + proto[3] = static_cast(buf_size & 0xFF); + proto[30] = static_cast((pssh.GetDataSize() >> 8) & 0xFF); + proto[31] = static_cast(pssh.GetDataSize()); + + memcpy(buf, proto, sizeof(proto)); + memcpy(&buf[32], pssh.GetData(), pssh.GetDataSize()); + m_pssh.SetData(buf, buf_size); + } + + drm.GetCdmAdapter()->CreateSessionAndGenerateRequest( + m_promiseId++, cdm::SessionType::kTemporary, cdm::InitDataType::kCenc, + reinterpret_cast(m_pssh.GetData()), m_pssh.GetDataSize()); + + 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()) + ; +} + +CWVCencSingleSampleDecrypter::~CWVCencSingleSampleDecrypter() +{ + m_wvCdmAdapter.removessd(this); +} + +void CWVCencSingleSampleDecrypter::GetCapabilities(const uint8_t* 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; + + /*if (media == IDecrypter::DecrypterCapabilites::SSD_MEDIA_VIDEO) + caps.flags |= (IDecrypter::DecrypterCapabilites::SSD_SECURE_PATH | IDecrypter::DecrypterCapabilites::SSD_ANNEXB_REQUIRED); + caps.flags |= IDecrypter::DecrypterCapabilites::SSD_SINGLE_DECRYPT; + return;*/ + + //caps.flags |= (IDecrypter::DecrypterCapabilites::SSD_SECURE_PATH | IDecrypter::DecrypterCapabilites::SSD_ANNEXB_REQUIRED); + //return; + + /*for (auto k : m_keys) + if (!key || memcmp(k.m_keyId.data(), key, 16) == 0) + { + if (k.status != 0) + { + if (media == IDecrypter::DecrypterCapabilites::SSD_MEDIA_VIDEO) + caps.flags |= (IDecrypter::DecrypterCapabilites::SSD_SECURE_PATH | IDecrypter::DecrypterCapabilites::SSD_ANNEXB_REQUIRED); + else + caps.flags = IDecrypter::DecrypterCapabilites::SSD_INVALID; + } + break; + } + */ + if ((caps.flags & IDecrypter::DecrypterCapabilites::SSD_SUPPORTS_DECODING) != 0) + { + AP4_UI32 poolId(AddPool()); + m_fragmentPool[poolId].m_key = + key ? key : reinterpret_cast(m_keys.front().m_keyId.data()); + m_fragmentPool[poolId].m_cryptoInfo.m_mode = m_EncryptionMode; + + AP4_DataBuffer in; + AP4_DataBuffer out; + AP4_UI32 encryptedBytes[2] = {1, 1}; + AP4_UI16 clearBytes[2] = {5, 5}; + AP4_Byte testData[12] = {0, 0, 0, 1, 9, 255, 0, 0, 0, 1, 10, 255}; + const AP4_UI08 iv[] = {1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0}; + in.SetBuffer(testData, 12); + in.SetDataSize(12); + try + { + encryptedBytes[0] = 12; + clearBytes[0] = 0; + if (DecryptSampleData(poolId, in, out, iv, 1, clearBytes, encryptedBytes) != AP4_SUCCESS) + { + LOG::LogF(LOGDEBUG, "Single decrypt failed, secure path only"); + if (media == IDecrypter::DecrypterCapabilites::SSD_MEDIA_VIDEO) + caps.flags |= (IDecrypter::DecrypterCapabilites::SSD_SECURE_PATH | + IDecrypter::DecrypterCapabilites::SSD_ANNEXB_REQUIRED); + else + caps.flags = IDecrypter::DecrypterCapabilites::SSD_INVALID; + } + else + { + LOG::LogF(LOGDEBUG, "Single decrypt possible"); + caps.flags |= IDecrypter::DecrypterCapabilites::SSD_SINGLE_DECRYPT; + caps.hdcpVersion = 99; + caps.hdcpLimit = m_resolutionLimit; + } + } + catch (const std::exception& e) + { + LOG::LogF(LOGDEBUG, "Decrypt error, assuming secure path: %s", e.what()); + caps.flags |= (IDecrypter::DecrypterCapabilites::SSD_SECURE_PATH | + IDecrypter::DecrypterCapabilites::SSD_ANNEXB_REQUIRED); + } + RemovePool(poolId); + } + else + { + LOG::LogF(LOGDEBUG, "Decoding not supported"); + } +} + +const char* CWVCencSingleSampleDecrypter::GetSessionId() +{ + return m_strSession.empty() ? nullptr : m_strSession.c_str(); +} + +void CWVCencSingleSampleDecrypter::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, "Widevine session ID %s closed", m_strSession.c_str()); + m_strSession.clear(); + } +} + +AP4_DataBuffer CWVCencSingleSampleDecrypter::GetChallengeData() +{ + return m_challenge; +} + +void CWVCencSingleSampleDecrypter::CheckLicenseRenewal() +{ + { + std::lock_guard lock(m_renewalLock); + if (!m_challenge.GetDataSize()) + return; + } + SendSessionMessage(); +} + +bool CWVCencSingleSampleDecrypter::SendSessionMessage() +{ + std::vector blocks{StringUtils::Split(m_wvCdmAdapter.GetLicenseURL(), '|')}; + + 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(), "EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED.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(), "EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED.response"); + FILESYS::SaveFile(debugFilePath, response, true); + } + + if (serverCertRequest && contentType.find("application/octet-stream") == std::string::npos) + serverCertRequest = false; + + if (!blocks[3].empty() && !serverCertRequest) + { + if (blocks[3][0] == 'J' || (blocks[3].size() > 1 && blocks[3][0] == 'B' && blocks[3][1] == 'J')) + { + int dataPos = 2; + + if (response.size() >= 3 && blocks[3][0] == 'B') + { + response = BASE64::Decode(response); + dataPos = 3; + } + + jsmn_parser jsn; + jsmntok_t tokens[256]; + + jsmn_init(&jsn); + int i(0), numTokens = jsmn_parse(&jsn, response.c_str(), response.size(), tokens, 256); + + std::vector jsonVals{StringUtils::Split(blocks[3].substr(dataPos), ';')}; + + // Find HDCP limit + if (jsonVals.size() > 1) + { + for (; i < numTokens; ++i) + if (tokens[i].type == JSMN_STRING && tokens[i].size == 1 && + jsonVals[1].size() == static_cast(tokens[i].end - tokens[i].start) && + strncmp(response.c_str() + tokens[i].start, jsonVals[1].c_str(), + tokens[i].end - tokens[i].start) == 0) + break; + if (i < numTokens) + m_hdcpLimit = std::atoi((response.c_str() + tokens[i + 1].start)); + } + // Find license key + if (jsonVals.size() > 0) + { + for (i = 0; i < numTokens; ++i) + if (tokens[i].type == JSMN_STRING && tokens[i].size == 1 && + jsonVals[0].size() == static_cast(tokens[i].end - tokens[i].start) && + strncmp(response.c_str() + tokens[i].start, jsonVals[0].c_str(), + tokens[i].end - tokens[i].start) == 0) + { + if (i + 1 < numTokens && tokens[i + 1].type == JSMN_ARRAY && tokens[i + 1].size == 1) + ++i; + break; + } + } + else + i = numTokens; + + if (i < numTokens) + { + std::string respData{ + response.substr(tokens[i + 1].start, tokens[i + 1].end - tokens[i + 1].start)}; + + if (blocks[3][dataPos - 1] == 'B') + { + respData = BASE64::Decode(respData); + } + + m_wvCdmAdapter.GetCdmAdapter()->UpdateSession( + ++m_promiseId, m_strSession.data(), m_strSession.size(), + reinterpret_cast(respData.c_str()), respData.size()); + } + else + { + LOG::LogF(LOGERROR, "Unable to find %s in JSON string", blocks[3].c_str() + 2); + return false; + } + } + else if (blocks[3][0] == 'H' && blocks[3].size() >= 2) + { + //Find the payload + std::string::size_type payloadPos = response.find("\r\n\r\n"); + if (payloadPos != std::string::npos) + { + payloadPos += 4; + if (blocks[3][1] == 'B') + m_wvCdmAdapter.GetCdmAdapter()->UpdateSession( + ++m_promiseId, m_strSession.data(), m_strSession.size(), + reinterpret_cast(response.c_str() + payloadPos), + response.size() - payloadPos); + else + { + LOG::LogF(LOGERROR, "Unsupported HTTP payload data type definition"); + return false; + } + } + else + { + LOG::LogF(LOGERROR, "Unable to find HTTP payload in response"); + return false; + } + } + else if (blocks[3][0] == 'B' && blocks[3].size() == 1) + { + std::string decRespData{BASE64::Decode(response)}; + + m_wvCdmAdapter.GetCdmAdapter()->UpdateSession( + ++m_promiseId, m_strSession.data(), m_strSession.size(), + reinterpret_cast(decRespData.c_str()), decRespData.size()); + } + else + { + LOG::LogF(LOGERROR, "Unsupported License request template (response)"); + return false; + } + } + else // its binary - simply push the returned data as update + { + 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 CWVCencSingleSampleDecrypter::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 CWVCencSingleSampleDecrypter::HasKeyId(const uint8_t* keyid) +{ + if (keyid) + for (std::vector::const_iterator kb(m_keys.begin()), ke(m_keys.end()); kb != ke; ++kb) + if (kb->m_keyId.size() == 16 && memcmp(kb->m_keyId.c_str(), keyid, 16) == 0) + return true; + return false; +} + +AP4_Result CWVCencSingleSampleDecrypter::SetFragmentInfo(AP4_UI32 poolId, + const AP4_UI08* key, + 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 = key; + 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 CWVCencSingleSampleDecrypter::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 CWVCencSingleSampleDecrypter::RemovePool(AP4_UI32 poolId) +{ + m_fragmentPool[poolId].m_nalLengthSize = 99; + m_fragmentPool[poolId].m_key = nullptr; +} + +void CWVCencSingleSampleDecrypter::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 CWVCencSingleSampleDecrypter::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 CWVCencSingleSampleDecrypter::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 CWVCencSingleSampleDecrypter::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 CWVCencSingleSampleDecrypter::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; + 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 CWVCencSingleSampleDecrypter::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) +{ + if (!m_wvCdmAdapter.GetCdmAdapter()) + { + dataOut.SetData(dataIn.GetData(), dataIn.GetDataSize()); + return AP4_SUCCESS; + } + + FINFO& fragInfo(m_fragmentPool[poolId]); + + if (fragInfo.m_decrypterFlags & + IDecrypter::DecrypterCapabilites::SSD_SECURE_PATH) //we can not decrypt only + { + if (fragInfo.m_nalLengthSize > 4) + { + LOG::LogF(LOGERROR, "Nalu length size > 4 not supported"); + return AP4_ERROR_NOT_SUPPORTED; + } + + AP4_UI16 dummyClear(0); + AP4_UI32 dummyCipher(dataIn.GetDataSize()); + + if (iv) + { + if (!subsampleCount) + { + subsampleCount = 1; + bytesOfCleartextData = &dummyClear; + bytesOfEncryptedData = &dummyCipher; + } + + dataOut.SetData(reinterpret_cast(&subsampleCount), sizeof(subsampleCount)); + dataOut.AppendData(reinterpret_cast(bytesOfCleartextData), + subsampleCount * sizeof(AP4_UI16)); + dataOut.AppendData(reinterpret_cast(bytesOfEncryptedData), + subsampleCount * sizeof(AP4_UI32)); + dataOut.AppendData(reinterpret_cast(iv), 16); + dataOut.AppendData(reinterpret_cast(fragInfo.m_key), 16); + } + else + { + dataOut.SetDataSize(0); + bytesOfCleartextData = &dummyClear; + bytesOfEncryptedData = &dummyCipher; + } + + if (fragInfo.m_nalLengthSize && (!iv || bytesOfCleartextData[0] > 0)) + { + //check NAL / subsample + const AP4_Byte *packetIn(dataIn.GetData()), + *packetInEnd(dataIn.GetData() + dataIn.GetDataSize()); + AP4_UI16* clrb_out( + iv ? reinterpret_cast(dataOut.UseData() + sizeof(subsampleCount)) + : nullptr); // is there a use for this? + size_t nalUnitCount = 0; // is there a use for this? + size_t nalUnitSum = 0; + size_t configSize = 0; // is there a use for this? + + while (packetIn < packetInEnd) + { + uint32_t nalSize(0); + for (size_t i = 0; i < fragInfo.m_nalLengthSize; ++i) + { + nalSize = (nalSize << 8) + *packetIn++; + }; + + //look if we have to inject sps / pps + if (fragInfo.m_annexbSpsPps.GetDataSize() && (*packetIn & 0x1F) != 9 /*AVC_NAL_AUD*/) + { + dataOut.AppendData(fragInfo.m_annexbSpsPps.GetData(), + fragInfo.m_annexbSpsPps.GetDataSize()); + if (clrb_out) + *clrb_out += fragInfo.m_annexbSpsPps.GetDataSize(); + configSize = fragInfo.m_annexbSpsPps.GetDataSize(); + fragInfo.m_annexbSpsPps.SetDataSize(0); + } + + // Annex-B Start pos + static AP4_Byte annexbStartCode[4] = {0x00, 0x00, 0x00, 0x01}; + dataOut.AppendData(annexbStartCode, 4); + dataOut.AppendData(packetIn, nalSize); + packetIn += nalSize; + if (clrb_out) + *clrb_out += (4 - fragInfo.m_nalLengthSize); + ++nalUnitCount; + + if (!iv) + { + nalUnitSum = 0; + } + else if (nalSize + fragInfo.m_nalLengthSize + nalUnitSum >= + *bytesOfCleartextData + *bytesOfEncryptedData) + { + AP4_UI32 summedBytes(0); + do + { + summedBytes += *bytesOfCleartextData + *bytesOfEncryptedData; + ++bytesOfCleartextData; + ++bytesOfEncryptedData; + ++clrb_out; + --subsampleCount; + } while (subsampleCount && nalSize + fragInfo.m_nalLengthSize + nalUnitSum > summedBytes); + + if (nalSize + fragInfo.m_nalLengthSize + nalUnitSum > summedBytes) + { + LOG::LogF(LOGERROR, "NAL Unit exceeds subsample definition (nls: %u) %u -> %u ", + static_cast(fragInfo.m_nalLengthSize), + static_cast(nalSize + fragInfo.m_nalLengthSize + nalUnitSum), + summedBytes); + return AP4_ERROR_NOT_SUPPORTED; + } + nalUnitSum = 0; + } + else + nalUnitSum += nalSize + fragInfo.m_nalLengthSize; + } + if (packetIn != packetInEnd || subsampleCount) + { + LOG::Log(LOGERROR, "NAL Unit definition incomplete (nls: %u) %u -> %u ", + static_cast(fragInfo.m_nalLengthSize), + static_cast(packetInEnd - packetIn), subsampleCount); + return AP4_ERROR_NOT_SUPPORTED; + } + } + else + dataOut.AppendData(dataIn.GetData(), dataIn.GetDataSize()); + return AP4_SUCCESS; + } + + if (!fragInfo.m_key) + { + LOG::LogF(LOGDEBUG, "No Key"); + return AP4_ERROR_INVALID_PARAMETERS; + } + + dataOut.SetDataSize(0); + + uint16_t clearBytes = 0; + uint32_t encryptedBytes = dataIn.GetDataSize(); + + // check input parameters + if (iv == NULL) + return AP4_ERROR_INVALID_PARAMETERS; + if (subsampleCount) + { + if (bytesOfCleartextData == NULL || bytesOfEncryptedData == NULL) + { + LOG::LogF(LOGDEBUG, "Invalid input params"); + return AP4_ERROR_INVALID_PARAMETERS; + } + } + else + { + subsampleCount = 1; + bytesOfCleartextData = &clearBytes; + bytesOfEncryptedData = &encryptedBytes; + } + cdm::Status ret{cdm::Status::kSuccess}; + std::vector subsamples; + subsamples.reserve(subsampleCount); + + bool useCbcDecrypt = fragInfo.m_cryptoInfo.m_mode == CryptoMode::AES_CBC; + + // We can only decrypt with subsamples set to 1 + // This must be handled differently for CENC and CBCS + // CENC: + // CDM should get 1 block of encrypted data per sample, encrypted data + // from all subsamples should be formed into a contiguous block. + // Even if there is only 1 subsample, we should remove cleartext data + // from it before passing to CDM. + // CBCS: + // Due to the nature of this cipher subsamples must be decrypted separately + + const size_t iterations = useCbcDecrypt ? subsampleCount : 1; + size_t absPos = 0; + + for (size_t i = 0; i < iterations; ++i) + { + m_decryptIn.Reserve(dataIn.GetDataSize()); + m_decryptIn.SetDataSize(0); + size_t decryptInPos = absPos; + if (useCbcDecrypt) + { + UnpackSubsampleData(dataIn, decryptInPos, i, bytesOfCleartextData, bytesOfEncryptedData); + } + else + { + for (size_t subsamplePos = 0; subsamplePos < subsampleCount; ++subsamplePos) + { + UnpackSubsampleData(dataIn, absPos, subsamplePos, bytesOfCleartextData, + bytesOfEncryptedData); + } + } + + if (m_decryptIn.GetDataSize() > 0) // remember to include when calling setcdmsubsamples + { + SetCdmSubsamples(subsamples, useCbcDecrypt); + } + + else // we have nothing to decrypt in this iteration + { + if (useCbcDecrypt) + { + dataOut.AppendData(dataIn.GetData() + absPos, bytesOfCleartextData[i]); + absPos += bytesOfCleartextData[i]; + continue; + } + else // we can exit here for CENC and just return the input buffer + { + dataOut.AppendData(dataIn.GetData(), dataIn.GetDataSize()); + return AP4_SUCCESS; + } + } + + cdm::InputBuffer_2 cdmIn; + SetInput(cdmIn, m_decryptIn, 1, iv, fragInfo, subsamples); + m_decryptOut.SetDataSize(m_decryptIn.GetDataSize()); + CdmBuffer buf = &m_decryptOut; + CdmDecryptedBlock cdmOut; + cdmOut.SetDecryptedBuffer(&buf); + + CheckLicenseRenewal(); + ret = m_wvCdmAdapter.GetCdmAdapter()->Decrypt(cdmIn, &cdmOut); + + if (ret == cdm::Status::kSuccess) + { + size_t cipherPos = 0; + if (useCbcDecrypt) + { + RepackSubsampleData(dataIn, dataOut, absPos, cipherPos, i, bytesOfCleartextData, + bytesOfEncryptedData); + } + else + { + size_t absPos = 0; + for (unsigned int i{0}; i < subsampleCount; ++i) + { + RepackSubsampleData(dataIn, dataOut, absPos, cipherPos, i, bytesOfCleartextData, + bytesOfEncryptedData); + } + } + } + else + { + LogDecryptError(ret, fragInfo.m_key); + } + } + return (ret == cdm::Status::kSuccess) ? AP4_SUCCESS : AP4_ERROR_INVALID_PARAMETERS; +} + +bool CWVCencSingleSampleDecrypter::OpenVideoDecoder(const VIDEOCODEC_INITDATA* initData) +{ + cdm::VideoDecoderConfig_3 vconfig = media::ToCdmVideoDecoderConfig(initData, m_EncryptionMode); + + // InputStream interface call OpenVideoDecoder also during playback when stream quality + // change, so we reinitialize the decoder only when the codec change + if (m_currentVideoDecConfig.has_value()) + { + cdm::VideoDecoderConfig_3& currVidConfig = *m_currentVideoDecConfig; + if (currVidConfig.codec == vconfig.codec && currVidConfig.profile == vconfig.profile) + return true; + + m_wvCdmAdapter.GetCdmAdapter()->DeinitializeDecoder(cdm::StreamType::kStreamTypeVideo); + } + + m_currentVideoDecConfig = vconfig; + + cdm::Status ret = m_wvCdmAdapter.GetCdmAdapter()->InitializeVideoDecoder(vconfig); + m_videoFrames.clear(); + m_isDrained = true; + + LOG::LogF(LOGDEBUG, "Initialization returned status: %s", media::CdmStatusToString(ret).c_str()); + return ret == cdm::Status::kSuccess; +} + +VIDEOCODEC_RETVAL CWVCencSingleSampleDecrypter::DecryptAndDecodeVideo( + kodi::addon::CInstanceVideoCodec* codecInstance, const DEMUX_PACKET* sample) +{ + // if we have an picture waiting, or not yet get the dest buffer, do nothing + if (m_videoFrames.size() == 4) + return VC_ERROR; + + if (sample->cryptoInfo && sample->cryptoInfo->numSubSamples > 0 && + (!sample->cryptoInfo->clearBytes || !sample->cryptoInfo->cipherBytes)) + { + return VC_ERROR; + } + + cdm::InputBuffer_2 inputBuffer{}; + std::vector subsamples; + + media::ToCdmInputBuffer(sample, &subsamples, &inputBuffer); + + if (sample->iSize > 0) + m_isDrained = false; + + //LICENSERENEWAL: + CheckLicenseRenewal(); + + media::CdmVideoFrame videoFrame; + cdm::Status status = + m_wvCdmAdapter.DecryptAndDecodeFrame(inputBuffer, &videoFrame, codecInstance); + + if (status == cdm::Status::kSuccess) + { + std::list::iterator f(m_videoFrames.begin()); + while (f != m_videoFrames.end() && f->Timestamp() < videoFrame.Timestamp()) + { + ++f; + } + m_videoFrames.insert(f, videoFrame); + return VC_NONE; + } + else if (status == cdm::Status::kNeedMoreData && inputBuffer.data) + { + return VC_NONE; + } + else if (status == cdm::Status::kNoKey) + { + char buf[36]; + buf[0] = 0; + buf[32] = 0; + AP4_FormatHex(inputBuffer.key_id, inputBuffer.key_id_size, buf); + LOG::LogF(LOGERROR, "Returned CDM status kNoKey for key %s", buf); + return VC_EOF; + } + + LOG::LogF(LOGDEBUG, "Returned CDM status: %i", status); + return VC_ERROR; +} + +VIDEOCODEC_RETVAL CWVCencSingleSampleDecrypter::VideoFrameDataToPicture( + kodi::addon::CInstanceVideoCodec* codecInstance, VIDEOCODEC_PICTURE* picture) +{ + if (m_videoFrames.size() == 4 || + (m_videoFrames.size() > 0 && (picture->flags & VIDEOCODEC_PICTURE_FLAG_DRAIN))) + { + media::CdmVideoFrame& videoFrame(m_videoFrames.front()); + + picture->width = videoFrame.Size().width; + picture->height = videoFrame.Size().height; + picture->pts = videoFrame.Timestamp(); + picture->decodedData = videoFrame.FrameBuffer()->Data(); + picture->decodedDataSize = videoFrame.FrameBuffer()->Size(); + picture->videoBufferHandle = static_cast(videoFrame.FrameBuffer())->Buffer(); + + for (size_t i = 0; i < cdm::VideoPlane::kMaxPlanes; ++i) + { + picture->planeOffsets[i] = videoFrame.PlaneOffset(static_cast(i)); + picture->stride[i] = videoFrame.Stride(static_cast(i)); + } + picture->videoFormat = media::ToSSDVideoFormat(videoFrame.Format()); + videoFrame.SetFrameBuffer(nullptr); //marker for "No Picture" + + delete (CdmFixedBuffer*)(videoFrame.FrameBuffer()); + m_videoFrames.pop_front(); + + return VC_PICTURE; + } + else if ((picture->flags & VIDEOCODEC_PICTURE_FLAG_DRAIN)) + { + static DEMUX_PACKET drainSample{}; + if (m_isDrained || DecryptAndDecodeVideo(codecInstance, &drainSample) == VC_ERROR) + { + m_isDrained = true; + return VC_EOF; + } + else + return VC_NONE; + } + + return VC_BUFFER; +} + +void CWVCencSingleSampleDecrypter::ResetVideo() +{ + m_wvCdmAdapter.GetCdmAdapter()->ResetDecoder(cdm::kStreamTypeVideo); + m_isDrained = true; +} + +void CWVCencSingleSampleDecrypter::SetDefaultKeyId(std::string_view keyId) +{ + m_defaultKeyId = keyId; +} + +void CWVCencSingleSampleDecrypter::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/widevine/WVCencSingleSampleDecrypter.h b/src/decrypters/widevine/WVCencSingleSampleDecrypter.h new file mode 100644 index 000000000..d4f5f7a5b --- /dev/null +++ b/src/decrypters/widevine/WVCencSingleSampleDecrypter.h @@ -0,0 +1,148 @@ +/* + * 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 CWVDecrypter; +class CWVCdmAdapter; + +namespace media +{ +class CdmVideoFrame; +} + +using namespace DRM; + +class ATTR_DLL_LOCAL CWVCencSingleSampleDecrypter : public Adaptive_CencSingleSampleDecrypter +{ +public: + // methods + CWVCencSingleSampleDecrypter(CWVCdmAdapter& drm, + AP4_DataBuffer& pssh, + std::string_view defaultKeyId, + bool skipSessionMessage, + CryptoMode cryptoMode, + CWVDecrypter* host); + virtual ~CWVCencSingleSampleDecrypter(); + + void GetCapabilities(const uint8_t* 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(const uint8_t* keyid); + + virtual AP4_Result SetFragmentInfo(AP4_UI32 poolId, + const AP4_UI08* key, + 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(); + + CWVCdmAdapter& m_wvCdmAdapter; + std::string m_strSession; + AP4_DataBuffer 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 + { + const AP4_UI08* 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; + CWVDecrypter* m_host; +}; diff --git a/src/decrypters/widevine/WVDecrypter.cpp b/src/decrypters/widevine/WVDecrypter.cpp new file mode 100644 index 000000000..93717a7f0 --- /dev/null +++ b/src/decrypters/widevine/WVDecrypter.cpp @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2016 liberty-developer (https://github.com/liberty-developer) + * 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 "WVDecrypter.h" + +#include "../../utils/Base64Utils.h" +#include "../../utils/log.h" +#include "WVCdmAdapter.h" +#include "WVCencSingleSampleDecrypter.h" + +#include + +using namespace DRM; +using namespace UTILS; +using namespace kodi::tools; + + +CWVDecrypter::~CWVDecrypter() +{ + delete m_WVCdmAdapter; + m_WVCdmAdapter = nullptr; +} + +const char* CWVDecrypter::SelectKeySytem(const char* keySystem) +{ + if (strcmp(keySystem, "com.widevine.alpha")) + return nullptr; + + return "urn:uuid:EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED"; +} + +bool CWVDecrypter::OpenDRMSystem(const char* licenseURL, + const AP4_DataBuffer& serverCertificate, + const uint8_t config) +{ + m_WVCdmAdapter = new CWVCdmAdapter(licenseURL, serverCertificate, config, this); + + return m_WVCdmAdapter->GetCdmAdapter() != nullptr; +} + +Adaptive_CencSingleSampleDecrypter* CWVDecrypter::CreateSingleSampleDecrypter( + AP4_DataBuffer& pssh, + const char* optionalKeyParameter, + std::string_view defaultKeyId, + bool skipSessionMessage, + CryptoMode cryptoMode) +{ + CWVCencSingleSampleDecrypter* decrypter = new CWVCencSingleSampleDecrypter( + *m_WVCdmAdapter, pssh, defaultKeyId, skipSessionMessage, cryptoMode, this); + if (!decrypter->GetSessionId()) + { + delete decrypter; + decrypter = nullptr; + } + return decrypter; +} + +void CWVDecrypter::DestroySingleSampleDecrypter(Adaptive_CencSingleSampleDecrypter* decrypter) +{ + if (decrypter) + { + // close session before dispose + static_cast(decrypter)->CloseSessionId(); + delete static_cast(decrypter); + } +} + +void CWVDecrypter::GetCapabilities(Adaptive_CencSingleSampleDecrypter* decrypter, + const uint8_t* keyId, + uint32_t media, + IDecrypter::DecrypterCapabilites& caps) +{ + if (!decrypter) + { + caps = {0, 0, 0}; + return; + } + + static_cast(decrypter)->GetCapabilities(keyId, media, caps); +} + +bool CWVDecrypter::HasLicenseKey(Adaptive_CencSingleSampleDecrypter* decrypter, + const uint8_t* keyId) +{ + if (decrypter) + return static_cast(decrypter)->HasKeyId(keyId); + return false; +} + +std::string CWVDecrypter::GetChallengeB64Data(Adaptive_CencSingleSampleDecrypter* decrypter) +{ + if (!decrypter) + return ""; + + AP4_DataBuffer challengeData = + static_cast(decrypter)->GetChallengeData(); + return BASE64::Encode(challengeData.GetData(), challengeData.GetDataSize()); +} + +bool CWVDecrypter::OpenVideoDecoder(Adaptive_CencSingleSampleDecrypter* decrypter, + const VIDEOCODEC_INITDATA* initData) +{ + if (!decrypter || !initData) + return false; + + m_decodingDecrypter = static_cast(decrypter); + return m_decodingDecrypter->OpenVideoDecoder(initData); +} + +VIDEOCODEC_RETVAL CWVDecrypter::DecryptAndDecodeVideo( + kodi::addon::CInstanceVideoCodec* codecInstance, const DEMUX_PACKET* sample) +{ + if (!m_decodingDecrypter) + return VC_ERROR; + + return m_decodingDecrypter->DecryptAndDecodeVideo(codecInstance, sample); +} + +VIDEOCODEC_RETVAL CWVDecrypter::VideoFrameDataToPicture( + kodi::addon::CInstanceVideoCodec* codecInstance, VIDEOCODEC_PICTURE* picture) +{ + if (!m_decodingDecrypter) + return VC_ERROR; + + return m_decodingDecrypter->VideoFrameDataToPicture(codecInstance, picture); +} + +void CWVDecrypter::ResetVideo() +{ + if (m_decodingDecrypter) + m_decodingDecrypter->ResetVideo(); +} + +void CWVDecrypter::SetLibraryPath(const char* libraryPath) +{ + m_strLibraryPath = libraryPath; + + const char* pathSep{libraryPath[0] && libraryPath[1] == ':' && isalpha(libraryPath[0]) ? "\\" + : "/"}; + + if (m_strLibraryPath.size() && m_strLibraryPath.back() != pathSep[0]) + m_strLibraryPath += pathSep; +} + +void CWVDecrypter::SetProfilePath(const std::string& profilePath) +{ + m_strProfilePath = profilePath; + + const char* pathSep{profilePath[0] && profilePath[1] == ':' && isalpha(profilePath[0]) ? "\\" + : "/"}; + + if (m_strProfilePath.size() && 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.c_str()); + m_strProfilePath += "cdm"; + m_strProfilePath += pathSep; + kodi::vfs::CreateDirectory(m_strProfilePath.c_str()); +} + +bool CWVDecrypter::GetBuffer(void* instance, VIDEOCODEC_PICTURE& picture) +{ + return instance ? static_cast(instance)->GetFrameBuffer( + *reinterpret_cast(&picture)) + : false; +} + +void CWVDecrypter::ReleaseBuffer(void* instance, void* buffer) +{ + if (instance) + static_cast(instance)->ReleaseFrameBuffer(buffer); +} diff --git a/src/decrypters/widevine/WVDecrypter.h b/src/decrypters/widevine/WVDecrypter.h new file mode 100644 index 000000000..5e810598b --- /dev/null +++ b/src/decrypters/widevine/WVDecrypter.h @@ -0,0 +1,72 @@ +/* + * 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 + +class CWVCdmAdapter; +class CWVCencSingleSampleDecrypter; + +using namespace DRM; +using namespace kodi::tools; + + +/*********************************************************************************************/ + +class ATTR_DLL_LOCAL CWVDecrypter : public IDecrypter +{ +public: + CWVDecrypter() : m_WVCdmAdapter(nullptr), m_decodingDecrypter(nullptr){}; + virtual ~CWVDecrypter() override; + + virtual const char* SelectKeySytem(const char* keySystem) override; + virtual bool OpenDRMSystem(const char* licenseURL, + const AP4_DataBuffer& serverCertificate, + const uint8_t config) override; + virtual Adaptive_CencSingleSampleDecrypter* CreateSingleSampleDecrypter( + AP4_DataBuffer& pssh, + const char* optionalKeyParameter, + std::string_view defaultKeyId, + bool skipSessionMessage, + CryptoMode cryptoMode) override; + virtual void DestroySingleSampleDecrypter(Adaptive_CencSingleSampleDecrypter* decrypter) override; + virtual void GetCapabilities(Adaptive_CencSingleSampleDecrypter* decrypter, + const uint8_t* keyId, + uint32_t media, + IDecrypter::DecrypterCapabilites& caps) override; + virtual bool HasLicenseKey(Adaptive_CencSingleSampleDecrypter* decrypter, + const uint8_t* keyId) override; + virtual bool IsInitialised() override { return m_WVCdmAdapter != nullptr; } + virtual 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; + virtual void SetLibraryPath(const char* libraryPath) override; + virtual void SetProfilePath(const std::string& profilePath) override; + virtual void SetDebugSaveLicense(bool isDebugSaveLicense) override + { + m_isDebugSaveLicense = isDebugSaveLicense; + } + virtual bool GetBuffer(void* instance, VIDEOCODEC_PICTURE& picture); + virtual void ReleaseBuffer(void* instance, void* buffer); + virtual const char* GetLibraryPath() const override { return m_strLibraryPath.c_str(); } + virtual const char* GetProfilePath() const override { return m_strProfilePath.c_str(); } + virtual const bool IsDebugSaveLicense() const override { return m_isDebugSaveLicense; } + +private: + CWVCdmAdapter* m_WVCdmAdapter; + CWVCencSingleSampleDecrypter* m_decodingDecrypter; + std::string m_strProfilePath; + std::string m_strLibraryPath; + bool m_isDebugSaveLicense; +}; diff --git a/wvdecrypter/jsmn.c b/src/decrypters/widevine/jsmn.c similarity index 100% rename from wvdecrypter/jsmn.c rename to src/decrypters/widevine/jsmn.c diff --git a/wvdecrypter/jsmn.h b/src/decrypters/widevine/jsmn.h similarity index 100% rename from wvdecrypter/jsmn.h rename to src/decrypters/widevine/jsmn.h diff --git a/src/decrypters/widevineandroid/CMakeLists.txt b/src/decrypters/widevineandroid/CMakeLists.txt new file mode 100644 index 000000000..a0fa971c8 --- /dev/null +++ b/src/decrypters/widevineandroid/CMakeLists.txt @@ -0,0 +1,18 @@ +set(SOURCES + WVCencSingleSampleDecrypter.cpp + WVCdmAdapter.cpp + WVDecrypter.cpp + jsmn.c +) + +set(HEADERS + WVCencSingleSampleDecrypter.h + WVCdmAdapter.h + WVDecrypter.h + jsmn.h +) + +add_dir_sources(SOURCES HEADERS) + +# JNI android wrapper library +add_dependency(jni lib/jni) diff --git a/src/decrypters/widevineandroid/WVCdmAdapter.cpp b/src/decrypters/widevineandroid/WVCdmAdapter.cpp new file mode 100644 index 000000000..1bf5ff33d --- /dev/null +++ b/src/decrypters/widevineandroid/WVCdmAdapter.cpp @@ -0,0 +1,209 @@ +/* + * 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 "WVCdmAdapter.h" + +#include "../../utils/log.h" +#include "WVDecrypter.h" + +#include +#include + +using namespace DRM; +using namespace jni; + +CWVCdmAdapterA::CWVCdmAdapterA(WV_KEYSYSTEM ks, + const char* licenseURL, + const AP4_DataBuffer& serverCert, + CJNIMediaDrmOnEventListener* listener, + CWVDecrypterA* host) + : m_keySystem(ks), m_mediaDrm(0), m_licenseUrl(licenseURL), m_host(host) +{ + std::string strBasePath = m_host->GetProfilePath(); + char cSep = strBasePath.back(); + strBasePath += ks == WIDEVINE ? "widevine" : ks == PLAYREADY ? "playready" : "wiseplay"; + strBasePath += cSep; + kodi::vfs::CreateDirectory(strBasePath.c_str()); + + //Build up a CDM path to store decrypter specific stuff. Each domain gets it own path + const char* bspos(strchr(m_licenseUrl.c_str(), ':')); + if (!bspos || bspos[1] != '/' || bspos[2] != '/' || !(bspos = strchr(bspos + 3, '/'))) + { + LOG::Log(LOGERROR, "Unable to find protocol inside license URL"); + return; + } + if (bspos - m_licenseUrl.c_str() > 256) + { + LOG::Log(LOGERROR, "Length of license URL exeeds max. size of 256"); + return; + } + char buffer[1024]; + buffer[(bspos - m_licenseUrl.c_str()) * 2] = 0; + AP4_FormatHex(reinterpret_cast(m_licenseUrl.c_str()), + bspos - m_licenseUrl.c_str(), buffer); + + strBasePath += buffer; + strBasePath += cSep; + kodi::vfs::CreateDirectory(strBasePath.c_str()); + m_strBasePath = strBasePath; + + int64_t mostSigBits(0), leastSigBits(0); + const uint8_t* keySystem = GetKeySystem(); + for (unsigned int i(0); i < 8; ++i) + mostSigBits = (mostSigBits << 8) | keySystem[i]; + for (unsigned int i(8); i < 16; ++i) + leastSigBits = (leastSigBits << 8) | keySystem[i]; + + CJNIUUID uuid(mostSigBits, leastSigBits); + m_mediaDrm = new CJNIMediaDrm(uuid); + if (xbmc_jnienv()->ExceptionCheck() || !*m_mediaDrm) + { + LOG::LogF(LOGERROR, "Unable to initialize MediaDrm"); + xbmc_jnienv()->ExceptionClear(); + delete m_mediaDrm, m_mediaDrm = nullptr; + return; + } + + m_mediaDrm->setOnEventListener(*listener); + if (xbmc_jnienv()->ExceptionCheck()) + { + LOG::LogF(LOGERROR, "Exception during installation of EventListener"); + xbmc_jnienv()->ExceptionClear(); + m_mediaDrm->release(); + delete m_mediaDrm, m_mediaDrm = nullptr; + return; + } + + std::vector strDeviceId = m_mediaDrm->getPropertyByteArray("deviceUniqueId"); + xbmc_jnienv()->ExceptionClear(); + std::string strSecurityLevel = m_mediaDrm->getPropertyString("securityLevel"); + xbmc_jnienv()->ExceptionClear(); + std::string strSystemId = m_mediaDrm->getPropertyString("systemId"); + xbmc_jnienv()->ExceptionClear(); + + + if (m_keySystem == WIDEVINE) + { + //m_mediaDrm->setPropertyString("sessionSharing", "enable"); + if (serverCert.GetDataSize()) + m_mediaDrm->setPropertyByteArray( + "serviceCertificate", + std::vector(serverCert.GetData(), serverCert.GetData() + serverCert.GetDataSize())); + else + LoadServiceCertificate(); + + if (xbmc_jnienv()->ExceptionCheck()) + { + LOG::LogF(LOGERROR, "Exception setting Service Certificate"); + xbmc_jnienv()->ExceptionClear(); + m_mediaDrm->release(); + delete m_mediaDrm, m_mediaDrm = nullptr; + return; + } + } + + LOG::Log(LOGDEBUG, + "MediaDrm initialized (Device unique ID size: %ld, System ID: %s, Security level: %s)", + strDeviceId.size(), strSystemId.c_str(), strSecurityLevel.c_str()); + + if (m_licenseUrl.find('|') == std::string::npos) + { + if (m_keySystem == WIDEVINE) + m_licenseUrl += "|Content-Type=application%2Foctet-stream|R{SSM}|"; + else if (m_keySystem == PLAYREADY) + m_licenseUrl += "|Content-Type=text%2Fxml&SOAPAction=http%3A%2F%2Fschemas.microsoft.com%" + "2FDRM%2F2007%2F03%2Fprotocols%2FAcquireLicense|R{SSM}|"; + else + m_licenseUrl += "|Content-Type=application/json|R{SSM}|"; + } +} + +CWVCdmAdapterA::~CWVCdmAdapterA() +{ + if (m_mediaDrm) + { + m_mediaDrm->release(); + if (xbmc_jnienv()->ExceptionCheck()) + { + LOG::LogF(LOGERROR, "Exception releasing media drm"); + xbmc_jnienv()->ExceptionClear(); + } + delete m_mediaDrm; + m_mediaDrm = nullptr; + } +} + +void CWVCdmAdapterA::LoadServiceCertificate() +{ + std::string filename = m_strBasePath + "service_certificate"; + char* data(nullptr); + size_t sz(0); + FILE* f = fopen(filename.c_str(), "rb"); + + if (f) + { + fseek(f, 0L, SEEK_END); + sz = ftell(f); + fseek(f, 0L, SEEK_SET); + if (sz > 8 && (data = (char*)malloc(sz))) + fread(data, 1, sz, f); + fclose(f); + } + if (data) + { + auto now = std::chrono::system_clock::now(); + uint64_t certTime = *((uint64_t*)data), + nowTime = + std::chrono::time_point_cast(now).time_since_epoch().count(); + + if (certTime < nowTime && nowTime - certTime < 86400) + m_mediaDrm->setPropertyByteArray("serviceCertificate", + std::vector(data + 8, data + sz)); + else + free(data), data = nullptr; + } + if (!data) + { + LOG::Log(LOGDEBUG, "Requesting new Service Certificate"); + m_mediaDrm->setPropertyString("privacyMode", "enable"); + } + else + { + LOG::Log(LOGDEBUG, "Use stored Service Certificate"); + free(data), data = nullptr; + } +} + +void CWVCdmAdapterA::SaveServiceCertificate() +{ + std::vector sc = m_mediaDrm->getPropertyByteArray("serviceCertificate"); + if (xbmc_jnienv()->ExceptionCheck()) + { + LOG::LogF(LOGWARNING, "Exception retrieving Service Certificate"); + xbmc_jnienv()->ExceptionClear(); + return; + } + + if (sc.empty()) + { + LOG::LogF(LOGWARNING, "Empty Service Certificate"); + return; + } + + std::string filename = m_strBasePath + "service_certificate"; + FILE* f = fopen(filename.c_str(), "wb"); + if (f) + { + auto now = std::chrono::system_clock::now(); + uint64_t nowTime = + std::chrono::time_point_cast(now).time_since_epoch().count(); + fwrite((char*)&nowTime, 1, sizeof(uint64_t), f); + fwrite(sc.data(), 1, sc.size(), f); + fclose(f); + } +} diff --git a/src/decrypters/widevineandroid/WVCdmAdapter.h b/src/decrypters/widevineandroid/WVCdmAdapter.h new file mode 100644 index 000000000..110720cdf --- /dev/null +++ b/src/decrypters/widevineandroid/WVCdmAdapter.h @@ -0,0 +1,62 @@ +/* + * 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 + +enum WV_KEYSYSTEM +{ + NONE, + WIDEVINE, + PLAYREADY, + WISEPLAY +}; + +class CWVDecrypterA; + +class ATTR_DLL_LOCAL CWVCdmAdapterA +{ +public: + CWVCdmAdapterA(WV_KEYSYSTEM ks, + const char* licenseURL, + const AP4_DataBuffer& serverCert, + jni::CJNIMediaDrmOnEventListener* listener, + CWVDecrypterA* host); + ~CWVCdmAdapterA(); + + jni::CJNIMediaDrm* GetMediaDrm() { return m_mediaDrm; }; + + const std::string& GetLicenseURL() const { return m_licenseUrl; }; + + const uint8_t* GetKeySystem() const + { + static const uint8_t keysystemId[3][16] = { + {0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, 0x4a, 0xce, 0xa3, 0xc8, 0x27, 0xdc, 0xd5, 0x1d, 0x21, + 0xed}, + {0x9A, 0x04, 0xF0, 0x79, 0x98, 0x40, 0x42, 0x86, 0xAB, 0x92, 0xE6, 0x5B, 0xE0, 0x88, 0x5F, + 0x95}, + {0x3d, 0x5e, 0x6d, 0x35, 0x9b, 0x9a, 0x41, 0xe8, 0xb8, 0x43, 0xdd, 0x3c, 0x6e, 0x72, 0xc4, + 0x2c}, + }; + return keysystemId[m_keySystem - 1]; + } + WV_KEYSYSTEM GetKeySystemType() const { return m_keySystem; }; + void SaveServiceCertificate(); + +private: + void LoadServiceCertificate(); + + WV_KEYSYSTEM m_keySystem; + jni::CJNIMediaDrm* m_mediaDrm; + std::string m_licenseUrl; + std::string m_strBasePath; + CWVDecrypterA* m_host; +}; diff --git a/src/decrypters/widevineandroid/WVCencSingleSampleDecrypter.cpp b/src/decrypters/widevineandroid/WVCencSingleSampleDecrypter.cpp new file mode 100644 index 000000000..cadeb90dc --- /dev/null +++ b/src/decrypters/widevineandroid/WVCencSingleSampleDecrypter.cpp @@ -0,0 +1,889 @@ +/* + * 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 "WVCencSingleSampleDecrypter.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 "WVCdmAdapter.h" +#include "WVDecrypter.h" +#include "jsmn.h" + +#include + +using namespace UTILS; +using namespace kodi::tools; + +CWVCencSingleSampleDecrypterA::CWVCencSingleSampleDecrypterA(CWVCdmAdapterA& drm, + AP4_DataBuffer& pssh, + const char* optionalKeyParameter, + std::string_view defaultKeyId, + CWVDecrypterA* host) + : m_mediaDrm(drm), + m_isProvisioningRequested(false), + m_isKeyUpdateRequested(false), + m_hdcpLimit(0), + m_resolutionLimit(0), + m_defaultKeyId{defaultKeyId}, + m_host{host} +{ + SetParentIsOwner(false); + + if (pssh.GetDataSize() > 65535) + { + LOG::LogF(LOGERROR, "PSSH init data with length %u seems not to be cenc init data", + pssh.GetDataSize()); + return; + } + + if (m_host->IsDebugSaveLicense()) + { + std::string debugFilePath = + FILESYS::PathCombine(m_host->GetProfilePath(), "EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED.init"); + std::string data{reinterpret_cast(pssh.GetData()), pssh.GetDataSize()}; + FILESYS::SaveFile(debugFilePath, data, true); + } + + m_pssh.assign(pssh.GetData(), pssh.GetData() + pssh.GetDataSize()); + + if (memcmp(pssh.GetData() + 4, "pssh", 4) != 0) + { + const uint8_t atomHeader[] = {0x00, 0x00, 0x00, 0x00, 0x70, 0x73, + 0x73, 0x68, 0x00, 0x00, 0x00, 0x00}; + uint8_t atom[32]; + memcpy(atom, atomHeader, 12); + memcpy(atom + 12, m_mediaDrm.GetKeySystem(), 16); + memset(atom + 28, 0, 4); + + m_pssh.insert(m_pssh.begin(), reinterpret_cast(atom), + reinterpret_cast(atom + sizeof(atom))); + + m_pssh[3] = static_cast(m_pssh.size()); + m_pssh[2] = static_cast(m_pssh.size() >> 8); + + m_pssh[sizeof(atom) - 1] = static_cast(m_pssh.size()) - sizeof(atom); + m_pssh[sizeof(atom) - 2] = static_cast((m_pssh.size() - sizeof(atom)) >> 8); + } + m_initialPssh = m_pssh; + + if (optionalKeyParameter) + m_optParams["PRCustomData"] = optionalKeyParameter; + + /* + std::vector pui = m_mediaDrm.GetMediaDrm()->getPropertyByteArray("provisioningUniqueId"); + xbmc_jnienv()->ExceptionClear(); + if (pui.size() > 0) + { + std::string encoded{BASE64::Encode(pui.data(), pui.size())}; + m_optParams["CDMID"] = encoded; + } + */ + + bool L3FallbackRequested = false; +RETRY_OPEN: + m_sessionId = m_mediaDrm.GetMediaDrm()->openSession(); + if (xbmc_jnienv()->ExceptionCheck()) + { + xbmc_jnienv()->ExceptionClear(); + if (!m_isProvisioningRequested) + { + LOG::LogF(LOGWARNING, "Exception during open session - provisioning..."); + m_isProvisioningRequested = true; + if (!ProvisionRequest()) + { + if (!L3FallbackRequested && + m_mediaDrm.GetMediaDrm()->getPropertyString("securityLevel") == "L1") + { + LOG::LogF(LOGWARNING, "L1 provisioning failed - retrying with L3..."); + L3FallbackRequested = true; + m_isProvisioningRequested = false; + m_mediaDrm.GetMediaDrm()->setPropertyString("securityLevel", "L3"); + goto RETRY_OPEN; + } + else + return; + } + goto RETRY_OPEN; + } + else + { + LOG::LogF(LOGERROR, "Exception during open session - abort"); + return; + } + } + + if (m_sessionId.size() == 0) + { + LOG::LogF(LOGERROR, "Unable to open DRM session"); + return; + } + + memcpy(m_sessionIdChar, m_sessionId.data(), m_sessionId.size()); + m_sessionIdChar[m_sessionId.size()] = 0; + + if (m_mediaDrm.GetKeySystemType() != PLAYREADY) + { + int maxSecuritylevel = m_mediaDrm.GetMediaDrm()->getMaxSecurityLevel(); + xbmc_jnienv()->ExceptionClear(); + + LOG::Log(LOGDEBUG, "Session ID: %s, Max security level: %d", m_sessionIdChar, maxSecuritylevel); + } +} + +CWVCencSingleSampleDecrypterA::~CWVCencSingleSampleDecrypterA() +{ + if (!m_sessionId.empty()) + { + m_mediaDrm.GetMediaDrm()->removeKeys(m_sessionId); + if (xbmc_jnienv()->ExceptionCheck()) + { + LOG::LogF(LOGERROR, "removeKeys has raised an exception"); + xbmc_jnienv()->ExceptionClear(); + } + m_mediaDrm.GetMediaDrm()->closeSession(m_sessionId); + if (xbmc_jnienv()->ExceptionCheck()) + { + LOG::LogF(LOGERROR, "closeSession has raised an exception"); + xbmc_jnienv()->ExceptionClear(); + } + } +} + +const char* CWVCencSingleSampleDecrypterA::GetSessionId() +{ + return m_sessionIdChar; +} + +std::vector CWVCencSingleSampleDecrypterA::GetChallengeData() +{ + return m_keyRequestData; +} + +bool CWVCencSingleSampleDecrypterA::HasLicenseKey(const uint8_t* keyId) +{ + // true = one session for all streams, false = one sessions per stream + // false fixes pixaltion issues on some devices when manifest has multiple encrypted streams + return true; +} + +void CWVCencSingleSampleDecrypterA::GetCapabilities(const uint8_t* keyId, + uint32_t media, + IDecrypter::DecrypterCapabilites& caps) +{ + caps = {IDecrypter::DecrypterCapabilites::SSD_SECURE_PATH | + IDecrypter::DecrypterCapabilites::SSD_ANNEXB_REQUIRED, + 0, m_hdcpLimit}; + + if (caps.hdcpLimit == 0) + caps.hdcpLimit = m_resolutionLimit; + + if (m_mediaDrm.GetMediaDrm()->getPropertyString("securityLevel") == "L1") + { + caps.hdcpLimit = m_resolutionLimit; //No restriction + caps.flags |= IDecrypter::DecrypterCapabilites::SSD_SECURE_DECODER; + } + LOG::LogF(LOGDEBUG, "hdcpLimit: %i", caps.hdcpLimit); + + caps.hdcpVersion = 99; +} + +bool CWVCencSingleSampleDecrypterA::ProvisionRequest() +{ + LOG::Log(LOGWARNING, "Provision data request (DRM:%p)", m_mediaDrm.GetMediaDrm()); + + CJNIMediaDrmProvisionRequest request = m_mediaDrm.GetMediaDrm()->getProvisionRequest(); + if (xbmc_jnienv()->ExceptionCheck()) + { + LOG::LogF(LOGERROR, "getProvisionRequest has raised an exception"); + xbmc_jnienv()->ExceptionClear(); + return false; + } + + std::vector provData = request.getData(); + std::string url = request.getDefaultUrl(); + + LOG::Log(LOGDEBUG, "Provision data size: %lu, url: %s", provData.size(), url.c_str()); + + std::string reqData("{\"signedRequest\":\""); + reqData += std::string(provData.data(), provData.size()); + reqData += "\"}"; + reqData = BASE64::Encode(reqData); + + CURL::CUrl file(url); + file.AddHeader("Content-Type", "application/json"); + file.AddHeader("postdata", reqData.c_str()); + + if (!file.Open()) + { + LOG::Log(LOGERROR, "Provisioning server returned failure"); + return false; + } + provData.clear(); + + // read the file + std::string response; + CURL::ReadStatus downloadStatus = CURL::ReadStatus::CHUNK_READ; + while (downloadStatus == CURL::ReadStatus::CHUNK_READ) + { + downloadStatus = file.Read(response); + } + std::copy(response.begin(), response.end(), std::back_inserter(provData)); + + m_mediaDrm.GetMediaDrm()->provideProvisionResponse(provData); + if (xbmc_jnienv()->ExceptionCheck()) + { + LOG::LogF(LOGERROR, "provideProvisionResponse has raised an exception"); + xbmc_jnienv()->ExceptionClear(); + return false; + } + return true; +} + +bool CWVCencSingleSampleDecrypterA::GetKeyRequest(std::vector& keyRequestData) +{ + CJNIMediaDrmKeyRequest keyRequest = m_mediaDrm.GetMediaDrm()->getKeyRequest( + m_sessionId, m_pssh, "video/mp4", CJNIMediaDrm::KEY_TYPE_STREAMING, m_optParams); + + if (xbmc_jnienv()->ExceptionCheck()) + { + xbmc_jnienv()->ExceptionClear(); + if (!m_isProvisioningRequested) + { + LOG::Log(LOGWARNING, "Key request not successful - trying provisioning"); + m_isProvisioningRequested = true; + return GetKeyRequest(keyRequestData); + } + else + LOG::LogF(LOGERROR, "Key request not successful"); + return false; + } + + keyRequestData = keyRequest.getData(); + LOG::Log(LOGDEBUG, "Key request successful size: %lu", keyRequestData.size()); + return true; +} + +bool CWVCencSingleSampleDecrypterA::KeyUpdateRequest(bool waitKeys, bool skipSessionMessage) +{ + if (!GetKeyRequest(m_keyRequestData)) + return false; + + m_pssh.clear(); + m_optParams.clear(); + + if (skipSessionMessage) + return true; + + m_isKeyUpdateRequested = false; + if (!SendSessionMessage(m_keyRequestData)) + return false; + + if (waitKeys && m_keyRequestData.size() == 2) // Service Certificate call + { + for (unsigned int i(0); i < 100 && !m_isKeyUpdateRequested; ++i) + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + if (m_isKeyUpdateRequested) + KeyUpdateRequest(false, false); + else + { + LOG::LogF(LOGERROR, "Timeout waiting for EVENT_KEYS_REQUIRED!"); + return false; + } + } + + if (m_mediaDrm.GetKeySystemType() != PLAYREADY) + { + int securityLevel = m_mediaDrm.GetMediaDrm()->getSecurityLevel(m_sessionId); + xbmc_jnienv()->ExceptionClear(); + LOG::Log(LOGDEBUG, "Security level: %d", securityLevel); + + std::map keyStatus = + m_mediaDrm.GetMediaDrm()->queryKeyStatus(m_sessionId); + LOG::Log(LOGDEBUG, "Key status (%ld):", keyStatus.size()); + for (auto const& ks : keyStatus) + { + LOG::Log(LOGDEBUG, "-> %s -> %s", ks.first.c_str(), ks.second.c_str()); + } + } + return true; +} + +bool CWVCencSingleSampleDecrypterA::SendSessionMessage(const std::vector& keyRequestData) +{ + std::vector blocks{StringUtils::Split(m_mediaDrm.GetLicenseURL(), '|')}; + + 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(), "EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED.challenge"); + UTILS::FILESYS::SaveFile(debugFilePath, keyRequestData.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(keyRequestData.data(), keyRequestData.size())}; + 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(keyRequestData.data(), static_cast(keyRequestData.size())); + md5.Finalize(); + blocks[0].replace(insPos, 6, md5.HexDigest()); + } + + CURL::CUrl file(blocks[0]); + std::string response; + std::string resLimit; + std::string contentType; + + //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], value); + } + } + + //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}")); + std::string::size_type psshPos(blocks[2].find("{PSSH}")); + + 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; + if (psshPos != std::string::npos) + psshPos -= 2; + } + + size_t sizeWritten(0); + + if (insPos > 0) + { + if (blocks[2][insPos - 1] == 'B' || blocks[2][insPos - 1] == 'b') + { + std::string msgEncoded{BASE64::Encode(keyRequestData.data(), keyRequestData.size())}; + if (blocks[2][insPos - 1] == 'B') + { + msgEncoded = STRING::URLEncode(msgEncoded); + } + blocks[2].replace(insPos - 1, 6, msgEncoded); + sizeWritten = msgEncoded.size(); + } + else if (blocks[2][insPos - 1] == 'D') + { + std::string msgEncoded{STRING::ToDecimal( + reinterpret_cast(keyRequestData.data()), keyRequestData.size())}; + blocks[2].replace(insPos - 1, 6, msgEncoded); + sizeWritten = msgEncoded.size(); + } + else + { + blocks[2].replace(insPos - 1, 6, keyRequestData.data(), keyRequestData.size()); + sizeWritten = keyRequestData.size(); + } + } + else + { + LOG::Log(LOGERROR, "Unsupported License request template (body / ?{SSM})"); + return false; + } + + if (sidPos != std::string::npos && insPos < sidPos) + sidPos += sizeWritten, sidPos -= 6; + + if (kidPos != std::string::npos && insPos < kidPos) + kidPos += sizeWritten, kidPos -= 6; + + if (psshPos != std::string::npos && insPos < psshPos) + psshPos += sizeWritten, psshPos -= 6; + + sizeWritten = 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_sessionId.data(), m_sessionId.size())}; + if (blocks[2][sidPos - 1] == 'B') + { + msgEncoded = STRING::URLEncode(msgEncoded); + } + blocks[2].replace(sidPos - 1, 6, msgEncoded); + sizeWritten = msgEncoded.size(); + } + else + { + blocks[2].replace(sidPos - 1, 6, m_sessionId.data(), m_sessionId.size()); + sizeWritten = m_sessionId.size(); + } + } + else + { + LOG::Log(LOGERROR, "Unsupported License request template (body / ?{SID})"); + return false; + } + } + + if (kidPos != std::string::npos && sidPos < kidPos) + kidPos += sizeWritten, kidPos -= 6; + + if (psshPos != std::string::npos && sidPos < psshPos) + psshPos += sizeWritten, psshPos -= 6; + + size_t kidPlaceholderLen = 6; + if (kidPos != std::string::npos) + { + 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); + kidPlaceholderLen = 5; + } + } + + if (psshPos != std::string::npos && kidPos < psshPos) + psshPos += sizeWritten, psshPos -= kidPlaceholderLen; + + if (psshPos != std::string::npos) + { + std::string msgEncoded{BASE64::Encode(m_initialPssh.data(), m_initialPssh.size())}; + if (blocks[2][psshPos - 1] == 'B') + { + msgEncoded = STRING::URLEncode(msgEncoded); + } + blocks[2].replace(psshPos - 1, 7, msgEncoded); + sizeWritten = msgEncoded.size(); + } + + if (fullDecode) + { + std::string msgEncoded{BASE64::Encode(blocks[2])}; + if (fullDecode == 'B') + { + msgEncoded = STRING::URLEncode(msgEncoded); + } + blocks[2] = msgEncoded; + } + + if (m_host->IsDebugSaveLicense()) + { + std::string debugFilePath = FILESYS::PathCombine( + m_host->GetProfilePath(), "EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED.postdata"); + UTILS::FILESYS::SaveFile(debugFilePath, blocks[2], true); + } + } + + std::string encData{BASE64::Encode(blocks[2])}; + file.AddHeader("postdata", encData.c_str()); + } + + 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="); + 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; + } + else if (response.empty()) + { + LOG::LogF(LOGERROR, "Empty SessionMessage response - invalid"); + return false; + } + + if (m_mediaDrm.GetKeySystemType() == PLAYREADY && + response.find("") == std::string::npos) + { + std::string::size_type dstPos(response.find("")); + std::string challenge(keyRequestData.data(), keyRequestData.size()); + std::string::size_type srcPosS(challenge.find("")); + if (dstPos != std::string::npos && srcPosS != std::string::npos) + { + LOG::Log(LOGDEBUG, "Inserting "); + std::string::size_type srcPosE(challenge.find("", srcPosS)); + if (srcPosE != std::string::npos) + response.insert(dstPos + 11, challenge.c_str() + srcPosS, srcPosE - srcPosS + 15); + } + } + + if (m_host->IsDebugSaveLicense()) + { + std::string debugFilePath = FILESYS::PathCombine( + m_host->GetProfilePath(), "EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED.response"); + UTILS::FILESYS::SaveFile(debugFilePath, response, true); + } + + if (!blocks[3].empty() && (keyRequestData.size() > 2 || + contentType.find("application/octet-stream") == std::string::npos)) + { + if (blocks[3][0] == 'J' || (blocks[3].size() > 1 && blocks[3][0] == 'B' && blocks[3][1] == 'J')) + { + int dataPos = 2; + + if (response.size() >= 3 && blocks[3][0] == 'B') + { + response = BASE64::Decode(response); + dataPos = 3; + } + + jsmn_parser jsn; + jsmntok_t tokens[256]; + + jsmn_init(&jsn); + int i(0), numTokens = jsmn_parse(&jsn, response.c_str(), response.size(), tokens, 256); + + std::vector jsonVals{StringUtils::Split(blocks[3].substr(dataPos), ';')}; + + // Find HDCP limit + if (jsonVals.size() > 1) + { + for (; i < numTokens; ++i) + if (tokens[i].type == JSMN_STRING && tokens[i].size == 1 && + jsonVals[1].size() == static_cast(tokens[i].end - tokens[i].start) && + strncmp(response.c_str() + tokens[i].start, jsonVals[1].c_str(), + tokens[i].end - tokens[i].start) == 0) + break; + if (i < numTokens) + m_hdcpLimit = atoi((response.c_str() + tokens[i + 1].start)); + } + // Find license key + if (jsonVals.size() > 0) + { + for (i = 0; i < numTokens; ++i) + if (tokens[i].type == JSMN_STRING && tokens[i].size == 1 && + jsonVals[0].size() == static_cast(tokens[i].end - tokens[i].start) && + strncmp(response.c_str() + tokens[i].start, jsonVals[0].c_str(), + tokens[i].end - tokens[i].start) == 0) + { + if (i + 1 < numTokens && tokens[i + 1].type == JSMN_ARRAY && tokens[i + 1].size == 1) + ++i; + break; + } + } + else + i = numTokens; + + if (i < numTokens) + { + response = response.substr(tokens[i + 1].start, tokens[i + 1].end - tokens[i + 1].start); + + if (blocks[3][dataPos - 1] == 'B') + { + response = BASE64::Decode(response); + } + } + else + { + LOG::LogF(LOGERROR, "Unable to find %s in JSON string", blocks[3].c_str() + 2); + return false; + } + } + else if (blocks[3][0] == 'H' && blocks[3].size() >= 2) + { + //Find the payload + std::string::size_type payloadPos = response.find("\r\n\r\n"); + if (payloadPos != std::string::npos) + { + payloadPos += 4; + if (blocks[3][1] == 'B') + response = std::string(response.c_str() + payloadPos, response.c_str() + response.size()); + else + { + LOG::LogF(LOGERROR, "Unsupported HTTP payload data type definition"); + return false; + } + } + else + { + LOG::LogF(LOGERROR, "Unable to find HTTP payload in response"); + return false; + } + } + else if (blocks[3][0] == 'B' && blocks[3].size() == 1) + { + response = BASE64::Decode(response); + } + else + { + LOG::LogF(LOGERROR, "Unsupported License request template (response)"); + return false; + } + } + + m_keySetId = m_mediaDrm.GetMediaDrm()->provideKeyResponse( + m_sessionId, std::vector(response.data(), response.data() + response.size())); + if (xbmc_jnienv()->ExceptionCheck()) + { + LOG::LogF(LOGERROR, "provideKeyResponse has raised an exception"); + xbmc_jnienv()->ExceptionClear(); + return false; + } + + if (keyRequestData.size() == 2) + m_mediaDrm.SaveServiceCertificate(); + + LOG::Log(LOGDEBUG, "License update successful"); + return true; +} + +AP4_Result CWVCencSingleSampleDecrypterA::SetFragmentInfo(AP4_UI32 poolId, + const AP4_UI08* key, + 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 = key; + m_fragmentPool[poolId].m_nalLengthSize = nalLengthSize; + m_fragmentPool[poolId].m_annexbSpsPps.SetData(annexbSpsPps.GetData(), annexbSpsPps.GetDataSize()); + m_fragmentPool[poolId].m_decrypterFlags = flags; + + if (m_isKeyUpdateRequested) + KeyUpdateRequest(false, false); + + return AP4_SUCCESS; +} + +AP4_UI32 CWVCencSingleSampleDecrypterA::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 CWVCencSingleSampleDecrypterA::RemovePool(AP4_UI32 poolId) +{ + m_fragmentPool[poolId].m_nalLengthSize = 99; + m_fragmentPool[poolId].m_key = nullptr; +} + +AP4_Result CWVCencSingleSampleDecrypterA::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) +{ + if (!m_mediaDrm.GetMediaDrm()) + return AP4_ERROR_INVALID_STATE; + + if (dataIn.GetDataSize() > 0) + { + FINFO& fragInfo(m_fragmentPool[poolId]); + + if (fragInfo.m_nalLengthSize > 4) + { + LOG::LogF(LOGERROR, "Nalu length size > 4 not supported"); + return AP4_ERROR_NOT_SUPPORTED; + } + + AP4_UI16 dummyClear = 0; + AP4_UI32 dummyCipher(dataIn.GetDataSize()); + + if (iv) + { + if (!subsampleCount) + { + subsampleCount = 1; + bytesOfCleartextData = &dummyClear; + bytesOfEncryptedData = &dummyCipher; + } + + dataOut.SetData(reinterpret_cast(&subsampleCount), sizeof(subsampleCount)); + dataOut.AppendData(reinterpret_cast(bytesOfCleartextData), + subsampleCount * sizeof(AP4_UI16)); + dataOut.AppendData(reinterpret_cast(bytesOfEncryptedData), + subsampleCount * sizeof(AP4_UI32)); + dataOut.AppendData(reinterpret_cast(iv), 16); + dataOut.AppendData(reinterpret_cast(fragInfo.m_key), 16); + } + else + { + dataOut.SetDataSize(0); + bytesOfCleartextData = &dummyClear; + bytesOfEncryptedData = &dummyCipher; + } + + if (fragInfo.m_nalLengthSize && (!iv || bytesOfCleartextData[0] > 0)) + { + //check NAL / subsample + const AP4_Byte* packetIn(dataIn.GetData()); + const AP4_Byte* packetInEnd(dataIn.GetData() + dataIn.GetDataSize()); + AP4_UI16* clrb_out( + iv ? reinterpret_cast(dataOut.UseData() + sizeof(subsampleCount)) + : nullptr); //! @todo: what is the point of this? + size_t nalUnitCount = 0; //! @todo: what is the point of this? + size_t nalUnitSum = 0; + size_t configSize = 0; //! @todo:what is the point of this? + + while (packetIn < packetInEnd) + { + uint32_t nalsize = 0; + for (size_t i = 0; i < fragInfo.m_nalLengthSize; ++i) + { + nalsize = (nalsize << 8) + *packetIn++; + }; + + //look if we have to inject sps / pps + if (fragInfo.m_annexbSpsPps.GetDataSize() && (*packetIn & 0x1F) != 9 /*AVC_NAL_AUD*/) + { + dataOut.AppendData(fragInfo.m_annexbSpsPps.GetData(), + fragInfo.m_annexbSpsPps.GetDataSize()); + if (clrb_out) + *clrb_out += fragInfo.m_annexbSpsPps.GetDataSize(); + configSize = fragInfo.m_annexbSpsPps.GetDataSize(); + fragInfo.m_annexbSpsPps.SetDataSize(0); + } + + //Annex-B Start pos + static AP4_Byte annexbStartCode[4] = {0x00, 0x00, 0x00, 0x01}; + dataOut.AppendData(annexbStartCode, 4); + dataOut.AppendData(packetIn, nalsize); + packetIn += nalsize; + if (clrb_out) + *clrb_out += (4 - fragInfo.m_nalLengthSize); + ++nalUnitCount; + + if (!iv) + { + nalUnitSum = 0; + } + else if (nalsize + fragInfo.m_nalLengthSize + nalUnitSum >= + *bytesOfCleartextData + *bytesOfEncryptedData) + { + AP4_UI32 summedBytes = 0; + do + { + summedBytes += *bytesOfCleartextData + *bytesOfEncryptedData; + ++bytesOfCleartextData; + ++bytesOfEncryptedData; + ++clrb_out; + --subsampleCount; + } while (subsampleCount && nalsize + fragInfo.m_nalLengthSize + nalUnitSum > summedBytes); + + if (nalsize + fragInfo.m_nalLengthSize + nalUnitSum > summedBytes) + { + LOG::LogF(LOGERROR, "NAL Unit exceeds subsample definition (nls: %u) %u -> %u ", + static_cast(fragInfo.m_nalLengthSize), + static_cast(nalsize + fragInfo.m_nalLengthSize + nalUnitSum), + summedBytes); + return AP4_ERROR_NOT_SUPPORTED; + } + nalUnitSum = 0; + } + else + nalUnitSum += nalsize + fragInfo.m_nalLengthSize; + } + if (packetIn != packetInEnd || subsampleCount) + { + LOG::LogF(LOGERROR, "NAL Unit definition incomplete (nls: %d) %d -> %u ", + fragInfo.m_nalLengthSize, (int)(packetInEnd - packetIn), subsampleCount); + return AP4_ERROR_NOT_SUPPORTED; + } + } + else + { + dataOut.AppendData(dataIn.GetData(), dataIn.GetDataSize()); + fragInfo.m_annexbSpsPps.SetDataSize(0); + } + } + else + dataOut.SetDataSize(0); + return AP4_SUCCESS; +} diff --git a/src/decrypters/widevineandroid/WVCencSingleSampleDecrypter.h b/src/decrypters/widevineandroid/WVCencSingleSampleDecrypter.h new file mode 100644 index 000000000..f2f994d36 --- /dev/null +++ b/src/decrypters/widevineandroid/WVCencSingleSampleDecrypter.h @@ -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. + */ + +#pragma once +#include "../../common/AdaptiveCencSampleDecrypter.h" +#include "../IDecrypter.h" + +#include + +#include + +class CWVCdmAdapterA; +class CWVDecrypterA; + +class ATTR_DLL_LOCAL CWVCencSingleSampleDecrypterA : public Adaptive_CencSingleSampleDecrypter +{ +public: + // methods + CWVCencSingleSampleDecrypterA(CWVCdmAdapterA& drm, + AP4_DataBuffer& pssh, + const char* optionalKeyParameter, + std::string_view defaultKeyId, + CWVDecrypterA* host); + virtual ~CWVCencSingleSampleDecrypterA(); + + bool StartSession(bool skipSessionMessage) { return KeyUpdateRequest(true, skipSessionMessage); }; + const std::vector& GetSessionIdRaw() { return m_sessionId; }; + virtual const char* GetSessionId() override; + std::vector GetChallengeData(); + virtual bool HasLicenseKey(const uint8_t* keyId); + + virtual AP4_Result SetFragmentInfo(AP4_UI32 poolId, + const AP4_UI08* key, + 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; + + void GetCapabilities(const uint8_t* keyId, + uint32_t media, + DRM::IDecrypter::DecrypterCapabilites& caps); + + void RequestNewKeys() { m_isKeyUpdateRequested = true; }; + +private: + bool ProvisionRequest(); + bool GetKeyRequest(std::vector& keyRequestData); + bool KeyUpdateRequest(bool waitForKeys, bool skipSessionMessage); + bool SendSessionMessage(const std::vector& keyRequestData); + + CWVCdmAdapterA& m_mediaDrm; + std::vector m_pssh; + std::vector m_initialPssh; + std::map m_optParams; + CWVDecrypterA* m_host; + + std::vector m_sessionId; + std::vector m_keySetId; + std::vector m_keyRequestData; + + char m_sessionIdChar[128]; + bool m_isProvisioningRequested; + bool m_isKeyUpdateRequested; + + std::string m_defaultKeyId; + + struct FINFO + { + const AP4_UI08* m_key; + AP4_UI08 m_nalLengthSize; + AP4_UI16 m_decrypterFlags; + AP4_DataBuffer m_annexbSpsPps; + }; + std::vector m_fragmentPool; + int m_hdcpLimit; + int m_resolutionLimit; +}; diff --git a/src/decrypters/widevineandroid/WVDecrypter.cpp b/src/decrypters/widevineandroid/WVDecrypter.cpp new file mode 100644 index 000000000..497b15314 --- /dev/null +++ b/src/decrypters/widevineandroid/WVDecrypter.cpp @@ -0,0 +1,293 @@ +/* + * 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 "WVDecrypter.h" + +#include "../../common/AdaptiveDecrypter.h" +#include "../../utils/Base64Utils.h" +#include "../../utils/DigestMD5Utils.h" +#include "../../utils/StringUtils.h" +#include "../../utils/Utils.h" +#include "WVCencSingleSampleDecrypter.h" +#include "jsmn.h" +#include "kodi/tools/StringUtils.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace DRM; +using namespace UTILS; +using namespace kodi::tools; + +namespace +{ +kodi::platform::CInterfaceAndroidSystem* ANDROID_SYSTEM{nullptr}; +} // unnamed namespace + +CMediaDrmOnEventListener::CMediaDrmOnEventListener(CMediaDrmOnEventCallback* decrypterEventCallback, + CJNIClassLoader* classLoader) + : CJNIMediaDrmOnEventListener(classLoader), m_decrypterEventCallback(decrypterEventCallback) +{ +} + +void CMediaDrmOnEventListener::onEvent(const CJNIMediaDrm& mediaDrm, + const std::vector& sessionId, + int event, + int extra, + const std::vector& data) +{ + m_decrypterEventCallback->OnMediaDrmEvent(mediaDrm, sessionId, event, extra, data); +} + +CWVDecrypterA::CWVDecrypterA() : m_keySystem(NONE), m_WVCdmAdapter(nullptr) +{ + // CInterfaceAndroidSystem need to be initialized at runtime + // then we have to set it to global variable just now + ANDROID_SYSTEM = &m_androidSystem; +}; + +CWVDecrypterA::~CWVDecrypterA() +{ + delete m_WVCdmAdapter; + m_WVCdmAdapter = nullptr; + +#ifdef DRMTHREAD + m_jniCondition.notify_one(); + m_jniWorker->join(); + delete m_jniWorker; +#endif +}; + +#ifdef DRMTHREAD +void JNIThread(JavaVM* vm) +{ + m_jniCondition.notify_one(); + std::unique_lock lk(m_jniMutex); + m_jniCondition.wait(lk); + + LOG::Log(SSDDEBUG, "JNI thread terminated"); +} +#endif + +void CWVDecrypterA::SetProfilePath(const std::string& profilePath) +{ + m_strProfilePath = profilePath; + + const char* pathSep{profilePath[0] && profilePath[1] == ':' && isalpha(profilePath[0]) ? "\\" + : "/"}; + + if (m_strProfilePath.size() && 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.c_str()); + m_strProfilePath += "cdm"; + m_strProfilePath += pathSep; + kodi::vfs::CreateDirectory(m_strProfilePath.c_str()); +} + +const char* CWVDecrypterA::SelectKeySytem(const char* keySystem) +{ + LOG::Log(LOGDEBUG, "Key system request: %s", keySystem); + if (strcmp(keySystem, "com.widevine.alpha") == 0) + { + m_keySystem = WIDEVINE; + return "urn:uuid:EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED"; + } + else if (strcmp(keySystem, "com.huawei.wiseplay") == 0) + { + m_keySystem = WISEPLAY; + return "urn:uuid:3D5E6D35-9B9A-41E8-B843-DD3C6E72C42C"; + } + else if (strcmp(keySystem, "com.microsoft.playready") == 0) + { + m_keySystem = PLAYREADY; + return "urn:uuid:9A04F079-9840-4286-AB92-E65BE0885F95"; + } + else + return nullptr; +} + +bool CWVDecrypterA::OpenDRMSystem(const char* licenseURL, + const AP4_DataBuffer& serverCertificate, + const uint8_t config) +{ + if (m_keySystem == NONE) + return false; + + m_WVCdmAdapter = new CWVCdmAdapterA(m_keySystem, licenseURL, serverCertificate, + m_mediaDrmEventListener.get(), this); + + return m_WVCdmAdapter->GetMediaDrm(); +} + +Adaptive_CencSingleSampleDecrypter* CWVDecrypterA::CreateSingleSampleDecrypter( + AP4_DataBuffer& pssh, + const char* optionalKeyParameter, + std::string_view defaultKeyId, + bool skipSessionMessage, + CryptoMode cryptoMode) +{ + CWVCencSingleSampleDecrypterA* decrypter = new CWVCencSingleSampleDecrypterA( + *m_WVCdmAdapter, pssh, optionalKeyParameter, defaultKeyId, this); + + { + std::lock_guard lk(m_decrypterListMutex); + m_decrypterList.push_back(decrypter); + } + + if (!(*decrypter->GetSessionId() && decrypter->StartSession(skipSessionMessage))) + { + DestroySingleSampleDecrypter(decrypter); + return nullptr; + } + return decrypter; +} + +void CWVDecrypterA::DestroySingleSampleDecrypter(Adaptive_CencSingleSampleDecrypter* decrypter) +{ + if (decrypter) + { + std::vector::const_iterator res = + std::find(m_decrypterList.begin(), m_decrypterList.end(), decrypter); + if (res != m_decrypterList.end()) + { + std::lock_guard lk(m_decrypterListMutex); + m_decrypterList.erase(res); + } + delete static_cast(decrypter); + } +} + +void CWVDecrypterA::GetCapabilities(Adaptive_CencSingleSampleDecrypter* decrypter, + const uint8_t* keyId, + uint32_t media, + IDecrypter::DecrypterCapabilites& caps) +{ + if (decrypter) + static_cast(decrypter)->GetCapabilities(keyId, media, caps); + else + caps = {0, 0, 0}; +} + +bool CWVDecrypterA::HasLicenseKey(Adaptive_CencSingleSampleDecrypter* decrypter, + const uint8_t* keyId) +{ + if (decrypter) + return static_cast(decrypter)->HasLicenseKey(keyId); + return false; +} + +std::string CWVDecrypterA::GetChallengeB64Data(Adaptive_CencSingleSampleDecrypter* decrypter) +{ + if (!decrypter) + return ""; + + std::vector challengeData = + static_cast(decrypter)->GetChallengeData(); + return BASE64::Encode(challengeData.data(), challengeData.size()); +} + +void CWVDecrypterA::OnMediaDrmEvent(const CJNIMediaDrm& mediaDrm, + const std::vector& sessionId, + int event, + int extra, + const std::vector& data) +{ + LOG::LogF(LOGDEBUG, "%d arrived, #decrypter: %lu", event, m_decrypterList.size()); + //we have only one DRM system running (m_WVCdmAdapter) so there is no need to compare mediaDrm + std::lock_guard lk(m_decrypterListMutex); + for (std::vector::iterator b(m_decrypterList.begin()), + e(m_decrypterList.end()); + b != e; ++b) + { + if (sessionId.empty() || (*b)->GetSessionIdRaw() == sessionId) + { + switch (event) + { + case CJNIMediaDrm::EVENT_KEY_REQUIRED: + (*b)->RequestNewKeys(); + break; + default:; + } + } + else + { + LOG::LogF(LOGDEBUG, "Session does not match: sizes: %lu -> %lu", sessionId.size(), + (*b)->GetSessionIdRaw().size()); + } + } +} + +bool CWVDecrypterA::Initialize() +{ +#ifdef DRMTHREAD + std::unique_lock lk(m_jniMutex); + m_jniWorker = new std::thread(&CWVDecrypterA::JNIThread, this, + reinterpret_cast(m_androidSystem.GetJNIEnv())); + m_jniCondition.wait(lk); +#endif + if (xbmc_jnienv()->ExceptionCheck()) + { + LOG::LogF(LOGERROR, "Failed to load MediaDrmOnEventListener"); + xbmc_jnienv()->ExceptionDescribe(); + xbmc_jnienv()->ExceptionClear(); + return false; + } + + //JNIEnv* env = static_cast(m_androidSystem.GetJNIEnv()); + CJNIClassLoader* classLoader; + CJNIBase::SetSDKVersion(m_androidSystem.GetSDKVersion()); + CJNIBase::SetBaseClassName(m_androidSystem.GetClassName()); + LOG::Log(LOGDEBUG, "WVDecrypter JNI, SDK version: %d", m_androidSystem.GetSDKVersion()); + + const char* apkEnv = getenv("XBMC_ANDROID_APK"); + if (!apkEnv) + apkEnv = getenv("KODI_ANDROID_APK"); + + if (!apkEnv) + return false; + + std::string apkPath = apkEnv; + + //! @todo: make classLoader a smartpointer + classLoader = new CJNIClassLoader(apkPath); + if (xbmc_jnienv()->ExceptionCheck()) + { + LOG::LogF(LOGERROR, "Failed to create ClassLoader"); + xbmc_jnienv()->ExceptionDescribe(); + xbmc_jnienv()->ExceptionClear(); + + delete classLoader, classLoader = nullptr; + + return false; + } + + m_classLoader = classLoader; + m_mediaDrmEventListener = std::make_unique(this, m_classLoader); + return true; +} + +// Definition for the xbmc_jnienv method of the jni utils (jutils.hpp) +JNIEnv* xbmc_jnienv() +{ + return static_cast(ANDROID_SYSTEM->GetJNIEnv()); +} diff --git a/src/decrypters/widevineandroid/WVDecrypter.h b/src/decrypters/widevineandroid/WVDecrypter.h new file mode 100644 index 000000000..9665ef78a --- /dev/null +++ b/src/decrypters/widevineandroid/WVDecrypter.h @@ -0,0 +1,164 @@ +/* + * 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 "../../utils/Base64Utils.h" +#include "../../utils/log.h" +#include "../IDecrypter.h" +#include "WVCdmAdapter.h" + +#include +#include + +#include +#include +#include + +class CWVCencSingleSampleDecrypterA; + +using namespace UTILS; +using namespace DRM; +using namespace jni; + +class CMediaDrmOnEventCallback +{ +public: + CMediaDrmOnEventCallback() = default; + virtual ~CMediaDrmOnEventCallback() = default; + + virtual void OnMediaDrmEvent(const CJNIMediaDrm& mediaDrm, + const std::vector& sessionId, + int event, + int extra, + const std::vector& data) = 0; +}; + +/*! + * \brief This class is derived from CJNIMediaDrmOnEventListener to allow us + * to initialize the base class at later time, since the constructors + * of CJNIMediaDrmOnEventListener need to access to the global + * xbmc_jnienv method immediately. + */ +class CMediaDrmOnEventListener : public CJNIMediaDrmOnEventListener +{ +public: + CMediaDrmOnEventListener(CMediaDrmOnEventCallback* decrypterEventCallback, + CJNIClassLoader* classLoader); + virtual ~CMediaDrmOnEventListener() = default; + + virtual void onEvent(const CJNIMediaDrm& mediaDrm, + const std::vector& sessionId, + int event, + int extra, + const std::vector& data) override; + +private: + CMediaDrmOnEventCallback* m_decrypterEventCallback; +}; + +class ATTR_DLL_LOCAL CWVDecrypterA : public IDecrypter, public CMediaDrmOnEventCallback +{ +public: + CWVDecrypterA(); + ~CWVDecrypterA(); + + bool Initialize() override; + +#ifdef DRMTHREAD + void JNIThread(JavaVM* vm) + { + m_jniCondition.notify_one(); + std::unique_lock lk(m_jniMutex); + m_jniCondition.wait(lk); + + LOG::Log(SSDDEBUG, "JNI thread terminated"); + } +#endif + + virtual const char* SelectKeySytem(const char* keySystem) override; + + virtual bool OpenDRMSystem(const char* licenseURL, + const AP4_DataBuffer& serverCertificate, + const uint8_t config) override; + + virtual Adaptive_CencSingleSampleDecrypter* CreateSingleSampleDecrypter( + AP4_DataBuffer& pssh, + const char* optionalKeyParameter, + std::string_view defaultKeyId, + bool skipSessionMessage, + CryptoMode cryptoMode) override; + + virtual void DestroySingleSampleDecrypter(Adaptive_CencSingleSampleDecrypter* decrypter) override; + + virtual void GetCapabilities(Adaptive_CencSingleSampleDecrypter* decrypter, + const uint8_t* keyId, + uint32_t media, + IDecrypter::DecrypterCapabilites& caps) override; + + virtual bool HasLicenseKey(Adaptive_CencSingleSampleDecrypter* decrypter, + const uint8_t* keyId) override; + + virtual std::string GetChallengeB64Data(Adaptive_CencSingleSampleDecrypter* decrypter) override; + + virtual bool IsInitialised() override { return m_WVCdmAdapter != nullptr; } + + virtual bool OpenVideoDecoder(Adaptive_CencSingleSampleDecrypter* decrypter, + const VIDEOCODEC_INITDATA* initData) override + { + return false; + } + + virtual VIDEOCODEC_RETVAL DecryptAndDecodeVideo(kodi::addon::CInstanceVideoCodec* codecInstance, + const DEMUX_PACKET* sample) override + { + return VC_ERROR; + } + + virtual VIDEOCODEC_RETVAL VideoFrameDataToPicture(kodi::addon::CInstanceVideoCodec* codecInstance, + VIDEOCODEC_PICTURE* picture) override + { + return VC_ERROR; + } + + virtual void ResetVideo() override {} + + virtual void SetProfilePath(const std::string& profilePath) override; + virtual void SetLibraryPath(const char* libraryPath) override{}; + virtual void SetDebugSaveLicense(bool isDebugSaveLicense) override + { + m_isDebugSaveLicense = isDebugSaveLicense; + } + + virtual const char* GetLibraryPath() const override { return ""; } + virtual const char* GetProfilePath() const override { return m_strProfilePath.c_str(); } + virtual const bool IsDebugSaveLicense() const override { return m_isDebugSaveLicense; } + + virtual void OnMediaDrmEvent(const CJNIMediaDrm& mediaDrm, + const std::vector& sessionId, + int event, + int extra, + const std::vector& data) override; + +private: + kodi::platform::CInterfaceAndroidSystem m_androidSystem; + std::unique_ptr m_mediaDrmEventListener; + WV_KEYSYSTEM m_keySystem; + CWVCdmAdapterA* m_WVCdmAdapter; + inline static CJNIClassLoader* m_classLoader{nullptr}; + std::vector m_decrypterList; + std::mutex m_decrypterListMutex; + std::string m_retvalHelper; + std::string m_strProfilePath; + bool m_isDebugSaveLicense; +#ifdef DRMTHREAD + std::mutex m_jniMutex; + std::condition_variable m_jniCondition; + std::thread* m_jniWorker; +#endif +}; diff --git a/src/decrypters/widevineandroid/jsmn.c b/src/decrypters/widevineandroid/jsmn.c new file mode 100644 index 000000000..77c9b1f08 --- /dev/null +++ b/src/decrypters/widevineandroid/jsmn.c @@ -0,0 +1,319 @@ +/* + * Copyright (C) 2017 peak3d (http://www.peak3d.de) + * 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 "jsmn.h" + +/** + * Allocates a fresh unused token from the token pull. + */ +static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser, + jsmntok_t *tokens, size_t num_tokens) { + jsmntok_t *tok; + if (parser->toknext >= num_tokens) { + return NULL; + } + tok = &tokens[parser->toknext++]; + tok->start = tok->end = -1; + tok->size = 0; +#ifdef JSMN_PARENT_LINKS + tok->parent = -1; +#endif + return tok; +} + +/** + * Fills token type and boundaries. + */ +static void jsmn_fill_token(jsmntok_t *token, jsmntype_t type, + int start, int end) { + token->type = type; + token->start = start; + token->end = end; + token->size = 0; +} + +/** + * Fills next available token with JSON primitive. + */ +static int jsmn_parse_primitive(jsmn_parser *parser, const char *js, + size_t len, jsmntok_t *tokens, size_t num_tokens) { + jsmntok_t *token; + int start; + + start = parser->pos; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + switch (js[parser->pos]) { +#ifndef JSMN_STRICT + /* In strict mode primitive must be followed by "," or "}" or "]" */ + case ':': +#endif + case '\t' : case '\r' : case '\n' : case ' ' : + case ',' : case ']' : case '}' : + goto found; + } + if (js[parser->pos] < 32 || js[parser->pos] >= 127) { + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } +#ifdef JSMN_STRICT + /* In strict mode primitive must be followed by a comma/object/array */ + parser->pos = start; + return JSMN_ERROR_PART; +#endif + +found: + if (tokens == NULL) { + parser->pos--; + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + parser->pos--; + return 0; +} + +/** + * Fills next token with JSON string. + */ +static int jsmn_parse_string(jsmn_parser *parser, const char *js, + size_t len, jsmntok_t *tokens, size_t num_tokens) { + jsmntok_t *token; + + int start = parser->pos; + + parser->pos++; + + /* Skip starting quote */ + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c = js[parser->pos]; + + /* Quote: end of string */ + if (c == '\"') { + if (tokens == NULL) { + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_STRING, start+1, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + return 0; + } + + /* Backslash: Quoted symbol expected */ + if (c == '\\' && parser->pos + 1 < len) { + int i; + parser->pos++; + switch (js[parser->pos]) { + /* Allowed escaped symbols */ + case '\"': case '/' : case '\\' : case 'b' : + case 'f' : case 'r' : case 'n' : case 't' : + break; + /* Allows escaped symbol \uXXXX */ + case 'u': + parser->pos++; + for(i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0'; i++) { + /* If it isn't a hex character we have an error */ + if(!((js[parser->pos] >= 48 && js[parser->pos] <= 57) || /* 0-9 */ + (js[parser->pos] >= 65 && js[parser->pos] <= 70) || /* A-F */ + (js[parser->pos] >= 97 && js[parser->pos] <= 102))) { /* a-f */ + parser->pos = start; + return JSMN_ERROR_INVAL; + } + parser->pos++; + } + parser->pos--; + break; + /* Unexpected symbol */ + default: + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } + } + parser->pos = start; + return JSMN_ERROR_PART; +} + +/** + * Parse JSON string and fill tokens. + */ +int jsmn_parse(jsmn_parser *parser, const char *js, size_t len, + jsmntok_t *tokens, unsigned int num_tokens) { + int r; + int i; + jsmntok_t *token; + int count = parser->toknext; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c; + jsmntype_t type; + + c = js[parser->pos]; + switch (c) { + case '{': case '[': + count++; + if (tokens == NULL) { + break; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) + return JSMN_ERROR_NOMEM; + if (parser->toksuper != -1) { + tokens[parser->toksuper].size++; +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + } + token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY); + token->start = parser->pos; + parser->toksuper = parser->toknext - 1; + break; + case '}': case ']': + if (tokens == NULL) + break; + type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY); +#ifdef JSMN_PARENT_LINKS + if (parser->toknext < 1) { + return JSMN_ERROR_INVAL; + } + token = &tokens[parser->toknext - 1]; + for (;;) { + if (token->start != -1 && token->end == -1) { + if (token->type != type) { + return JSMN_ERROR_INVAL; + } + token->end = parser->pos + 1; + parser->toksuper = token->parent; + break; + } + if (token->parent == -1) { + break; + } + token = &tokens[token->parent]; + } +#else + for (i = parser->toknext - 1; i >= 0; i--) { + token = &tokens[i]; + if (token->start != -1 && token->end == -1) { + if (token->type != type) { + return JSMN_ERROR_INVAL; + } + parser->toksuper = -1; + token->end = parser->pos + 1; + break; + } + } + /* Error if unmatched closing bracket */ + if (i == -1) return JSMN_ERROR_INVAL; + for (; i >= 0; i--) { + token = &tokens[i]; + if (token->start != -1 && token->end == -1) { + parser->toksuper = i; + break; + } + } +#endif + break; + case '\"': + r = jsmn_parse_string(parser, js, len, tokens, num_tokens); + if (r < 0) return r; + count++; + if (parser->toksuper != -1 && tokens != NULL) + tokens[parser->toksuper].size++; + break; + case '\t' : case '\r' : case '\n' : case ' ': + break; + case ':': + parser->toksuper = parser->toknext - 1; + break; + case ',': + if (tokens != NULL && parser->toksuper != -1 && + tokens[parser->toksuper].type != JSMN_ARRAY && + tokens[parser->toksuper].type != JSMN_OBJECT) { +#ifdef JSMN_PARENT_LINKS + parser->toksuper = tokens[parser->toksuper].parent; +#else + for (i = parser->toknext - 1; i >= 0; i--) { + if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT) { + if (tokens[i].start != -1 && tokens[i].end == -1) { + parser->toksuper = i; + break; + } + } + } +#endif + } + break; +#ifdef JSMN_STRICT + /* In strict mode primitives are: numbers and booleans */ + case '-': case '0': case '1' : case '2': case '3' : case '4': + case '5': case '6': case '7' : case '8': case '9': + case 't': case 'f': case 'n' : + /* And they must not be keys of the object */ + if (tokens != NULL && parser->toksuper != -1) { + jsmntok_t *t = &tokens[parser->toksuper]; + if (t->type == JSMN_OBJECT || + (t->type == JSMN_STRING && t->size != 0)) { + return JSMN_ERROR_INVAL; + } + } +#else + /* In non-strict mode every unquoted value is a primitive */ + default: +#endif + r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens); + if (r < 0) return r; + count++; + if (parser->toksuper != -1 && tokens != NULL) + tokens[parser->toksuper].size++; + break; + +#ifdef JSMN_STRICT + /* Unexpected char in strict mode */ + default: + return JSMN_ERROR_INVAL; +#endif + } + } + + if (tokens != NULL) { + for (i = parser->toknext - 1; i >= 0; i--) { + /* Unmatched opened object or array */ + if (tokens[i].start != -1 && tokens[i].end == -1) { + return JSMN_ERROR_PART; + } + } + } + + return count; +} + +/** + * Creates a new parser based over a given buffer with an array of tokens + * available. + */ +void jsmn_init(jsmn_parser *parser) { + parser->pos = 0; + parser->toknext = 0; + parser->toksuper = -1; +} + diff --git a/src/decrypters/widevineandroid/jsmn.h b/src/decrypters/widevineandroid/jsmn.h new file mode 100644 index 000000000..d187da53c --- /dev/null +++ b/src/decrypters/widevineandroid/jsmn.h @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2017 peak3d (http://www.peak3d.de) + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#ifndef __JSMN_H_ +#define __JSMN_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * JSON type identifier. Basic types are: + * o Object + * o Array + * o String + * o Other primitive: number, boolean (true/false) or null + */ +typedef enum { + JSMN_UNDEFINED = 0, + JSMN_OBJECT = 1, + JSMN_ARRAY = 2, + JSMN_STRING = 3, + JSMN_PRIMITIVE = 4 +} jsmntype_t; + +enum jsmnerr { + /* Not enough tokens were provided */ + JSMN_ERROR_NOMEM = -1, + /* Invalid character inside JSON string */ + JSMN_ERROR_INVAL = -2, + /* The string is not a full JSON packet, more bytes expected */ + JSMN_ERROR_PART = -3 +}; + +/** + * JSON token description. + * @param type type (object, array, string etc.) + * @param start start position in JSON data string + * @param end end position in JSON data string + */ +typedef struct { + jsmntype_t type; + int start; + int end; + int size; +#ifdef JSMN_PARENT_LINKS + int parent; +#endif +} jsmntok_t; + +/** + * JSON parser. Contains an array of token blocks available. Also stores + * the string being parsed now and current position in that string + */ +typedef struct { + unsigned int pos; /* offset in the JSON string */ + unsigned int toknext; /* next token to allocate */ + int toksuper; /* superior token node, e.g parent object or array */ +} jsmn_parser; + +/** + * Create JSON parser over an array of tokens + */ +void jsmn_init(jsmn_parser *parser); + +/** + * Run JSON parser. It parses a JSON data string into and array of tokens, each describing + * a single JSON object. + */ +int jsmn_parse(jsmn_parser *parser, const char *js, size_t len, + jsmntok_t *tokens, unsigned int num_tokens); + +#ifdef __cplusplus +} +#endif + +#endif /* __JSMN_H_ */ diff --git a/src/main.cpp b/src/main.cpp index f424ade5b..c97ed6435 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -51,7 +51,7 @@ bool CInputStreamAdaptive::Open(const kodi::addon::InputstreamProperty& props) std::uint8_t drmConfig{0}; if (m_kodiProps.m_isLicensePersistentStorage) - drmConfig |= SSD::SSD_DECRYPTER::CONFIG_PERSISTENTSTORAGE; + drmConfig |= DRM::IDecrypter::CONFIG_PERSISTENTSTORAGE; m_session = std::make_shared(m_kodiProps, url, props.GetProfileFolder()); m_session->SetVideoResolution(m_currentVideoWidth, m_currentVideoHeight, m_currentVideoMaxWidth, @@ -161,13 +161,13 @@ bool CInputStreamAdaptive::GetStream(int streamid, kodi::addon::InputstreamInfo& cryptoSession.SetSessionId(sessionId); if (m_session->GetDecrypterCaps(cdmId).flags & - SSD::SSD_DECRYPTER::SSD_CAPS::SSD_SUPPORTS_DECODING) + DRM::IDecrypter::DecrypterCapabilites::SSD_SUPPORTS_DECODING) stream->m_info.SetFeatures(INPUTSTREAM_FEATURE_DECODE); else stream->m_info.SetFeatures(0); cryptoSession.SetFlags((m_session->GetDecrypterCaps(cdmId).flags & - SSD::SSD_DECRYPTER::SSD_CAPS::SSD_SECURE_DECODER) + DRM::IDecrypter::DecrypterCapabilites::SSD_SECURE_DECODER) ? STREAM_CRYPTO_FLAG_SECURE_DECODER : 0); stream->m_info.SetCryptoSession(cryptoSession); @@ -637,7 +637,7 @@ bool CVideoCodecAdaptive::Open(const kodi::addon::VideoCodecInitdata& initData) { if (!m_session || !m_session->GetDecrypter()) return false; - + if ((initData.GetCodecType() == VIDEOCODEC_H264 || initData.GetCodecType() == VIDEOCODEC_AV1) && !initData.GetExtraDataSize() && !(m_state & STATE_WAIT_EXTRADATA)) { @@ -673,7 +673,7 @@ bool CVideoCodecAdaptive::Open(const kodi::addon::VideoCodecInitdata& initData) Adaptive_CencSingleSampleDecrypter* ssd(m_session->GetSingleSampleDecrypter(sessionId)); return m_session->GetDecrypter()->OpenVideoDecoder( - ssd, reinterpret_cast(initData.GetCStructure())); + ssd, initData.GetCStructure()); } bool CVideoCodecAdaptive::Reconfigure(const kodi::addon::VideoCodecInitdata& initData) @@ -686,31 +686,8 @@ bool CVideoCodecAdaptive::AddData(const DEMUX_PACKET& packet) if (!m_session || !m_session->GetDecrypter()) return false; - SSD::SSD_SAMPLE sample{}; - sample.data = packet.pData; - sample.dataSize = packet.iSize; - sample.pts = static_cast(packet.pts); - if (packet.cryptoInfo) // Is an encrypted demux packet - { - sample.cryptoInfo.numSubSamples = packet.cryptoInfo->numSubSamples; - sample.cryptoInfo.mode = packet.cryptoInfo->mode; - sample.cryptoInfo.cryptBlocks = packet.cryptoInfo->cryptBlocks; - sample.cryptoInfo.skipBlocks = packet.cryptoInfo->skipBlocks; - sample.cryptoInfo.clearBytes = packet.cryptoInfo->clearBytes; - sample.cryptoInfo.cipherBytes = packet.cryptoInfo->cipherBytes; - sample.cryptoInfo.iv = packet.cryptoInfo->iv; - sample.cryptoInfo.ivSize = 16; - sample.cryptoInfo.kid = packet.cryptoInfo->kid; - sample.cryptoInfo.kidSize = 16; - sample.cryptoInfo.flags = packet.cryptoInfo->flags; - } - else - { - sample.cryptoInfo.mode = static_cast(CryptoMode::NONE); - } - return m_session->GetDecrypter()->DecryptAndDecodeVideo( - dynamic_cast(this), &sample) != SSD::VC_ERROR; + dynamic_cast(this), &packet) != VC_ERROR; } VIDEOCODEC_RETVAL CVideoCodecAdaptive::GetPicture(VIDEOCODEC_PICTURE& picture) @@ -723,8 +700,7 @@ VIDEOCODEC_RETVAL CVideoCodecAdaptive::GetPicture(VIDEOCODEC_PICTURE& picture) VIDEOCODEC_RETVAL::VC_EOF}; return vrvm[m_session->GetDecrypter()->VideoFrameDataToPicture( - dynamic_cast(this), - reinterpret_cast(&picture))]; + dynamic_cast(this), &picture)]; } void CVideoCodecAdaptive::Reset() diff --git a/src/samplereader/FragmentedSampleReader.cpp b/src/samplereader/FragmentedSampleReader.cpp index 2664a0663..09639fa0f 100644 --- a/src/samplereader/FragmentedSampleReader.cpp +++ b/src/samplereader/FragmentedSampleReader.cpp @@ -34,7 +34,7 @@ CFragmentedSampleReader::CFragmentedSampleReader(AP4_ByteStream* input, AP4_Track* track, AP4_UI32 streamId, Adaptive_CencSingleSampleDecrypter* ssd, - const SSD::SSD_DECRYPTER::SSD_CAPS& dcaps) + const DRM::IDecrypter::DecrypterCapabilites& dcaps) : AP4_LinearReader{*movie, input}, m_track{track}, m_streamId{streamId}, @@ -121,7 +121,7 @@ AP4_Result CFragmentedSampleReader::ReadSample() { bool useDecryptingDecoder = m_protectedDesc && - (m_decrypterCaps.flags & SSD::SSD_DECRYPTER::SSD_CAPS::SSD_SECURE_PATH) != 0; + (m_decrypterCaps.flags & DRM::IDecrypter::DecrypterCapabilites::SSD_SECURE_PATH) != 0; bool decrypterPresent{m_decrypter != nullptr}; if (AP4_FAILED(result = ReadNextSample(m_track->GetId(), m_sample, (m_decrypter || useDecryptingDecoder) ? m_encrypted @@ -221,7 +221,7 @@ uint64_t CFragmentedSampleReader::GetDuration() const bool CFragmentedSampleReader::IsEncrypted() const { - return (m_decrypterCaps.flags & SSD::SSD_DECRYPTER::SSD_CAPS::SSD_SECURE_PATH) != 0 && + return (m_decrypterCaps.flags & DRM::IDecrypter::DecrypterCapabilites::SSD_SECURE_PATH) != 0 && m_decrypter != nullptr; } @@ -484,7 +484,7 @@ void CFragmentedSampleReader::UpdateSampleDescription() } } - if ((m_decrypterCaps.flags & SSD::SSD_DECRYPTER::SSD_CAPS::SSD_ANNEXB_REQUIRED) != 0) + if ((m_decrypterCaps.flags & DRM::IDecrypter::DecrypterCapabilites::SSD_ANNEXB_REQUIRED) != 0) m_codecHandler->ExtraDataToAnnexB(); } diff --git a/src/samplereader/FragmentedSampleReader.h b/src/samplereader/FragmentedSampleReader.h index 668f13b84..0ecd91735 100644 --- a/src/samplereader/FragmentedSampleReader.h +++ b/src/samplereader/FragmentedSampleReader.h @@ -8,10 +8,10 @@ #pragma once -#include "../SSD_dll.h" #include "../codechandler/CodecHandler.h" -#include "../common/AdaptiveDecrypter.h" #include "../common/AdaptiveCencSampleDecrypter.h" +#include "../common/AdaptiveDecrypter.h" +#include "../decrypters/IDecrypter.h" #include "../utils/log.h" #include "SampleReader.h" @@ -23,7 +23,7 @@ class ATTR_DLL_LOCAL CFragmentedSampleReader : public ISampleReader, public AP4_ AP4_Track* track, AP4_UI32 streamId, Adaptive_CencSingleSampleDecrypter* ssd, - const SSD::SSD_DECRYPTER::SSD_CAPS& dcaps); + const DRM::IDecrypter::DecrypterCapabilites& dcaps); ~CFragmentedSampleReader(); @@ -65,7 +65,7 @@ class ATTR_DLL_LOCAL CFragmentedSampleReader : public ISampleReader, public AP4_ AP4_UI32 m_poolId{0}; AP4_UI32 m_streamId; AP4_UI32 m_sampleDescIndex{1}; - SSD::SSD_DECRYPTER::SSD_CAPS m_decrypterCaps; + DRM::IDecrypter::DecrypterCapabilites m_decrypterCaps; unsigned int m_failCount{0}; bool m_bSampleDescChanged{false}; bool m_eos{false}; diff --git a/src/utils/Base64Utils.cpp b/src/utils/Base64Utils.cpp index e45e9e94e..dfee8c063 100644 --- a/src/utils/Base64Utils.cpp +++ b/src/utils/Base64Utils.cpp @@ -8,10 +8,7 @@ #include "Base64Utils.h" -#ifndef INPUTSTREAM_SSD_BUILD -//! @todo: not accessible between two differents projects builds #include "log.h" -#endif using namespace UTILS::BASE64; @@ -140,9 +137,7 @@ void UTILS::BASE64::Decode(const char* input, const size_t length, std::string& // Characters that are not '=', in the middle of the padding, are not allowed if (paddingStarted) { -#ifndef INPUTSTREAM_SSD_BUILD LOG::LogF(LOGERROR, "Invalid base64-encoded string: Incorrect padding characters"); -#endif output.clear(); return; } @@ -179,16 +174,12 @@ void UTILS::BASE64::Decode(const char* input, const size_t length, std::string& // There is exactly one extra valid, non-padding, base64 character. // This is an invalid length, as there is no possible input that // could encoded into such a base64 string. -#ifndef INPUTSTREAM_SSD_BUILD LOG::LogF(LOGERROR, "Invalid base64-encoded string: number of data characters cannot be 1 " "more than a multiple of 4"); -#endif } else { -#ifndef INPUTSTREAM_SSD_BUILD LOG::LogF(LOGERROR, "Invalid base64-encoded string: Incorrect padding"); -#endif } output.clear(); } diff --git a/wvdecrypter/CMakeLists.txt b/wvdecrypter/CMakeLists.txt deleted file mode 100644 index 4c79b31c5..000000000 --- a/wvdecrypter/CMakeLists.txt +++ /dev/null @@ -1,87 +0,0 @@ -cmake_minimum_required(VERSION 3.5) - -if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") - # require at least gcc 4.8 - if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 4.8) - message(FATAL_ERROR "GCC version must be at least 4.8!") - endif() -endif() - -project(wvdecrypter) - -if(NOT BENTO4_FOUND) - find_package(Bento4 REQUIRED) -endif() - -if(CORE_SYSTEM_NAME STREQUAL android) - SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wl,--no-undefined") - include_directories ( - ${BENTO4_INCLUDE_DIRS} - jni/jutils - jni/src - ) -else() - include_directories ( - ${BENTO4_INCLUDE_DIRS} - ${CMAKE_CURRENT_SOURCE_DIR}/cdm - ) -endif() - -set(BENTOUSESTCFS 1) -add_definitions(-DINPUTSTREAM_SSD_BUILD) - -if(CORE_SYSTEM_NAME STREQUAL android) - subdirs ( jni ) - add_library ( ssd_wv SHARED - Helper.cpp - wvdecrypter_android.cpp - jsmn.c - ../src/utils/Utils.cpp - ../src/utils/StringUtils.cpp - ../src/utils/Base64Utils.cpp - ../src/utils/DigestMD5Utils.cpp - ) -else() - if(WIN32) - set(CDMTYPE "win.cc") - add_definitions("-DWIDEVINECDMFILENAME=\"widevinecdm.dll\"") - elseif(${CMAKE_SYSTEM_NAME} MATCHES "Darwin") - set(CDMTYPE "mac.mm") - add_definitions("-DWIDEVINECDMFILENAME=\"libwidevinecdm.dylib\"") - else() - set(CDMTYPE "posix.cc") - add_definitions("-DWIDEVINECDMFILENAME=\"libwidevinecdm.so\"") - endif() - - add_library ( ssd_wv SHARED - Helper.cpp - wvdecrypter.cpp - jsmn.c - ../src/utils/Utils.cpp - ../src/utils/StringUtils.cpp - ../src/utils/Base64Utils.cpp - ../src/utils/DigestMD5Utils.cpp - cdm/base/native_library.cc - cdm/base/native_library_${CDMTYPE} - cdm/media/cdm/cdm_adapter.cc - cdm/media/cdm/cdm_type_conversion.cc - ) -endif() - -if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin") - target_link_libraries (ssd_wv - ${BENTO4_LIBRARIES} - "-framework CoreFoundation" - ) -elseif(CORE_SYSTEM_NAME STREQUAL android) - target_link_libraries (ssd_wv -# mediandk - jni - ${BENTO4_LIBRARIES} - ) -else() - target_link_libraries (ssd_wv - ${CMAKE_DL_LIBS} - ${BENTO4_LIBRARIES} - ) -endif() diff --git a/wvdecrypter/Helper.cpp b/wvdecrypter/Helper.cpp deleted file mode 100644 index 3bb5b017c..000000000 --- a/wvdecrypter/Helper.cpp +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2022 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 "Helper.h" - -#include // fopen - -SSD::SSD_HOST* GLOBAL::Host = nullptr; - -void LOG::Log(SSD::SSDLogLevel level, const char* format, ...) -{ - if (!GLOBAL::Host) - return; - - va_list args; - va_start(args, format); - GLOBAL::Host->LogVA(level, format, args); - va_end(args); -} - -//! @todo: with ssd_wv refactor we must use the file management of the IA interface -//! so this method will be removed in favour of UTILS::FILESYS::SaveFile -void SSD_UTILS::SaveFile(std::string_view filePath, std::string_view data) -{ - FILE* f = std::fopen(filePath.data(), "wb"); - if (f) - { - std::fwrite(data.data(), 1, data.size(), f); - std::fclose(f); - } - else - LOG::LogF(SSD::SSDLogLevel::SSDERROR, "Cannot open file \"%s\" for writing.", filePath.data()); -} diff --git a/wvdecrypter/Helper.h b/wvdecrypter/Helper.h deleted file mode 100644 index 5b9d0358f..000000000 --- a/wvdecrypter/Helper.h +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 2022 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 "../src/SSD_dll.h" - -namespace GLOBAL -{ -// Give shared access to the host interface -extern SSD::SSD_HOST* Host; -} // namespace GLOBAL - -namespace LOG -{ -void Log(SSD::SSDLogLevel level, const char* format, ...); - -#define LogF(level, format, ...) Log((level), ("%s: " format), __FUNCTION__, ##__VA_ARGS__) - -} // namespace LOG - -namespace SSD_UTILS -{ -void SaveFile(std::string_view filePath, std::string_view data); -} diff --git a/wvdecrypter/README.md b/wvdecrypter/README.md deleted file mode 100644 index 79cabfdb9..000000000 --- a/wvdecrypter/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# wvdecrypter - -This piece of software implements a CencSingleSampleDecrypter wich can be used with inputstream.mpd addon. - -- wvdecrypter is developed to decrypt widevine encrypted media content. On most operating systems it is necessary to provide third part software ([lib]w\*devi\*ecdm.dll/so). -- the inputstream.mpd is part of the official kodi repository and has not to be build. wvdecrypter NOT! -wvdecrypter comes together with the inputstream.mpd source code because of the interface files wich are necessary for compiling wvdecrypter. Beside this the Bento4 library wich comes with inputstream.mpd already has some other cenc decrypter implementations (e.g. clearkey) and can be implemented easily. - -##### How to build: -Linux: -1.) go into the wvdecrypter folder -2.) cmake . -3.) make -This will produce a libssd_wv.so file - -Windows: -1.) open an cygwin or msys terminal window -2.) go into the wvdecrypter folder -3.) cmake . -G"Visual Studio 12 2013" -4.) Open VisualStudio, open the generated .sln solution and compile -This will produce a ssd_wv.dll file - -##### Installation: -- search your kodi addon folder for "inputstream.mpd.[dll / so] -- go into this folder with the file named above -- create a new folder "decrypter" -- copy the shared library from the build step into this new folder "decrypter" -- search your system for [lib]w\*devi\*ecdm.dll/so (asterix's must be replaced) and copy it also into the new decrypters folder. - -##### Todo: -- automate things with [lib]w\*devi\*ecdm.dll/so -- remove the need copying [lib]w\*devi\*ecdm.dll/so - -With kodi17 + inputstream.mpd + wvdecrypter you are prepared to play files wich are orginated to HTML5 players. - -Have fun! \ No newline at end of file diff --git a/wvdecrypter/jni/CMakeLists.txt b/wvdecrypter/jni/CMakeLists.txt deleted file mode 100644 index c2137383a..000000000 --- a/wvdecrypter/jni/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -cmake_minimum_required(VERSION 3.5) -project(jni) - -# jni Library -file(GLOB JNI_SOURCES - jutils/*.cpp - src/*.cpp -) -include_directories(BEFORE ${PROJECT_SOURCE_DIR}/jutils) -add_library(jni STATIC ${JNI_SOURCES}) diff --git a/wvdecrypter/wvdecrypter.cpp b/wvdecrypter/wvdecrypter.cpp deleted file mode 100644 index ea3120b45..000000000 --- a/wvdecrypter/wvdecrypter.cpp +++ /dev/null @@ -1,1658 +0,0 @@ -/* - * Copyright (C) 2016 liberty-developer (https://github.com/liberty-developer) - * 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 "../src/common/AdaptiveDecrypter.h" -#include "../src/utils/Base64Utils.h" -#include "../src/utils/DigestMD5Utils.h" -#include "../src/utils/StringUtils.h" -#include "../src/utils/Utils.h" -#include "Helper.h" -#include "cdm/media/cdm/cdm_adapter.h" -#include "cdm/media/cdm/cdm_type_conversion.h" -#include "jsmn.h" -#include "kodi/tools/StringUtils.h" - -#include -#include -#include -#include -#include - -#include - -#ifndef WIDEVINECDMFILENAME -#error "WIDEVINECDMFILENAME must be set" -#endif - -using namespace SSD; -using namespace UTILS; -using namespace kodi::tools; - -/******************************************************* -CDM -********************************************************/ - -/*---------------------------------------------------------------------- -| CdmDecryptedBlock implementation -+---------------------------------------------------------------------*/ - -class CdmDecryptedBlock : public cdm::DecryptedBlock { -public: - CdmDecryptedBlock() :buffer_(0), timestamp_(0) {}; - virtual ~CdmDecryptedBlock() {}; - - virtual void SetDecryptedBuffer(cdm::Buffer* buffer) override { buffer_ = buffer; }; - virtual cdm::Buffer* DecryptedBuffer() override { return buffer_; }; - - virtual void SetTimestamp(int64_t timestamp) override { timestamp_ = timestamp; }; - virtual int64_t Timestamp() const override { return timestamp_; }; -private: - cdm::Buffer *buffer_; - int64_t timestamp_; -}; - -/*---------------------------------------------------------------------- -| CdmDecryptedBlock implementation -+---------------------------------------------------------------------*/ -class CdmBuffer : public cdm::Buffer { -public: - CdmBuffer(AP4_DataBuffer *buffer) :buffer_(buffer) {}; - virtual ~CdmBuffer() {}; - - virtual void Destroy() override {}; - - virtual uint32_t Capacity() const override - { - return buffer_->GetBufferSize(); - }; - virtual uint8_t* Data() override - { - return (uint8_t*)buffer_->GetData(); - }; - virtual void SetSize(uint32_t size) override - { - buffer_->SetDataSize(size); - }; - virtual uint32_t Size() const override - { - return buffer_->GetDataSize(); - }; -private: - AP4_DataBuffer *buffer_; -}; - -/*---------------------------------------------------------------------- -| CdmVideoDecoder implementation -+---------------------------------------------------------------------*/ - -class CdmFixedBuffer : public cdm::Buffer { -public: - CdmFixedBuffer() : data_(nullptr), dataSize_(0), capacity_(0), buffer_(nullptr), instance_(nullptr) {}; - virtual ~CdmFixedBuffer() {}; - - virtual void Destroy() override - { - GLOBAL::Host->ReleaseBuffer(instance_, buffer_); - delete this; - }; - - virtual uint32_t Capacity() const override - { - return capacity_; - }; - - virtual uint8_t* Data() override - { - return data_; - }; - - virtual void SetSize(uint32_t size) override - { - dataSize_ = size; - }; - - virtual uint32_t Size() const override - { - return dataSize_; - }; - - void initialize(void *instance, uint8_t* data, size_t dataSize, void *buffer) - { - instance_ = instance; - data_ = data; - dataSize_ = 0; - capacity_ = dataSize; - buffer_ = buffer; - } - - void *Buffer() const - { - return buffer_; - }; - -private: - uint8_t *data_; - size_t dataSize_, capacity_; - void *buffer_; - void *instance_; -}; - -/*---------------------------------------------------------------------- -| WV_CencSingleSampleDecrypter -+---------------------------------------------------------------------*/ -class WV_DRM; - -class WV_CencSingleSampleDecrypter : public Adaptive_CencSingleSampleDecrypter -{ -public: - // methods - WV_CencSingleSampleDecrypter(WV_DRM &drm, AP4_DataBuffer &pssh, std::string_view defaultKeyId, bool skipSessionMessage, CryptoMode cryptoMode); - virtual ~WV_CencSingleSampleDecrypter(); - - void GetCapabilities(const uint8_t* key, uint32_t media, SSD_DECRYPTER::SSD_CAPS &caps); - virtual const char *GetSessionId() override; - void CloseSessionId(); - AP4_DataBuffer GetChallengeData(); - - void SetSession(const char* session, uint32_t session_size, const uint8_t *data, size_t data_size) - { - std::lock_guard lock(renewal_lock_); - - session_ = std::string(session, session_size); - challenge_.SetData(data, data_size); - LOG::Log(SSDDEBUG, "Opened widevine session ID: %s", session_.c_str()); - } - - void AddSessionKey(const uint8_t *data, size_t data_size, uint32_t status); - bool HasKeyId(const uint8_t *keyid); - - virtual AP4_Result SetFragmentInfo(AP4_UI32 pool_id, - const AP4_UI08* key, - const AP4_UI08 nal_length_size, - AP4_DataBuffer& annexb_sps_pps, - AP4_UI32 flags, - CryptoInfo cryptoInfo) override; - virtual AP4_UI32 AddPool() override; - virtual void RemovePool(AP4_UI32 poolid) override; - - - virtual AP4_Result DecryptSampleData(AP4_UI32 pool_id, - AP4_DataBuffer& data_in, - AP4_DataBuffer& data_out, - - // always 16 bytes - const AP4_UI08* iv, - - // pass 0 for full decryption - unsigned int subsample_count, - - // array of integers. NULL if subsample_count is 0 - const AP4_UI16* bytes_of_cleartext_data, - - // array of integers. NULL if subsample_count is 0 - const AP4_UI32* bytes_of_encrypted_data) override; - - bool OpenVideoDecoder(const SSD_VIDEOINITDATA *initData); - SSD_DECODE_RETVAL DecryptAndDecodeVideo(void* hostInstance, SSD_SAMPLE* sample); - SSD_DECODE_RETVAL VideoFrameDataToPicture(void* hostInstance, SSD_PICTURE *picture); - void ResetVideo(); - - void SetDefaultKeyId(std::string_view keyId) override; - void AddKeyId(std::string_view keyId) override; - -private: - void CheckLicenseRenewal(); - bool SendSessionMessage(); - - WV_DRM &drm_; - std::string session_; - AP4_DataBuffer pssh_, challenge_; - std::string m_defaultKeyId; - struct WVSKEY - { - bool operator == (WVSKEY const &other) const { return keyid == other.keyid; }; - std::string keyid; - cdm::KeyStatus status; - }; - std::vector keys_; - - AP4_UI16 hdcp_version_; - int hdcp_limit_; - int resolution_limit_; - - AP4_DataBuffer decrypt_in_, decrypt_out_; - - struct FINFO - { - const AP4_UI08 *key_; - AP4_UI08 nal_length_size_; - AP4_UI16 decrypter_flags_; - AP4_DataBuffer annexb_sps_pps_; - CryptoInfo m_cryptoInfo; - }; - std::vector fragment_pool_; - 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& data_in, - size_t& startPos, - const unsigned int subsamplePos, - const AP4_UI16* bytes_of_cleartext_data, - const AP4_UI32* bytes_of_encrypted_data); - 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 promise_id_; - bool drained_; - - std::list m_videoFrames; - std::mutex renewal_lock_; - CryptoMode m_EncryptionMode; - - std::optional m_currentVideoDecConfig; -}; - - -class WV_DRM : public media::CdmAdapterClient -{ -public: - WV_DRM(const char* licenseURL, const AP4_DataBuffer &serverCert, const uint8_t config); - virtual ~WV_DRM(); - - virtual void OnCDMMessage(const char* session, uint32_t session_size, CDMADPMSG msg, const uint8_t *data, size_t data_size, uint32_t status) override; - - virtual cdm::Buffer *AllocateBuffer(size_t sz) override - { - SSD_PICTURE pic; - pic.decodedDataSize = sz; - if (GLOBAL::Host->GetBuffer(host_instance_, pic)) - { - CdmFixedBuffer *buf = new CdmFixedBuffer; - buf->initialize(host_instance_, pic.decodedData, pic.decodedDataSize, pic.buffer); - return buf; - } - return nullptr; - }; - - void insertssd(WV_CencSingleSampleDecrypter* ssd) { ssds.push_back(ssd); }; - void removessd(WV_CencSingleSampleDecrypter* ssd) - { - std::vector::iterator res(std::find(ssds.begin(), ssds.end(), ssd)); - if (res != ssds.end()) - ssds.erase(res); - }; - - media::CdmAdapter *GetCdmAdapter() { return wv_adapter.get(); }; - const std::string &GetLicenseURL() { return license_url_; }; - - cdm::Status DecryptAndDecodeFrame(void* hostInstance, cdm::InputBuffer_2 &cdm_in, media::CdmVideoFrame *frame) - { - // DecryptAndDecodeFrame calls CdmAdapter::Allocate which calls Host->GetBuffer - // that cast hostInstance to CInstanceVideoCodec to get the frame buffer - // so we have temporary set the host instance - host_instance_ = hostInstance; - cdm::Status ret = wv_adapter->DecryptAndDecodeFrame(cdm_in, frame); - host_instance_ = nullptr; - return ret; - } - -private: - std::shared_ptr wv_adapter; - std::string license_url_; - void *host_instance_; - - std::vector ssds; -}; - -WV_DRM::WV_DRM(const char* licenseURL, const AP4_DataBuffer &serverCert, const uint8_t config) - : license_url_(licenseURL) - , host_instance_(0) -{ - std::string strLibPath = GLOBAL::Host->GetLibraryPath(); - if (strLibPath.empty()) - { - LOG::Log(SSDERROR, "No Widevine library path specified in settings"); - return; - } - strLibPath += WIDEVINECDMFILENAME; - - std::string strBasePath = GLOBAL::Host->GetProfilePath(); - char cSep = strBasePath.back(); - strBasePath += "widevine"; - strBasePath += cSep; - GLOBAL::Host->CreateDir(strBasePath.c_str()); - - //Build up a CDM path to store decrypter specific stuff. Each domain gets it own path - const char* bspos(strchr(license_url_.c_str(), ':')); - if (!bspos || bspos[1] != '/' || bspos[2] != '/' || !(bspos = strchr(bspos + 3, '/'))) - { - LOG::Log(SSDERROR, "Unable to find protocol inside license URL"); - return; - } - if (bspos - license_url_.c_str() > 256) - { - LOG::Log(SSDERROR, "Length of license URL domain exeeds max. size of 256"); - return; - } - char buffer[1024]; - buffer[(bspos - license_url_.c_str()) * 2] = 0; - AP4_FormatHex(reinterpret_cast(license_url_.c_str()), bspos - license_url_.c_str(), buffer); - - strBasePath += buffer; - strBasePath += cSep; - GLOBAL::Host->CreateDir(strBasePath.c_str()); - - wv_adapter = std::shared_ptr(new media::CdmAdapter( - "com.widevine.alpha", - strLibPath, - strBasePath, - media::CdmConfig(false, (config & SSD::SSD_DECRYPTER::CONFIG_PERSISTENTSTORAGE) != 0), - dynamic_cast(this))); - if (!wv_adapter->valid()) - { - LOG::Log(SSDERROR, "Unable to load widevine shared library (%s)", strLibPath.c_str()); - wv_adapter = nullptr; - return; - } - - if (serverCert.GetDataSize()) - wv_adapter->SetServerCertificate(0, serverCert.GetData(), serverCert.GetDataSize()); - - // For backward compatibility: If no | is found in URL, use the most common working config - if (license_url_.find('|') == std::string::npos) - license_url_ += "|Content-Type=application%2Foctet-stream|R{SSM}|"; - - //wv_adapter->GetStatusForPolicy(); - //wv_adapter->QueryOutputProtectionStatus(); -} - -WV_DRM::~WV_DRM() -{ - if (wv_adapter) - { - wv_adapter->RemoveClient(); - wv_adapter = nullptr; - } -} - -void WV_DRM::OnCDMMessage(const char* session, uint32_t session_size, CDMADPMSG msg, const uint8_t *data, size_t data_size, uint32_t status) -{ - LOG::Log(SSDDEBUG, "CDMMessage: %u arrived!", msg); - std::vector::iterator b(ssds.begin()), e(ssds.end()); - for (; b != e; ++b) - if (!(*b)->GetSessionId() || strncmp((*b)->GetSessionId(), session, session_size) == 0) - break; - - if (b == ssds.end()) - return; - - if (msg == CDMADPMSG::kSessionMessage) - { - (*b)->SetSession(session, session_size, data, data_size); - } - else if (msg == CDMADPMSG::kSessionKeysChange) - (*b)->AddSessionKey(data, data_size, status); -}; - -/*---------------------------------------------------------------------- -| WV_CencSingleSampleDecrypter::WV_CencSingleSampleDecrypter -+---------------------------------------------------------------------*/ - -WV_CencSingleSampleDecrypter::WV_CencSingleSampleDecrypter(WV_DRM& drm, - AP4_DataBuffer& pssh, - std::string_view defaultKeyId, - bool skipSessionMessage, - CryptoMode cryptoMode) - : drm_(drm), - pssh_(pssh), - hdcp_version_(99), - hdcp_limit_(0), - resolution_limit_(0), - promise_id_(1), - drained_(true), - m_defaultKeyId{defaultKeyId}, - m_EncryptionMode{cryptoMode} -{ - SetParentIsOwner(false); - - if (pssh.GetDataSize() > 4096) - { - LOG::LogF(SSDERROR, "PSSH init data with length %u seems not to be cenc init data", - pssh.GetDataSize()); - return; - } - - drm_.insertssd(this); - - if (GLOBAL::Host->IsDebugSaveLicense()) - { - //! @todo: with ssd_wv refactor the path must be combined with - //! UTILS::FILESYS::PathCombine - std::string debugFilePath = GLOBAL::Host->GetProfilePath(); - debugFilePath += "EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED.init"; - - std::string data{reinterpret_cast(pssh.GetData()), pssh.GetDataSize()}; - SSD_UTILS::SaveFile(debugFilePath, data); - } - - if (memcmp(pssh.GetData() + 4, "pssh", 4) != 0) - { - unsigned int buf_size = 32 + pssh.GetDataSize(); - uint8_t buf[4096 + 32]; - - // This will request a new session and initializes session_id and message members in cdm_adapter. - // message will be used to create a license request in the step after CreateSession call. - // Initialization data is the widevine cdm pssh code in google proto style found in mpd schemeIdUri - static uint8_t proto[] = { 0x00, 0x00, 0x00, 0x63, 0x70, 0x73, 0x73, 0x68, 0x00, 0x00, 0x00, 0x00, 0xed, 0xef, 0x8b, 0xa9, - 0x79, 0xd6, 0x4a, 0xce, 0xa3, 0xc8, 0x27, 0xdc, 0xd5, 0x1d, 0x21, 0xed, 0x00, 0x00, 0x00, 0x00 }; - - proto[2] = static_cast((buf_size >> 8) & 0xFF); - proto[3] = static_cast(buf_size & 0xFF); - proto[30] = static_cast((pssh.GetDataSize() >> 8) & 0xFF); - proto[31] = static_cast(pssh.GetDataSize()); - - memcpy(buf, proto, sizeof(proto)); - memcpy(&buf[32], pssh.GetData(), pssh.GetDataSize()); - pssh_.SetData(buf, buf_size); - } - - drm.GetCdmAdapter()->CreateSessionAndGenerateRequest(promise_id_++, cdm::SessionType::kTemporary, cdm::InitDataType::kCenc, - reinterpret_cast(pssh_.GetData()), pssh_.GetDataSize()); - - int retrycount=0; - while (session_.empty() && ++retrycount < 100) - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - if (session_.empty()) - { - LOG::LogF(SSDERROR, "Cannot perform License update, no session available"); - return; - } - - if (skipSessionMessage) - return; - - while (challenge_.GetDataSize() > 0 && SendSessionMessage()); -} - -WV_CencSingleSampleDecrypter::~WV_CencSingleSampleDecrypter() -{ - drm_.removessd(this); -} - -void WV_CencSingleSampleDecrypter::GetCapabilities(const uint8_t* key, uint32_t media, SSD_DECRYPTER::SSD_CAPS &caps) -{ - caps = { 0, hdcp_version_, hdcp_limit_ }; - - if (session_.empty()) { - LOG::LogF(SSDDEBUG, "Session empty"); - return; - } - - caps.flags = SSD_DECRYPTER::SSD_CAPS::SSD_SUPPORTS_DECODING; - - if (keys_.empty()) { - LOG::LogF(SSDDEBUG, "Keys empty"); - return; - } - - if (!caps.hdcpLimit) - caps.hdcpLimit = resolution_limit_; - - //caps.flags |= (SSD_DECRYPTER::SSD_CAPS::SSD_SECURE_PATH | SSD_DECRYPTER::SSD_CAPS::SSD_ANNEXB_REQUIRED); - //return; - - /*for (auto k : keys_) - if (!key || memcmp(k.keyid.data(), key, 16) == 0) - { - if (k.status != 0) - { - if (media == SSD_DECRYPTER::SSD_CAPS::SSD_MEDIA_VIDEO) - caps.flags |= (SSD_DECRYPTER::SSD_CAPS::SSD_SECURE_PATH | SSD_DECRYPTER::SSD_CAPS::SSD_ANNEXB_REQUIRED); - else - caps.flags = SSD_DECRYPTER::SSD_CAPS::SSD_INVALID; - } - break; - } - */ - if ((caps.flags & SSD_DECRYPTER::SSD_CAPS::SSD_SUPPORTS_DECODING) != 0) - { - AP4_UI32 poolid(AddPool()); - fragment_pool_[poolid].key_ = key ? key : reinterpret_cast(keys_.front().keyid.data()); - fragment_pool_[poolid].m_cryptoInfo.m_mode = m_EncryptionMode; - - AP4_DataBuffer in, out; - AP4_UI32 encb[2] = { 1,1 }; - AP4_UI16 clearb[2] = { 5,5 }; - AP4_Byte vf[12]={0,0,0,1,9,255,0,0,0,1,10,255}; - const AP4_UI08 iv[] = { 1,2,3,4,5,6,7,8,0,0,0,0,0,0,0,0 }; - in.SetBuffer(vf,12); - in.SetDataSize(12); - try { - encb[0] = 12; - clearb[0] = 0; - if (DecryptSampleData(poolid, in, out, iv, 1, clearb, encb) != AP4_SUCCESS) - { - LOG::LogF(SSDDEBUG, "Single decrypt failed, secure path only"); - if (media == SSD_DECRYPTER::SSD_CAPS::SSD_MEDIA_VIDEO) - caps.flags |= (SSD_DECRYPTER::SSD_CAPS::SSD_SECURE_PATH | SSD_DECRYPTER::SSD_CAPS::SSD_ANNEXB_REQUIRED); - else - caps.flags = SSD_DECRYPTER::SSD_CAPS::SSD_INVALID; - } - else - { - LOG::LogF(SSDDEBUG, "Single decrypt possible"); - caps.flags |= SSD_DECRYPTER::SSD_CAPS::SSD_SINGLE_DECRYPT; - caps.hdcpVersion = 99; - caps.hdcpLimit = resolution_limit_; - } - } - catch (const std::exception& e) { - LOG::LogF(SSDDEBUG, "Decrypt error, assuming secure path: %s", e.what()); - caps.flags |= (SSD_DECRYPTER::SSD_CAPS::SSD_SECURE_PATH | SSD_DECRYPTER::SSD_CAPS::SSD_ANNEXB_REQUIRED); - } - RemovePool(poolid); - } else { - LOG::LogF(SSDDEBUG, "Decoding not supported"); - } -} - -const char *WV_CencSingleSampleDecrypter::GetSessionId() -{ - return session_.empty()? nullptr : session_.c_str(); -} - -void WV_CencSingleSampleDecrypter::CloseSessionId() -{ - if (!session_.empty()) - { - LOG::LogF(SSDDEBUG, "Closing widevine session ID: %s", session_.c_str()); - drm_.GetCdmAdapter()->CloseSession(++promise_id_, session_.data(), session_.size()); - - LOG::LogF(SSDDEBUG, "Widevine session ID %s closed", session_.c_str()); - session_.clear(); - } -} - -AP4_DataBuffer WV_CencSingleSampleDecrypter::GetChallengeData() -{ - return challenge_; -} - -void WV_CencSingleSampleDecrypter::CheckLicenseRenewal() -{ - { - std::lock_guard lock(renewal_lock_); - if (!challenge_.GetDataSize()) - return; - } - SendSessionMessage(); -} - -bool WV_CencSingleSampleDecrypter::SendSessionMessage() -{ - std::vector blocks{StringUtils::Split(drm_.GetLicenseURL(), '|')}; - - if (blocks.size() != 4) - { - LOG::LogF(SSDERROR, "Wrong \"|\" blocks in license URL. Four blocks (req | header | body | " - "response) are expected in license URL"); - return false; - } - - if (GLOBAL::Host->IsDebugSaveLicense()) - { - //! @todo: with ssd_wv refactor the path must be combined with - //! UTILS::FILESYS::PathCombine - std::string debugFilePath = GLOBAL::Host->GetProfilePath(); - debugFilePath += "EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED.challenge"; - - std::string data{reinterpret_cast(challenge_.GetData()), challenge_.GetDataSize()}; - SSD_UTILS::SaveFile(debugFilePath, data); - } - - //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(challenge_.GetData(), challenge_.GetDataSize())}; - msgEncoded = STRING::URLEncode(msgEncoded); - blocks[0].replace(insPos - 1, 6, msgEncoded); - } - else - { - LOG::Log(SSDERROR, "Unsupported License request template (command)"); - return false; - } - } - - insPos = blocks[0].find("{HASH}"); - if (insPos != std::string::npos) - { - DIGEST::MD5 md5; - md5.Update(challenge_.GetData(), challenge_.GetDataSize()); - md5.Finalize(); - blocks[0].replace(insPos, 6, md5.HexDigest()); - } - - void* file = GLOBAL::Host->CURLCreate(blocks[0].c_str()); - - size_t nbRead; - std::string response, resLimit, contentType; - char buf[2048]; - bool serverCertRequest; - - //Set our std headers - GLOBAL::Host->CURLAddOption(file, SSD_HOST::OPTION_PROTOCOL, "acceptencoding", "gzip, deflate"); - GLOBAL::Host->CURLAddOption(file, SSD_HOST::OPTION_PROTOCOL, "seekable", "0"); - GLOBAL::Host->CURLAddOption(file, SSD_HOST::OPTION_HEADER, "Expect", ""); - - //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]); - } - GLOBAL::Host->CURLAddOption(file, SSD_HOST::OPTION_PROTOCOL, 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(challenge_.GetData(), 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(challenge_.GetData(), challenge_.GetDataSize())}; - blocks[2].replace(insPos - 1, 6, msgEncoded); - size_written = msgEncoded.size(); - } - else - { - blocks[2].replace(insPos - 1, 6, reinterpret_cast(challenge_.GetData()), challenge_.GetDataSize()); - size_written = challenge_.GetDataSize(); - } - } - else - { - LOG::Log(SSDERROR, "Unsupported License request template (body / ?{SSM})"); - goto SSMFAIL; - } - - 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(session_)}; - - 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, session_.data(), session_.size()); - size_written = session_.size(); - } - } - else - { - LOG::LogF(SSDERROR, "Unsupported License request template (body / ?{SID})"); - goto SSMFAIL; - } - } - - 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])}; - GLOBAL::Host->CURLAddOption(file, SSD_HOST::OPTION_PROTOCOL, "postdata", encData.c_str()); - } - - serverCertRequest = challenge_.GetDataSize() == 2; - challenge_.SetDataSize(0); - - if (!GLOBAL::Host->CURLOpen(file)) - { - LOG::Log(SSDERROR, "License server returned failure"); - goto SSMFAIL; - } - - // read the file - while ((nbRead = GLOBAL::Host->ReadFile(file, buf, 1024)) > 0) - response += std::string((const char*)buf, nbRead); - - resLimit = - GLOBAL::Host->CURLGetProperty(file, SSD_HOST::CURLPROPERTY::PROPERTY_HEADER, "X-Limit-Video"); - contentType = - GLOBAL::Host->CURLGetProperty(file, SSD_HOST::CURLPROPERTY::PROPERTY_HEADER, "Content-Type"); - - if (!resLimit.empty()) - { - std::string::size_type posMax = resLimit.find("max="); - if (posMax != std::string::npos) - resolution_limit_ = std::atoi(resLimit.data() + (posMax + 4)); - } - - GLOBAL::Host->CloseFile(file); - file = 0; - - if (nbRead != 0) - { - LOG::LogF(SSDERROR, "Could not read full SessionMessage response"); - goto SSMFAIL; - } - - if (GLOBAL::Host->IsDebugSaveLicense()) - { - //! @todo: with ssd_wv refactor the path must be combined with - //! UTILS::FILESYS::PathCombine - std::string debugFilePath = GLOBAL::Host->GetProfilePath(); - debugFilePath += "EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED.response"; - - SSD_UTILS::SaveFile(debugFilePath, response); - } - - if (serverCertRequest && contentType.find("application/octet-stream") == std::string::npos) - serverCertRequest = false; - - if (!blocks[3].empty() && !serverCertRequest) - { - if (blocks[3][0] == 'J' || (blocks[3].size() > 1 && blocks[3][0] == 'B' && blocks[3][1] == 'J')) - { - int dataPos = 2; - - if (response.size() >= 3 && blocks[3][0] == 'B') - { - response = BASE64::Decode(response); - dataPos = 3; - } - - jsmn_parser jsn; - jsmntok_t tokens[256]; - - jsmn_init(&jsn); - int i(0), numTokens = jsmn_parse(&jsn, response.c_str(), response.size(), tokens, 256); - - std::vector jsonVals{StringUtils::Split(blocks[3].substr(dataPos), ';')}; - - // Find HDCP limit - if (jsonVals.size() > 1) - { - for (; i < numTokens; ++i) - if (tokens[i].type == JSMN_STRING && tokens[i].size == 1 && jsonVals[1].size() == static_cast(tokens[i].end - tokens[i].start) - && strncmp(response.c_str() + tokens[i].start, jsonVals[1].c_str(), tokens[i].end - tokens[i].start) == 0) - break; - if (i < numTokens) - hdcp_limit_ = std::atoi((response.c_str() + tokens[i + 1].start)); - } - // Find license key - if (jsonVals.size() > 0) - { - for (i = 0; i < numTokens; ++i) - if (tokens[i].type == JSMN_STRING && tokens[i].size == 1 && jsonVals[0].size() == static_cast(tokens[i].end - tokens[i].start) - && strncmp(response.c_str() + tokens[i].start, jsonVals[0].c_str(), tokens[i].end - tokens[i].start) == 0) - { - if (i + 1 < numTokens && tokens[i + 1].type == JSMN_ARRAY && tokens[i + 1].size == 1) - ++i; - break; - } - } - else - i = numTokens; - - if (i < numTokens) - { - std::string respData{ - response.substr(tokens[i + 1].start, tokens[i + 1].end - tokens[i + 1].start)}; - - if (blocks[3][dataPos - 1] == 'B') - { - respData = BASE64::Decode(respData); - } - - drm_.GetCdmAdapter()->UpdateSession(++promise_id_, session_.data(), session_.size(), - reinterpret_cast(respData.c_str()), - respData.size()); - } - else - { - LOG::LogF(SSDERROR, "Unable to find %s in JSON string", blocks[3].c_str() + 2); - goto SSMFAIL; - } - } - else if (blocks[3][0] == 'H' && blocks[3].size() >= 2) - { - //Find the payload - std::string::size_type payloadPos = response.find("\r\n\r\n"); - if (payloadPos != std::string::npos) - { - payloadPos += 4; - if (blocks[3][1] == 'B') - drm_.GetCdmAdapter()->UpdateSession(++promise_id_, session_.data(), session_.size(), - reinterpret_cast(response.c_str() + payloadPos), response.size() - payloadPos); - else - { - LOG::LogF(SSDERROR, "Unsupported HTTP payload data type definition"); - goto SSMFAIL; - } - } - else - { - LOG::LogF(SSDERROR, "Unable to find HTTP payload in response"); - goto SSMFAIL; - } - } - else if (blocks[3][0] == 'B' && blocks[3].size() == 1) - { - std::string decRespData{BASE64::Decode(response)}; - - drm_.GetCdmAdapter()->UpdateSession(++promise_id_, session_.data(), session_.size(), - reinterpret_cast(decRespData.c_str()), - decRespData.size()); - } - else - { - LOG::LogF(SSDERROR, "Unsupported License request template (response)"); - goto SSMFAIL; - } - } - else // its binary - simply push the returned data as update - { - drm_.GetCdmAdapter()->UpdateSession(++promise_id_, session_.data(), session_.size(), - reinterpret_cast(response.data()), - response.size()); - } - - if (keys_.empty()) - { - LOG::LogF(SSDERROR, "License update not successful (no keys)"); - CloseSessionId(); - return false; - } - - LOG::Log(SSDDEBUG, "License update successful"); - return true; - -SSMFAIL: - if (file) - GLOBAL::Host->CloseFile(file); - - return false; -} - -void WV_CencSingleSampleDecrypter::AddSessionKey(const uint8_t *data, size_t data_size, uint32_t status) -{ - WVSKEY key; - std::vector::iterator res; - - key.keyid = std::string((const char*)data, data_size); - if ((res = std::find(keys_.begin(), keys_.end(), key)) == keys_.end()) - res = keys_.insert(res, key); - res->status = static_cast(status); -} - -/*---------------------------------------------------------------------- -| WV_CencSingleSampleDecrypter::SetKeyId -+---------------------------------------------------------------------*/ - -bool WV_CencSingleSampleDecrypter::HasKeyId(const uint8_t *keyid) -{ - if (keyid) - for (std::vector::const_iterator kb(keys_.begin()), ke(keys_.end()); kb != ke; ++kb) - if (kb->keyid.size() == 16 && memcmp(kb->keyid.c_str(), keyid, 16) == 0) - return true; - return false; -} - -AP4_Result WV_CencSingleSampleDecrypter::SetFragmentInfo(AP4_UI32 pool_id, - const AP4_UI08* key, - const AP4_UI08 nal_length_size, - AP4_DataBuffer& annexb_sps_pps, - AP4_UI32 flags, - CryptoInfo cryptoInfo) -{ - if (pool_id >= fragment_pool_.size()) - return AP4_ERROR_OUT_OF_RANGE; - - fragment_pool_[pool_id].key_ = key; - fragment_pool_[pool_id].nal_length_size_ = nal_length_size; - fragment_pool_[pool_id].annexb_sps_pps_.SetData(annexb_sps_pps.GetData(), annexb_sps_pps.GetDataSize()); - fragment_pool_[pool_id].decrypter_flags_ = flags; - fragment_pool_[pool_id].m_cryptoInfo = cryptoInfo; - - return AP4_SUCCESS; -} - -AP4_UI32 WV_CencSingleSampleDecrypter::AddPool() -{ - for (size_t i(0); i < fragment_pool_.size(); ++i) - if (fragment_pool_[i].nal_length_size_ == 99) - { - fragment_pool_[i].nal_length_size_ = 0; - return i; - } - fragment_pool_.push_back(FINFO()); - fragment_pool_.back().nal_length_size_ = 0; - return static_cast(fragment_pool_.size() - 1); -} - - -void WV_CencSingleSampleDecrypter::RemovePool(AP4_UI32 poolid) -{ - fragment_pool_[poolid].nal_length_size_ = 99; - fragment_pool_[poolid].key_ = nullptr; -} - -void WV_CencSingleSampleDecrypter::LogDecryptError(const cdm::Status status, const AP4_UI08* key) -{ - char buf[36]; - buf[32] = 0; - AP4_FormatHex(key, 16, buf); - LOG::LogF(SSDDEBUG, "Decrypt failed with error: %d and key: %s", status, buf); -} - -void WV_CencSingleSampleDecrypter::SetCdmSubsamples(std::vector& subsamples, - bool isCbc) -{ - if (isCbc) - { - subsamples.resize(1); - subsamples[0] = {0, decrypt_in_.GetDataSize()}; - } - else - { - subsamples.push_back({0, decrypt_in_.GetDataSize()}); - } -} - -void WV_CencSingleSampleDecrypter::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(decrypt_out_.GetData() + cipherPos, bytesOfEncryptedData[subsamplePos]); - pos += bytesOfEncryptedData[subsamplePos]; - cipherPos += bytesOfEncryptedData[subsamplePos]; -} - -void WV_CencSingleSampleDecrypter::UnpackSubsampleData(AP4_DataBuffer& dataIn, - size_t& pos, - const unsigned int subsamplePos, - const AP4_UI16* bytesOfCleartextData, - const AP4_UI32* bytesOfEncryptedData) -{ - pos += bytesOfCleartextData[subsamplePos]; - decrypt_in_.AppendData(dataIn.GetData() + pos, bytesOfEncryptedData[subsamplePos]); - pos += bytesOfEncryptedData[subsamplePos]; -} - -void WV_CencSingleSampleDecrypter::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.key_; - 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}; -} - -/*---------------------------------------------------------------------- -| WV_CencSingleSampleDecrypter::DecryptSampleData -+---------------------------------------------------------------------*/ -AP4_Result WV_CencSingleSampleDecrypter::DecryptSampleData(AP4_UI32 pool_id, - AP4_DataBuffer& data_in, - AP4_DataBuffer& data_out, - const AP4_UI08* iv, - unsigned int subsample_count, - const AP4_UI16* bytes_of_cleartext_data, - const AP4_UI32* bytes_of_encrypted_data) -{ - if (!drm_.GetCdmAdapter()) - { - data_out.SetData(data_in.GetData(), data_in.GetDataSize()); - return AP4_SUCCESS; - } - - FINFO &fragInfo(fragment_pool_[pool_id]); - - if(fragInfo.decrypter_flags_ & SSD_DECRYPTER::SSD_CAPS::SSD_SECURE_PATH) //we can not decrypt only - { - if (fragInfo.nal_length_size_ > 4) - { - LOG::LogF(SSDERROR, "Nalu length size > 4 not supported"); - return AP4_ERROR_NOT_SUPPORTED; - } - - AP4_UI16 dummyClear(0); - AP4_UI32 dummyCipher(data_in.GetDataSize()); - - if (iv) - { - if (!subsample_count) - { - subsample_count = 1; - bytes_of_cleartext_data = &dummyClear; - bytes_of_encrypted_data = &dummyCipher; - } - - data_out.SetData(reinterpret_cast(&subsample_count), sizeof(subsample_count)); - data_out.AppendData(reinterpret_cast(bytes_of_cleartext_data), subsample_count * sizeof(AP4_UI16)); - data_out.AppendData(reinterpret_cast(bytes_of_encrypted_data), subsample_count * sizeof(AP4_UI32)); - data_out.AppendData(reinterpret_cast(iv), 16); - data_out.AppendData(reinterpret_cast(fragInfo.key_), 16); - } - else - { - data_out.SetDataSize(0); - bytes_of_cleartext_data = &dummyClear; - bytes_of_encrypted_data = &dummyCipher; - } - - if (fragInfo.nal_length_size_ && (!iv || bytes_of_cleartext_data[0] > 0)) - { - //check NAL / subsample - const AP4_Byte *packet_in(data_in.GetData()), *packet_in_e(data_in.GetData() + data_in.GetDataSize()); - AP4_UI16 *clrb_out(iv ? reinterpret_cast(data_out.UseData() + sizeof(subsample_count)):nullptr); - unsigned int nalunitcount(0), nalunitsum(0), configSize(0); - - while (packet_in < packet_in_e) - { - uint32_t nalsize(0); - for (unsigned int i(0); i < fragInfo.nal_length_size_; ++i) { nalsize = (nalsize << 8) + *packet_in++; }; - - //look if we have to inject sps / pps - if (fragInfo.annexb_sps_pps_.GetDataSize() && (*packet_in & 0x1F) != 9 /*AVC_NAL_AUD*/) - { - data_out.AppendData(fragInfo.annexb_sps_pps_.GetData(), - fragInfo.annexb_sps_pps_.GetDataSize()); - if(clrb_out) *clrb_out += fragInfo.annexb_sps_pps_.GetDataSize(); - configSize = fragInfo.annexb_sps_pps_.GetDataSize(); - fragInfo.annexb_sps_pps_.SetDataSize(0); - } - - // Annex-B Start pos - static AP4_Byte annexbStartCode[4] = {0x00, 0x00, 0x00, 0x01}; - data_out.AppendData(annexbStartCode, 4); - data_out.AppendData(packet_in, nalsize); - packet_in += nalsize; - if (clrb_out) *clrb_out += (4 - fragInfo.nal_length_size_); - ++nalunitcount; - - if (!iv) - { - nalunitsum = 0; - } - else if (nalsize + fragInfo.nal_length_size_ + nalunitsum >= *bytes_of_cleartext_data + *bytes_of_encrypted_data) - { - AP4_UI32 summedBytes(0); - do - { - summedBytes += *bytes_of_cleartext_data + *bytes_of_encrypted_data; - ++bytes_of_cleartext_data; - ++bytes_of_encrypted_data; - ++clrb_out; - --subsample_count; - } while (subsample_count && nalsize + fragInfo.nal_length_size_ + nalunitsum > summedBytes); - - if (nalsize + fragInfo.nal_length_size_ + nalunitsum > summedBytes) - { - LOG::LogF(SSDERROR, "NAL Unit exceeds subsample definition (nls: %u) %u -> %u ", - static_cast(fragInfo.nal_length_size_), - static_cast(nalsize + fragInfo.nal_length_size_ + nalunitsum), - summedBytes); - return AP4_ERROR_NOT_SUPPORTED; - } - nalunitsum = 0; - } - else - nalunitsum += nalsize + fragInfo.nal_length_size_; - } - if (packet_in != packet_in_e || subsample_count) - { - LOG::Log(SSDERROR, "NAL Unit definition incomplete (nls: %u) %u -> %u ", - static_cast(fragInfo.nal_length_size_), - static_cast(packet_in_e - packet_in), subsample_count); - return AP4_ERROR_NOT_SUPPORTED; - } - } - else - data_out.AppendData(data_in.GetData(), data_in.GetDataSize()); - return AP4_SUCCESS; - } - - if (!fragInfo.key_) - { - LOG::LogF(SSDDEBUG, "No Key"); - return AP4_ERROR_INVALID_PARAMETERS; - } - - data_out.SetDataSize(0); - - uint16_t clearb(0); - uint32_t cipherb(data_in.GetDataSize()); - - // check input parameters - if (iv == NULL) return AP4_ERROR_INVALID_PARAMETERS; - if (subsample_count) { - if (bytes_of_cleartext_data == NULL || bytes_of_encrypted_data == NULL) - { - LOG::LogF(SSDDEBUG, "Invalid input params"); - return AP4_ERROR_INVALID_PARAMETERS; - } - } - else - { - subsample_count = 1; - bytes_of_cleartext_data = &clearb; - bytes_of_encrypted_data = &cipherb; - } - cdm::Status ret{cdm::Status::kSuccess}; - std::vector subsamples; - subsamples.reserve(subsample_count); - - bool useCbcDecrypt{fragInfo.m_cryptoInfo.m_mode == CryptoMode::AES_CBC}; - - // We can only decrypt with subsamples set to 1 - // This must be handled differently for CENC and CBCS - // CENC: - // CDM should get 1 block of encrypted data per sample, encrypted data - // from all subsamples should be formed into a contiguous block. - // Even if there is only 1 subsample, we should remove cleartext data - // from it before passing to CDM. - // CBCS: - // Due to the nature of this cipher subsamples must be decrypted separately - - const unsigned int iterations{useCbcDecrypt ? subsample_count : 1}; - size_t absPos{0}; - - for (unsigned int i{0}; i < iterations; ++i) - { - decrypt_in_.Reserve(data_in.GetDataSize()); - decrypt_in_.SetDataSize(0); - size_t decryptInPos = absPos; - if (useCbcDecrypt) - { - UnpackSubsampleData(data_in, decryptInPos, i, bytes_of_cleartext_data, - bytes_of_encrypted_data); - } - else - { - for (unsigned int subsamplePos{0}; subsamplePos < subsample_count; ++subsamplePos) - { - UnpackSubsampleData(data_in, absPos, subsamplePos, bytes_of_cleartext_data, bytes_of_encrypted_data); - } - } - - if (decrypt_in_.GetDataSize() > 0) // remember to include when calling setcdmsubsamples - { - SetCdmSubsamples(subsamples, useCbcDecrypt); - } - - else // we have nothing to decrypt in this iteration - { - if (useCbcDecrypt) - { - data_out.AppendData(data_in.GetData() + absPos, bytes_of_cleartext_data[i]); - absPos += bytes_of_cleartext_data[i]; - continue; - } - else // we can exit here for CENC and just return the input buffer - { - data_out.AppendData(data_in.GetData(), data_in.GetDataSize()); - return AP4_SUCCESS; - } - } - - cdm::InputBuffer_2 cdm_in; - SetInput(cdm_in, decrypt_in_, 1, iv, fragInfo, subsamples); - decrypt_out_.SetDataSize(decrypt_in_.GetDataSize()); - CdmBuffer buf{&decrypt_out_}; - CdmDecryptedBlock cdm_out; - cdm_out.SetDecryptedBuffer(&buf); - - CheckLicenseRenewal(); - ret = drm_.GetCdmAdapter()->Decrypt(cdm_in, &cdm_out); - - if (ret == cdm::Status::kSuccess) - { - size_t cipherPos = 0; - if (useCbcDecrypt) - { - RepackSubsampleData(data_in, data_out, absPos, cipherPos, i, bytes_of_cleartext_data, - bytes_of_encrypted_data); - } - else - { - size_t absPos{0}; - for (unsigned int i{0}; i < subsample_count; ++i) - { - RepackSubsampleData(data_in, data_out, absPos, cipherPos, i, bytes_of_cleartext_data, - bytes_of_encrypted_data); - } - } - } - else - { - LogDecryptError(ret, fragInfo.key_); - } - } - return (ret == cdm::Status::kSuccess) ? AP4_SUCCESS : AP4_ERROR_INVALID_PARAMETERS; -} - -bool WV_CencSingleSampleDecrypter::OpenVideoDecoder(const SSD_VIDEOINITDATA* initData) -{ - cdm::VideoDecoderConfig_3 vconfig = media::ToCdmVideoDecoderConfig(initData, m_EncryptionMode); - - // InputStream interface call OpenVideoDecoder also during playback when stream quality - // change, so we reinitialize the decoder only when the codec change - if (m_currentVideoDecConfig.has_value()) - { - cdm::VideoDecoderConfig_3& currVidConfig = *m_currentVideoDecConfig; - if (currVidConfig.codec == vconfig.codec && currVidConfig.profile == vconfig.profile) - return true; - - drm_.GetCdmAdapter()->DeinitializeDecoder(cdm::StreamType::kStreamTypeVideo); - } - - m_currentVideoDecConfig = vconfig; - - cdm::Status ret = drm_.GetCdmAdapter()->InitializeVideoDecoder(vconfig); - m_videoFrames.clear(); - drained_ = true; - - LOG::LogF(SSDDEBUG, "Initialization returned status: %s", - media::CdmStatusToString(ret).c_str()); - return ret == cdm::Status::kSuccess; -} - -SSD_DECODE_RETVAL WV_CencSingleSampleDecrypter::DecryptAndDecodeVideo(void* hostInstance, SSD_SAMPLE* sample) -{ - // if we have an picture waiting, or not yet get the dest buffer, do nothing - if (m_videoFrames.size() == 4) - return VC_ERROR; - - if (sample->cryptoInfo.numSubSamples > 0 && - (!sample->cryptoInfo.clearBytes || !sample->cryptoInfo.cipherBytes)) - { - return VC_ERROR; - } - - cdm::InputBuffer_2 inputBuffer{}; - std::vector subsamples; - - media::ToCdmInputBuffer(sample, &subsamples, &inputBuffer); - - if (sample->dataSize > 0) - drained_ = false; - - //LICENSERENEWAL: - CheckLicenseRenewal(); - - media::CdmVideoFrame videoFrame; - cdm::Status status = drm_.DecryptAndDecodeFrame(hostInstance, inputBuffer, &videoFrame); - - if (status == cdm::Status::kSuccess) - { - std::list::iterator f(m_videoFrames.begin()); - while (f != m_videoFrames.end() && f->Timestamp() < videoFrame.Timestamp()) - { - ++f; - } - m_videoFrames.insert(f, videoFrame); - return VC_NONE; - } - else if (status == cdm::Status::kNeedMoreData && inputBuffer.data) - { - return VC_NONE; - } - else if (status == cdm::Status::kNoKey) - { - char buf[36]; - buf[0] = 0; - buf[32] = 0; - AP4_FormatHex(inputBuffer.key_id, inputBuffer.key_id_size, buf); - LOG::LogF(SSDERROR, "Returned CDM status kNoKey for key %s", buf); - return VC_EOF; - } - - LOG::LogF(SSDDEBUG, "Returned CDM status: %i", status); - return VC_ERROR; -} - -SSD_DECODE_RETVAL WV_CencSingleSampleDecrypter::VideoFrameDataToPicture(void* hostInstance, SSD_PICTURE* picture) -{ - if (m_videoFrames.size() == 4 || (m_videoFrames.size() > 0 && (picture->flags & SSD_PICTURE::FLAG_DRAIN))) - { - media::CdmVideoFrame& videoFrame(m_videoFrames.front()); - - picture->width = videoFrame.Size().width; - picture->height = videoFrame.Size().height; - picture->pts = videoFrame.Timestamp(); - picture->decodedData = videoFrame.FrameBuffer()->Data(); - picture->decodedDataSize = videoFrame.FrameBuffer()->Size(); - picture->buffer = static_cast(videoFrame.FrameBuffer())->Buffer(); - - for (unsigned int i(0); i < cdm::VideoPlane::kMaxPlanes; ++i) - { - picture->planeOffsets[i] = videoFrame.PlaneOffset(static_cast(i)); - picture->stride[i] = videoFrame.Stride(static_cast(i)); - } - picture->videoFormat = media::ToSSDVideoFormat(videoFrame.Format()); - videoFrame.SetFrameBuffer(nullptr); //marker for "No Picture" - - delete (CdmFixedBuffer*)(videoFrame.FrameBuffer()); - m_videoFrames.pop_front(); - - return VC_PICTURE; - } - else if ((picture->flags & SSD_PICTURE::FLAG_DRAIN)) - { - static SSD_SAMPLE drainSample{}; - if (drained_ || DecryptAndDecodeVideo(hostInstance, &drainSample) == VC_ERROR) - { - drained_ = true; - return VC_EOF; - } - else - return VC_NONE; - } - - return VC_BUFFER; -} - -void WV_CencSingleSampleDecrypter::ResetVideo() -{ - drm_.GetCdmAdapter()->ResetDecoder(cdm::kStreamTypeVideo); - drained_ = true; -} - -void WV_CencSingleSampleDecrypter::SetDefaultKeyId(std::string_view keyId) -{ - m_defaultKeyId = keyId; -} - -void WV_CencSingleSampleDecrypter::AddKeyId(std::string_view keyId) -{ - WVSKEY key; - key.keyid = keyId; - key.status = cdm::KeyStatus::kUsable; - - if (std::find(keys_.begin(), keys_.end(), key) == keys_.end()) - { - keys_.push_back(key); - } -} - -/*********************************************************************************************/ - -class WVDecrypter : public SSD_DECRYPTER -{ -public: - WVDecrypter() : cdmsession_(nullptr), decoding_decrypter_(nullptr) {}; - virtual ~WVDecrypter() - { - delete cdmsession_; - cdmsession_ = nullptr; - }; - - virtual const char *SelectKeySytem(const char* keySystem) override - { - if (strcmp(keySystem, "com.widevine.alpha")) - return nullptr; - - return "urn:uuid:EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED"; - } - - virtual bool OpenDRMSystem(const char *licenseURL, const AP4_DataBuffer &serverCertificate, const uint8_t config) override - { - cdmsession_ = new WV_DRM(licenseURL, serverCertificate, config); - - return cdmsession_->GetCdmAdapter() != nullptr; - } - - virtual Adaptive_CencSingleSampleDecrypter* CreateSingleSampleDecrypter( - AP4_DataBuffer& pssh, - const char* optionalKeyParameter, - std::string_view defaultkeyid, - bool skipSessionMessage, - CryptoMode cryptoMode) override - { - WV_CencSingleSampleDecrypter* decrypter = - new WV_CencSingleSampleDecrypter(*cdmsession_, pssh, defaultkeyid, skipSessionMessage, cryptoMode); - if (!decrypter->GetSessionId()) - { - delete decrypter; - decrypter = nullptr; - } - return decrypter; - } - - virtual void DestroySingleSampleDecrypter(Adaptive_CencSingleSampleDecrypter* decrypter) override - { - if (decrypter) - { - // close session before dispose - static_cast(decrypter)->CloseSessionId(); - delete static_cast(decrypter); - } - } - - virtual void GetCapabilities(Adaptive_CencSingleSampleDecrypter* decrypter, const uint8_t *keyid, uint32_t media, SSD_DECRYPTER::SSD_CAPS &caps) override - { - if (!decrypter) - { - caps = { 0,0,0 }; - return; - } - - static_cast(decrypter)->GetCapabilities(keyid, media, caps); - } - - virtual bool HasLicenseKey(Adaptive_CencSingleSampleDecrypter* decrypter, - const uint8_t* keyid) override - { - if (decrypter) - return static_cast(decrypter)->HasKeyId(keyid); - return false; - } - - virtual bool HasCdmSession() override - { - return cdmsession_ != nullptr; - } - - virtual std::string GetChallengeB64Data(Adaptive_CencSingleSampleDecrypter* decrypter) override - { - if (!decrypter) - return ""; - - AP4_DataBuffer challengeData = static_cast(decrypter)->GetChallengeData(); - return BASE64::Encode(challengeData.GetData(), challengeData.GetDataSize()); - } - - virtual bool OpenVideoDecoder(Adaptive_CencSingleSampleDecrypter* decrypter, - const SSD_VIDEOINITDATA* initData) override - { - if (!decrypter || !initData) - return false; - - decoding_decrypter_ = static_cast(decrypter); - return decoding_decrypter_->OpenVideoDecoder(initData); - } - - virtual SSD_DECODE_RETVAL DecryptAndDecodeVideo(void* hostInstance, SSD_SAMPLE* sample) override - { - if (!decoding_decrypter_) - return VC_ERROR; - - return decoding_decrypter_->DecryptAndDecodeVideo(hostInstance, sample); - } - - virtual SSD_DECODE_RETVAL VideoFrameDataToPicture(void* hostInstance, SSD_PICTURE* picture) override - { - if (!decoding_decrypter_) - return VC_ERROR; - - return decoding_decrypter_->VideoFrameDataToPicture(hostInstance, picture); - } - - virtual void ResetVideo() override - { - if (decoding_decrypter_) - decoding_decrypter_->ResetVideo(); - } - -private: - WV_DRM *cdmsession_; - WV_CencSingleSampleDecrypter *decoding_decrypter_; -}; - -extern "C" { - -// Linux arm64 version of libwidevinecdm.so depends on two -// dynamic symbols. See https://github.com/xbmc/inputstream.adaptive/issues/1128 -#if defined(__linux__) && defined(__aarch64__) && !defined(ANDROID) -__attribute__((target("no-outline-atomics"))) -int32_t __aarch64_ldadd4_acq_rel(int32_t value, int32_t *ptr) -{ - return __atomic_fetch_add(ptr, value, __ATOMIC_ACQ_REL); -} - -__attribute__((target("no-outline-atomics"))) -int32_t __aarch64_swp4_acq_rel(int32_t value, int32_t *ptr) -{ - return __atomic_exchange_n(ptr, value, __ATOMIC_ACQ_REL); -} -#endif - -#ifdef _WIN32 -#define MODULE_API __declspec(dllexport) -#else -#define MODULE_API -#endif - - SSD_DECRYPTER MODULE_API *CreateDecryptorInstance(class SSD_HOST *h, uint32_t host_version) - { - if (host_version != SSD_HOST::version) - return 0; - GLOBAL::Host = h; - return new WVDecrypter(); - }; - - void MODULE_API DeleteDecryptorInstance(SSD_DECRYPTER *d) - { - delete static_cast(d); - } -}; diff --git a/wvdecrypter/wvdecrypter_android.cpp b/wvdecrypter/wvdecrypter_android.cpp deleted file mode 100644 index b570670be..000000000 --- a/wvdecrypter/wvdecrypter_android.cpp +++ /dev/null @@ -1,1461 +0,0 @@ -/* - * Copyright (C) 2016 liberty-developer (https://github.com/liberty-developer) - * 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 "../src/common/AdaptiveDecrypter.h" -#include "../src/utils/Base64Utils.h" -#include "../src/utils/DigestMD5Utils.h" -#include "../src/utils/StringUtils.h" -#include "../src/utils/Utils.h" -#include "ClassLoader.h" -#include "Helper.h" -#include "jni/src/MediaDrm.h" -#include "jni/src/MediaDrmOnEventListener.h" -#include "jni/src/UUID.h" -#include "jsmn.h" -#include "kodi/tools/StringUtils.h" - -#include -#include -#include -#include -#include - -#include - -using namespace SSD; -using namespace UTILS; -using namespace kodi::tools; - -/******************************************************* -CDM -********************************************************/ - -enum WV_KEYSYSTEM -{ - NONE, - WIDEVINE, - PLAYREADY, - WISEPLAY -}; - -class WV_DRM -{ -public: - WV_DRM(WV_KEYSYSTEM ks, const char* licenseURL, const AP4_DataBuffer &serverCert, jni::CJNIMediaDrmOnEventListener *listener); - ~WV_DRM(); - - jni::CJNIMediaDrm *GetMediaDrm() { return media_drm_; }; - - const std::string &GetLicenseURL() const { return license_url_; }; - - const uint8_t *GetKeySystem() const - { - static const uint8_t keysystemId[3][16] = { - { 0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, 0x4a, 0xce, 0xa3, 0xc8, 0x27, 0xdc, 0xd5, 0x1d, 0x21, 0xed }, - { 0x9A, 0x04, 0xF0, 0x79, 0x98, 0x40, 0x42, 0x86, 0xAB, 0x92, 0xE6, 0x5B, 0xE0, 0x88, 0x5F, 0x95 }, - { 0x3d, 0x5e, 0x6d, 0x35, 0x9b, 0x9a, 0x41, 0xe8, 0xb8, 0x43, 0xdd, 0x3c, 0x6e, 0x72, 0xc4, 0x2c }, - }; - return keysystemId[key_system_-1]; - } - WV_KEYSYSTEM GetKeySystemType() const { return key_system_; }; - void SaveServiceCertificate(); - -private: - void LoadServiceCertificate(); - - WV_KEYSYSTEM key_system_; - jni::CJNIMediaDrm *media_drm_; - std::string license_url_; - std::string m_strBasePath; -}; - -WV_DRM::WV_DRM(WV_KEYSYSTEM ks, const char* licenseURL, const AP4_DataBuffer &serverCert, jni::CJNIMediaDrmOnEventListener *listener) - : key_system_(ks) - , media_drm_(0) - , license_url_(licenseURL) -{ - std::string strBasePath = GLOBAL::Host->GetProfilePath(); - char cSep = strBasePath.back(); - strBasePath += ks == WIDEVINE ? "widevine" : ks == PLAYREADY ? "playready" : "wiseplay"; - strBasePath += cSep; - GLOBAL::Host->CreateDir(strBasePath.c_str()); - - //Build up a CDM path to store decrypter specific stuff. Each domain gets it own path - const char* bspos(strchr(license_url_.c_str(), ':')); - if (!bspos || bspos[1] != '/' || bspos[2] != '/' || !(bspos = strchr(bspos + 3, '/'))) - { - LOG::Log(SSDERROR, "Unable to find protocol inside license URL"); - return; - } - if (bspos - license_url_.c_str() > 256) - { - LOG::Log(SSDERROR, "Length of license URL exeeds max. size of 256"); - return; - } - char buffer[1024]; - buffer[(bspos - license_url_.c_str()) * 2] = 0; - AP4_FormatHex(reinterpret_cast(license_url_.c_str()), bspos - license_url_.c_str(), buffer); - - strBasePath += buffer; - strBasePath += cSep; - GLOBAL::Host->CreateDir(strBasePath.c_str()); - m_strBasePath = strBasePath; - - int64_t mostSigBits(0), leastSigBits(0); - const uint8_t *keySystem = GetKeySystem(); - for (unsigned int i(0); i < 8; ++i) - mostSigBits = (mostSigBits << 8) | keySystem[i]; - for (unsigned int i(8); i < 16; ++i) - leastSigBits = (leastSigBits << 8) | keySystem[i]; - - jni::CJNIUUID uuid(mostSigBits, leastSigBits); - media_drm_ = new jni::CJNIMediaDrm(uuid); - if (xbmc_jnienv()->ExceptionCheck() || !*media_drm_) - { - LOG::LogF(SSDERROR, "Unable to initialize MediaDrm"); - xbmc_jnienv()->ExceptionClear(); - delete media_drm_, media_drm_ = nullptr; - return; - } - - media_drm_->setOnEventListener(*listener); - if (xbmc_jnienv()->ExceptionCheck()) - { - LOG::LogF(SSDERROR, "Exception during installation of EventListener"); - xbmc_jnienv()->ExceptionClear(); - media_drm_->release(); - delete media_drm_, media_drm_ = nullptr; - return; - } - - std::vector strDeviceId = media_drm_->getPropertyByteArray("deviceUniqueId"); - xbmc_jnienv()->ExceptionClear(); - std::string strSecurityLevel = media_drm_->getPropertyString("securityLevel"); - xbmc_jnienv()->ExceptionClear(); - std::string strSystemId = media_drm_->getPropertyString("systemId"); - xbmc_jnienv()->ExceptionClear(); - - - if (key_system_ == WIDEVINE) - { - //media_drm_->setPropertyString("sessionSharing", "enable"); - if (serverCert.GetDataSize()) - media_drm_->setPropertyByteArray("serviceCertificate", std::vector(serverCert.GetData(), serverCert.GetData() + serverCert.GetDataSize())); - else - LoadServiceCertificate(); - - if (xbmc_jnienv()->ExceptionCheck()) - { - LOG::LogF(SSDERROR, "Exception setting Service Certificate"); - xbmc_jnienv()->ExceptionClear(); - media_drm_->release(); - delete media_drm_, media_drm_ = nullptr; - return; - } - } - - LOG::Log(SSDDEBUG, - "MediaDrm initialized (Device unique ID size: %ld, System ID: %s, Security level: %s)", - strDeviceId.size(), strSystemId.c_str(), strSecurityLevel.c_str()); - - if (license_url_.find('|') == std::string::npos) - { - if (key_system_ == WIDEVINE) - license_url_ += "|Content-Type=application%2Foctet-stream|R{SSM}|"; - else if (key_system_ == PLAYREADY) - license_url_ += "|Content-Type=text%2Fxml&SOAPAction=http%3A%2F%2Fschemas.microsoft.com%2FDRM%2F2007%2F03%2Fprotocols%2FAcquireLicense|R{SSM}|"; - else - license_url_ += "|Content-Type=application/json|R{SSM}|"; - } -} - -WV_DRM::~WV_DRM() -{ - if (media_drm_) - { - media_drm_->release(); - if (xbmc_jnienv()->ExceptionCheck()) - { - LOG::LogF(SSDERROR, "Exception releasing media drm"); - xbmc_jnienv()->ExceptionClear(); - } - delete media_drm_, media_drm_ = nullptr; - } -} - -void WV_DRM::LoadServiceCertificate() -{ - std::string filename = m_strBasePath + "service_certificate"; - char* data(nullptr); - size_t sz(0); - FILE *f = fopen(filename.c_str(), "rb"); - - if (f) - { - fseek(f, 0L, SEEK_END); - sz = ftell(f); - fseek(f, 0L, SEEK_SET); - if (sz > 8 && (data = (char*)malloc(sz))) - fread(data, 1, sz, f); - fclose(f); - } - if (data) - { - auto now = std::chrono::system_clock::now(); - uint64_t certTime = *((uint64_t*)data), nowTime = std::chrono::time_point_cast(now).time_since_epoch().count(); - - if (certTime < nowTime && nowTime - certTime < 86400) - media_drm_->setPropertyByteArray("serviceCertificate", std::vector(data + 8, data + sz)); - else - free(data), data = nullptr; - } - if (!data) - { - LOG::Log(SSDDEBUG, "Requesting new Service Certificate"); - media_drm_->setPropertyString("privacyMode", "enable"); - } - else - { - LOG::Log(SSDDEBUG, "Use stored Service Certificate"); - free(data), data = nullptr; - } -} - -void WV_DRM::SaveServiceCertificate() -{ - std::vector sc = media_drm_->getPropertyByteArray("serviceCertificate"); - if (xbmc_jnienv()->ExceptionCheck()) - { - LOG::LogF(SSDWARNING, "Exception retrieving Service Certificate"); - xbmc_jnienv()->ExceptionClear(); - return; - } - - if (sc.empty()) - { - LOG::LogF(SSDWARNING, "Empty Service Certificate"); - return; - } - - std::string filename = m_strBasePath + "service_certificate"; - FILE *f = fopen(filename.c_str(), "wb"); - if (f) - { - auto now = std::chrono::system_clock::now(); - uint64_t nowTime = std::chrono::time_point_cast(now).time_since_epoch().count(); - fwrite((char*)&nowTime, 1, sizeof(uint64_t), f); - fwrite(sc.data(), 1, sc.size(), f); - fclose(f); - } -} - -/*---------------------------------------------------------------------- -| WV_CencSingleSampleDecrypter -+---------------------------------------------------------------------*/ -class WV_CencSingleSampleDecrypter : public Adaptive_CencSingleSampleDecrypter -{ -public: - // methods - WV_CencSingleSampleDecrypter(WV_DRM& drm, - AP4_DataBuffer& pssh, - const char* optionalKeyParameter, - std::string_view defaultKeyId); - ~WV_CencSingleSampleDecrypter(); - - bool StartSession(bool skipSessionMessage) { return KeyUpdateRequest(true, skipSessionMessage); }; - const std::vector &GetSessionIdRaw() { return session_id_; }; - virtual const char *GetSessionId() override; - std::vector GetChallengeData(); - virtual bool HasLicenseKey(const uint8_t *keyid); - - virtual AP4_Result SetFragmentInfo(AP4_UI32 pool_id, - const AP4_UI08* key, - const AP4_UI08 nal_length_size, - AP4_DataBuffer& annexb_sps_pps, - AP4_UI32 flags, - CryptoInfo cryptoInfo) override; - virtual AP4_UI32 AddPool() override; - virtual void RemovePool(AP4_UI32 poolid) override; - - virtual AP4_Result DecryptSampleData(AP4_UI32 pool_id, - AP4_DataBuffer& data_in, - AP4_DataBuffer& data_out, - - // always 16 bytes - const AP4_UI08* iv, - - // pass 0 for full decryption - unsigned int subsample_count, - - // array of integers. NULL if subsample_count is 0 - const AP4_UI16* bytes_of_cleartext_data, - - // array of integers. NULL if subsample_count is 0 - const AP4_UI32* bytes_of_encrypted_data) override; - - void GetCapabilities(const uint8_t *keyid, uint32_t media, SSD_DECRYPTER::SSD_CAPS &caps); - - void RequestNewKeys() { keyUpdateRequested = true; }; - -private: - bool ProvisionRequest(); - bool GetKeyRequest(std::vector& keyRequestData); - bool KeyUpdateRequest(bool waitForKeys, bool skipSessionMessage); - bool SendSessionMessage(const std::vector &keyRequestData); - - WV_DRM &media_drm_; - std::vector pssh_, initial_pssh_; - std::map optParams_; - - std::vector session_id_; - std::vector keySetId_; - std::vector keyRequestData_; - - char session_id_char_[128]; - bool provisionRequested, keyUpdateRequested; - - std::string m_defaultKeyId; - - struct FINFO - { - const AP4_UI08 *key_; - AP4_UI08 nal_length_size_; - AP4_UI16 decrypter_flags_; - AP4_DataBuffer annexb_sps_pps_; - }; - std::vector fragment_pool_; - int hdcp_limit_; - int resolution_limit_; -}; - -/*---------------------------------------------------------------------- -| WV_CencSingleSampleDecrypter::WV_CencSingleSampleDecrypter -+---------------------------------------------------------------------*/ - -WV_CencSingleSampleDecrypter::WV_CencSingleSampleDecrypter(WV_DRM& drm, - AP4_DataBuffer& pssh, - const char* optionalKeyParameter, - std::string_view defaultKeyId) - : media_drm_(drm), - provisionRequested(false), - keyUpdateRequested(false), - hdcp_limit_(0), - resolution_limit_(0), - m_defaultKeyId{defaultKeyId} -{ - SetParentIsOwner(false); - - if (pssh.GetDataSize() > 65535) - { - LOG::LogF(SSDERROR, "PSSH init data with length %u seems not to be cenc init data", - pssh.GetDataSize()); - return; - } - - if (GLOBAL::Host->IsDebugSaveLicense()) - { - //! @todo: with ssd_wv refactor the path must be combined with - //! UTILS::FILESYS::PathCombine - std::string debugFilePath = GLOBAL::Host->GetProfilePath(); - debugFilePath += "EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED.init"; - - std::string data{reinterpret_cast(pssh.GetData()), pssh.GetDataSize()}; - SSD_UTILS::SaveFile(debugFilePath, data); - } - - pssh_.assign(pssh.GetData(), pssh.GetData() + pssh.GetDataSize()); - - if (memcmp(pssh.GetData() + 4, "pssh", 4) != 0) - { - const uint8_t atomheader[] = { 0x00, 0x00, 0x00, 0x00, 0x70, 0x73, 0x73, 0x68, 0x00, 0x00, 0x00, 0x00 }; - uint8_t atom[32]; - memcpy(atom, atomheader, 12); - memcpy(atom+12, media_drm_.GetKeySystem(), 16); - memset(atom+28, 0, 4); - - pssh_.insert(pssh_.begin(), reinterpret_cast(atom), reinterpret_cast(atom + sizeof(atom))); - - pssh_[3] = static_cast(pssh_.size()); - pssh_[2] = static_cast(pssh_.size() >> 8); - - pssh_[sizeof(atom) - 1] = static_cast(pssh_.size()) - sizeof(atom); - pssh_[sizeof(atom) - 2] = static_cast((pssh_.size() - sizeof(atom)) >> 8); - } - initial_pssh_ = pssh_; - - if (optionalKeyParameter) - optParams_["PRCustomData"] = optionalKeyParameter; - - /* - std::vector pui = media_drm_.GetMediaDrm()->getPropertyByteArray("provisioningUniqueId"); - xbmc_jnienv()->ExceptionClear(); - if (pui.size() > 0) - { - std::string encoded{BASE64::Encode(pui.data(), pui.size())}; - optParams_["CDMID"] = encoded; - } - */ - - bool L3FallbackRequested = false; -RETRY_OPEN: - session_id_ = media_drm_.GetMediaDrm()->openSession(); - if (xbmc_jnienv()->ExceptionCheck()) - { - xbmc_jnienv()->ExceptionClear(); - if (!provisionRequested) - { - LOG::LogF(SSDWARNING, "Exception during open session - provisioning..."); - provisionRequested = true; - if (!ProvisionRequest()) - { - if (!L3FallbackRequested && media_drm_.GetMediaDrm()->getPropertyString("securityLevel") == "L1") - { - LOG::LogF(SSDWARNING, "L1 provisioning failed - retrying with L3..."); - L3FallbackRequested = true; - provisionRequested = false; - media_drm_.GetMediaDrm()->setPropertyString("securityLevel", "L3"); - goto RETRY_OPEN; - } - else - return; - } - goto RETRY_OPEN; - } - else - { - LOG::LogF(SSDERROR, "Exception during open session - abort"); - return; - } - } - - if (session_id_.size() == 0) - { - LOG::LogF(SSDERROR, "Unable to open DRM session"); - return; - } - - memcpy(session_id_char_, session_id_.data(), session_id_.size()); - session_id_char_[session_id_.size()] = 0; - - if (media_drm_.GetKeySystemType() != PLAYREADY) - { - int maxSecuritylevel = media_drm_.GetMediaDrm()->getMaxSecurityLevel(); - xbmc_jnienv()->ExceptionClear(); - - LOG::Log(SSDDEBUG, "Session ID: %s, Max security level: %d", session_id_char_, maxSecuritylevel); - } -} - -WV_CencSingleSampleDecrypter::~WV_CencSingleSampleDecrypter() -{ - if (!session_id_.empty()) - { - media_drm_.GetMediaDrm()->removeKeys(session_id_); - if (xbmc_jnienv()->ExceptionCheck()) - { - LOG::LogF(SSDERROR, "removeKeys has raised an exception"); - xbmc_jnienv()->ExceptionClear(); - } - media_drm_.GetMediaDrm()->closeSession(session_id_); - if (xbmc_jnienv()->ExceptionCheck()) - { - LOG::LogF(SSDERROR, "closeSession has raised an exception"); - xbmc_jnienv()->ExceptionClear(); - } - } -} - -const char *WV_CencSingleSampleDecrypter::GetSessionId() -{ - return session_id_char_; -} - -std::vector WV_CencSingleSampleDecrypter::GetChallengeData() -{ - return keyRequestData_; -} - -bool WV_CencSingleSampleDecrypter::HasLicenseKey(const uint8_t *keyid) -{ - // true = one session for all streams, false = one sessions per stream - // false fixes pixaltion issues on some devices when manifest has multiple encrypted streams - return true; -} - -void WV_CencSingleSampleDecrypter::GetCapabilities(const uint8_t *keyid, uint32_t media, SSD_DECRYPTER::SSD_CAPS &caps) -{ - caps = { SSD_DECRYPTER::SSD_CAPS::SSD_SECURE_PATH | SSD_DECRYPTER::SSD_CAPS::SSD_ANNEXB_REQUIRED, 0, hdcp_limit_ }; - - if (caps.hdcpLimit == 0) - caps.hdcpLimit = resolution_limit_; - - if (media_drm_.GetMediaDrm()->getPropertyString("securityLevel") == "L1") - { - caps.hdcpLimit = resolution_limit_; //No restriction - caps.flags |= SSD_DECRYPTER::SSD_CAPS::SSD_SECURE_DECODER; - } - LOG::LogF(SSDDEBUG, "hdcpLimit: %i", caps.hdcpLimit); - - caps.hdcpVersion = 99; -} - -bool WV_CencSingleSampleDecrypter::ProvisionRequest() -{ - LOG::Log(SSDWARNING, "Provision data request (DRM:%p)" , media_drm_.GetMediaDrm()); - - jni::CJNIMediaDrmProvisionRequest request = media_drm_.GetMediaDrm()->getProvisionRequest(); - if (xbmc_jnienv()->ExceptionCheck()) - { - LOG::LogF(SSDERROR, "getProvisionRequest has raised an exception"); - xbmc_jnienv()->ExceptionClear(); - return false; - } - - std::vector provData = request.getData(); - std::string url = request.getDefaultUrl(); - - LOG::Log(SSDDEBUG, "Provision data size: %lu, url: %s", provData.size(), url.c_str()); - - std::string reqData("{\"signedRequest\":\""); - reqData += std::string(provData.data(), provData.size()); - reqData += "\"}"; - reqData = BASE64::Encode(reqData); - - void* file = GLOBAL::Host->CURLCreate(url.c_str()); - GLOBAL::Host->CURLAddOption(file, SSD_HOST::OPTION_PROTOCOL, "Content-Type", "application/json"); - GLOBAL::Host->CURLAddOption(file, SSD_HOST::OPTION_PROTOCOL, "seekable", "0"); - GLOBAL::Host->CURLAddOption(file, SSD_HOST::OPTION_PROTOCOL, "postdata", reqData.c_str()); - - if (!GLOBAL::Host->CURLOpen(file)) - { - LOG::Log(SSDERROR, "Provisioning server returned failure"); - return false; - } - provData.clear(); - char buf[8192]; - size_t nbRead; - - // read the file - while ((nbRead = GLOBAL::Host->ReadFile(file, buf, 8192)) > 0) - provData.insert(provData.end(), buf, buf + nbRead); - - media_drm_.GetMediaDrm()->provideProvisionResponse(provData); - if (xbmc_jnienv()->ExceptionCheck()) - { - LOG::LogF(SSDERROR, "provideProvisionResponse has raised an exception"); - xbmc_jnienv()->ExceptionClear(); - return false; - } - return true; -} - -bool WV_CencSingleSampleDecrypter::GetKeyRequest(std::vector& keyRequestData) -{ - jni::CJNIMediaDrmKeyRequest keyRequest = media_drm_.GetMediaDrm()->getKeyRequest( - session_id_, pssh_, "video/mp4", jni::CJNIMediaDrm::KEY_TYPE_STREAMING, optParams_); - - if (xbmc_jnienv()->ExceptionCheck()) - { - xbmc_jnienv()->ExceptionClear(); - if (!provisionRequested) - { - LOG::Log(SSDWARNING, "Key request not successful - trying provisioning"); - provisionRequested = true; - return GetKeyRequest(keyRequestData); - } - else - LOG::LogF(SSDERROR, "Key request not successful"); - return false; - } - - keyRequestData = keyRequest.getData(); - LOG::Log(SSDDEBUG, "Key request successful size: %lu", keyRequestData.size()); - return true; -} - -bool WV_CencSingleSampleDecrypter::KeyUpdateRequest(bool waitKeys, bool skipSessionMessage) -{ - if (!GetKeyRequest(keyRequestData_)) - return false; - - pssh_.clear(); - optParams_.clear(); - - if (skipSessionMessage) - return true; - - keyUpdateRequested = false; - if (!SendSessionMessage(keyRequestData_)) - return false; - - if (waitKeys && keyRequestData_.size() == 2) // Service Certificate call - { - for (unsigned int i(0); i < 100 && !keyUpdateRequested; ++i) - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - if (keyUpdateRequested) - KeyUpdateRequest(false, false); - else - { - LOG::LogF(SSDERROR, "Timeout waiting for EVENT_KEYS_REQUIRED!"); - return false; - } - } - - if (media_drm_.GetKeySystemType() != PLAYREADY) - { - int securityLevel = media_drm_.GetMediaDrm()->getSecurityLevel(session_id_); - xbmc_jnienv()->ExceptionClear(); - LOG::Log(SSDDEBUG, "Security level: %d", securityLevel); - - std::map keyStatus = media_drm_.GetMediaDrm()->queryKeyStatus(session_id_); - LOG::Log(SSDDEBUG, "Key status (%ld):", keyStatus.size()); - for (auto const& ks : keyStatus) - { - LOG::Log(SSDDEBUG, "-> %s -> %s", ks.first.c_str(), ks.second.c_str()); - } - } - return true; -} - -bool WV_CencSingleSampleDecrypter::SendSessionMessage(const std::vector &keyRequestData) -{ - std::vector blocks{StringUtils::Split(media_drm_.GetLicenseURL(), '|')}; - - if (blocks.size() != 4) - { - LOG::LogF(SSDERROR, "Wrong \"|\" blocks in license URL. Four blocks (req | header | body | " - "response) are expected in license URL"); - return false; - } - - if (GLOBAL::Host->IsDebugSaveLicense()) - { - //! @todo: with ssd_wv refactor the path must be combined with - //! UTILS::FILESYS::PathCombine - std::string debugFilePath = GLOBAL::Host->GetProfilePath(); - debugFilePath += "EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED.challenge"; - - SSD_UTILS::SaveFile(debugFilePath, keyRequestData.data()); - } - - //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(keyRequestData.data(), keyRequestData.size())}; - msgEncoded = STRING::URLEncode(msgEncoded); - blocks[0].replace(insPos - 1, 6, msgEncoded); - } - else - { - LOG::Log(SSDERROR, "Unsupported License request template (command)"); - return false; - } - } - - insPos = blocks[0].find("{HASH}"); - if (insPos != std::string::npos) - { - DIGEST::MD5 md5; - md5.Update(keyRequestData.data(), static_cast(keyRequestData.size())); - md5.Finalize(); - blocks[0].replace(insPos, 6, md5.HexDigest()); - } - - void* file = GLOBAL::Host->CURLCreate(blocks[0].c_str()); - - size_t nbRead; - std::string response, resLimit, contentType; - char buf[2048]; - - //Set our std headers - GLOBAL::Host->CURLAddOption(file, SSD_HOST::OPTION_PROTOCOL, "acceptencoding", "gzip, deflate"); - GLOBAL::Host->CURLAddOption(file, SSD_HOST::OPTION_PROTOCOL, "seekable", "0"); - - //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]); - } - GLOBAL::Host->CURLAddOption(file, SSD_HOST::OPTION_PROTOCOL, 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}")); - std::string::size_type psshPos(blocks[2].find("{PSSH}")); - - 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; - if (psshPos != std::string::npos) - psshPos -= 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(keyRequestData.data(), keyRequestData.size())}; - 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( - reinterpret_cast(keyRequestData.data()), keyRequestData.size())}; - blocks[2].replace(insPos - 1, 6, msgEncoded); - size_written = msgEncoded.size(); - } - else - { - blocks[2].replace(insPos - 1, 6, keyRequestData.data(), keyRequestData.size()); - size_written = keyRequestData.size(); - } - } - else - { - LOG::Log(SSDERROR, "Unsupported License request template (body / ?{SSM})"); - goto SSMFAIL; - } - - if (sidPos != std::string::npos && insPos < sidPos) - sidPos += size_written, sidPos -= 6; - - if (kidPos != std::string::npos && insPos < kidPos) - kidPos += size_written, kidPos -= 6; - - if (psshPos != std::string::npos && insPos < psshPos) - psshPos += size_written, psshPos -= 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(session_id_.data(), session_id_.size())}; - 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, session_id_.data(), session_id_.size()); - size_written = session_id_.size(); - } - } - else - { - LOG::Log(SSDERROR, "Unsupported License request template (body / ?{SID})"); - goto SSMFAIL; - } - } - - if (kidPos != std::string::npos && sidPos < kidPos) - kidPos += size_written, kidPos -= 6; - - if (psshPos != std::string::npos && sidPos < psshPos) - psshPos += size_written, psshPos -= 6; - - size_t kidPlaceholderLen = 6; - if (kidPos != std::string::npos) - { - 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); - kidPlaceholderLen = 5; - } - } - - if (psshPos != std::string::npos && kidPos < psshPos) - psshPos += size_written, psshPos -= kidPlaceholderLen; - - if (psshPos != std::string::npos) - { - std::string msgEncoded{BASE64::Encode(initial_pssh_.data(), initial_pssh_.size())}; - if (blocks[2][psshPos - 1] == 'B') { - msgEncoded = STRING::URLEncode(msgEncoded); - } - blocks[2].replace(psshPos - 1, 7, msgEncoded); - size_written = msgEncoded.size(); - } - - if (fullDecode) - { - std::string msgEncoded{BASE64::Encode(blocks[2])}; - if (fullDecode == 'B') - { - msgEncoded = STRING::URLEncode(msgEncoded); - } - blocks[2] = msgEncoded; - } - - if (GLOBAL::Host->IsDebugSaveLicense()) - { - //! @todo: with ssd_wv refactor the path must be combined with - //! UTILS::FILESYS::PathCombine - std::string debugFilePath = GLOBAL::Host->GetProfilePath(); - debugFilePath += "EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED.postdata"; - - SSD_UTILS::SaveFile(debugFilePath, blocks[2]); - } - } - - std::string encData{BASE64::Encode(blocks[2])}; - GLOBAL::Host->CURLAddOption(file, SSD_HOST::OPTION_PROTOCOL, "postdata", encData.c_str()); - } - - if (!GLOBAL::Host->CURLOpen(file)) - { - LOG::Log(SSDERROR, "License server returned failure"); - goto SSMFAIL; - } - - // read the file - while ((nbRead = GLOBAL::Host->ReadFile(file, buf, 1024)) > 0) - response += std::string((const char*)buf, nbRead); - - resLimit = GLOBAL::Host->CURLGetProperty(file, SSD_HOST::CURLPROPERTY::PROPERTY_HEADER, "X-Limit-Video"); - contentType = GLOBAL::Host->CURLGetProperty(file, SSD_HOST::CURLPROPERTY::PROPERTY_HEADER, "Content-Type"); - - if (!resLimit.empty()) - { - std::string::size_type posMax = resLimit.find("max="); - if (posMax != std::string::npos) - resolution_limit_ = std::atoi(resLimit.data() + (posMax + 4)); - } - - GLOBAL::Host->CloseFile(file); - file = 0; - - if (nbRead != 0) - { - LOG::LogF(SSDERROR, "Could not read full SessionMessage response"); - goto SSMFAIL; - } - else if (response.empty()) - { - LOG::LogF(SSDERROR, "Empty SessionMessage response - invalid"); - goto SSMFAIL; - } - - if (media_drm_.GetKeySystemType() == PLAYREADY && response.find("") == std::string::npos) - { - std::string::size_type dstPos(response.find("")); - std::string challenge(keyRequestData.data(), keyRequestData.size()); - std::string::size_type srcPosS(challenge.find("")); - if (dstPos != std::string::npos && srcPosS != std::string::npos) - { - LOG::Log(SSDDEBUG, "Inserting "); - std::string::size_type srcPosE(challenge.find("", srcPosS)); - if (srcPosE != std::string::npos) - response.insert(dstPos + 11, challenge.c_str() + srcPosS, srcPosE - srcPosS + 15); - } - } - - if (GLOBAL::Host->IsDebugSaveLicense()) - { - //! @todo: with ssd_wv refactor the path must be combined with - //! UTILS::FILESYS::PathCombine - std::string debugFilePath = GLOBAL::Host->GetProfilePath(); - debugFilePath += "EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED.response"; - - SSD_UTILS::SaveFile(debugFilePath, response); - } - - if (!blocks[3].empty() && (keyRequestData.size() > 2 || contentType.find("application/octet-stream") == std::string::npos)) - { - if (blocks[3][0] == 'J' || (blocks[3].size() > 1 && blocks[3][0] == 'B' && blocks[3][1] == 'J')) - { - int dataPos = 2; - - if (response.size() >= 3 && blocks[3][0] == 'B') - { - response = BASE64::Decode(response); - dataPos = 3; - } - - jsmn_parser jsn; - jsmntok_t tokens[256]; - - jsmn_init(&jsn); - int i(0), numTokens = jsmn_parse(&jsn, response.c_str(), response.size(), tokens, 256); - - std::vector jsonVals{StringUtils::Split(blocks[3].substr(dataPos), ';')}; - - // Find HDCP limit - if (jsonVals.size() > 1) - { - for (; i < numTokens; ++i) - if (tokens[i].type == JSMN_STRING && tokens[i].size == 1 && jsonVals[1].size() == static_cast(tokens[i].end - tokens[i].start) - && strncmp(response.c_str() + tokens[i].start, jsonVals[1].c_str(), tokens[i].end - tokens[i].start) == 0) - break; - if (i < numTokens) - hdcp_limit_ = atoi((response.c_str() + tokens[i + 1].start)); - } - // Find license key - if (jsonVals.size() > 0) - { - for (i = 0; i < numTokens; ++i) - if (tokens[i].type == JSMN_STRING && tokens[i].size == 1 && jsonVals[0].size() == static_cast(tokens[i].end - tokens[i].start) - && strncmp(response.c_str() + tokens[i].start, jsonVals[0].c_str(), tokens[i].end - tokens[i].start) == 0) - { - if (i + 1 < numTokens && tokens[i + 1].type == JSMN_ARRAY && tokens[i + 1].size == 1) - ++i; - break; - } - } - else - i = numTokens; - - if (i < numTokens) - { - response = response.substr(tokens[i + 1].start, tokens[i + 1].end - tokens[i + 1].start); - - if (blocks[3][dataPos - 1] == 'B') - { - response = BASE64::Decode(response); - } - } - else - { - LOG::LogF(SSDERROR, "Unable to find %s in JSON string", blocks[3].c_str() + 2); - goto SSMFAIL; - } - } - else if (blocks[3][0] == 'H' && blocks[3].size() >= 2) - { - //Find the payload - std::string::size_type payloadPos = response.find("\r\n\r\n"); - if (payloadPos != std::string::npos) - { - payloadPos += 4; - if (blocks[3][1] == 'B') - response = std::string(response.c_str() + payloadPos, response.c_str() + response.size()); - else - { - LOG::LogF(SSDERROR, "Unsupported HTTP payload data type definition"); - goto SSMFAIL; - } - } - else - { - LOG::LogF(SSDERROR, "Unable to find HTTP payload in response"); - goto SSMFAIL; - } - } - else if (blocks[3][0] == 'B' && blocks[3].size() == 1) - { - response = BASE64::Decode(response); - } - else - { - LOG::LogF(SSDERROR, "Unsupported License request template (response)"); - goto SSMFAIL; - } - } - - keySetId_ = media_drm_.GetMediaDrm()->provideKeyResponse(session_id_, std::vector(response.data(), response.data() + response.size())); - if (xbmc_jnienv()->ExceptionCheck()) - { - LOG::LogF(SSDERROR, "provideKeyResponse has raised an exception"); - xbmc_jnienv()->ExceptionClear(); - return false; - } - - if (keyRequestData.size() == 2) - media_drm_.SaveServiceCertificate(); - - LOG::Log(SSDDEBUG, "License update successful"); - return true; - -SSMFAIL: - if (file) - GLOBAL::Host->CloseFile(file); - return false; -} - -/*---------------------------------------------------------------------- -| WV_CencSingleSampleDecrypter::SetKeyId -+---------------------------------------------------------------------*/ - -AP4_Result WV_CencSingleSampleDecrypter::SetFragmentInfo(AP4_UI32 pool_id, - const AP4_UI08* key, - const AP4_UI08 nal_length_size, - AP4_DataBuffer& annexb_sps_pps, - AP4_UI32 flags, - CryptoInfo cryptoInfo) -{ - if (pool_id >= fragment_pool_.size()) - return AP4_ERROR_OUT_OF_RANGE; - - fragment_pool_[pool_id].key_ = key; - fragment_pool_[pool_id].nal_length_size_ = nal_length_size; - fragment_pool_[pool_id].annexb_sps_pps_.SetData(annexb_sps_pps.GetData(), annexb_sps_pps.GetDataSize()); - fragment_pool_[pool_id].decrypter_flags_ = flags; - - if (keyUpdateRequested) - KeyUpdateRequest(false, false); - - return AP4_SUCCESS; -} - -AP4_UI32 WV_CencSingleSampleDecrypter::AddPool() -{ - for (size_t i(0); i < fragment_pool_.size(); ++i) - if (fragment_pool_[i].nal_length_size_ == 99) - { - fragment_pool_[i].nal_length_size_ = 0; - return i; - } - fragment_pool_.push_back(FINFO()); - fragment_pool_.back().nal_length_size_ = 0; - return static_cast(fragment_pool_.size() - 1); -} - - -void WV_CencSingleSampleDecrypter::RemovePool(AP4_UI32 poolid) -{ - fragment_pool_[poolid].nal_length_size_ = 99; - fragment_pool_[poolid].key_ = nullptr; -} - -/*---------------------------------------------------------------------- -| WV_CencSingleSampleDecrypter::DecryptSampleData -+---------------------------------------------------------------------*/ -AP4_Result WV_CencSingleSampleDecrypter::DecryptSampleData(AP4_UI32 pool_id, - AP4_DataBuffer& data_in, - AP4_DataBuffer& data_out, - const AP4_UI08* iv, - unsigned int subsample_count, - const AP4_UI16* bytes_of_cleartext_data, - const AP4_UI32* bytes_of_encrypted_data) -{ - if (!media_drm_.GetMediaDrm()) - return AP4_ERROR_INVALID_STATE; - - if (data_in.GetDataSize() > 0) - { - FINFO &fragInfo(fragment_pool_[pool_id]); - - if (fragInfo.nal_length_size_ > 4) - { - LOG::LogF(SSDERROR, "Nalu length size > 4 not supported"); - return AP4_ERROR_NOT_SUPPORTED; - } - - AP4_UI16 dummyClear(0); - AP4_UI32 dummyCipher(data_in.GetDataSize()); - - if (iv) - { - if (!subsample_count) - { - subsample_count = 1; - bytes_of_cleartext_data = &dummyClear; - bytes_of_encrypted_data = &dummyCipher; - } - - data_out.SetData(reinterpret_cast(&subsample_count), sizeof(subsample_count)); - data_out.AppendData(reinterpret_cast(bytes_of_cleartext_data), subsample_count * sizeof(AP4_UI16)); - data_out.AppendData(reinterpret_cast(bytes_of_encrypted_data), subsample_count * sizeof(AP4_UI32)); - data_out.AppendData(reinterpret_cast(iv), 16); - data_out.AppendData(reinterpret_cast(fragInfo.key_), 16); - } - else - { - data_out.SetDataSize(0); - bytes_of_cleartext_data = &dummyClear; - bytes_of_encrypted_data = &dummyCipher; - } - - if (fragInfo.nal_length_size_ && (!iv || bytes_of_cleartext_data[0] > 0)) - { - //check NAL / subsample - const AP4_Byte *packet_in(data_in.GetData()), *packet_in_e(data_in.GetData() + data_in.GetDataSize()); - AP4_UI16 *clrb_out(iv ? reinterpret_cast(data_out.UseData() + sizeof(subsample_count)) : nullptr); - unsigned int nalunitcount(0), nalunitsum(0), configSize(0); - - while (packet_in < packet_in_e) - { - uint32_t nalsize(0); - for (unsigned int i(0); i < fragInfo.nal_length_size_; ++i) { nalsize = (nalsize << 8) + *packet_in++; }; - - //look if we have to inject sps / pps - if (fragInfo.annexb_sps_pps_.GetDataSize() && (*packet_in & 0x1F) != 9 /*AVC_NAL_AUD*/) - { - data_out.AppendData(fragInfo.annexb_sps_pps_.GetData(), - fragInfo.annexb_sps_pps_.GetDataSize()); - if (clrb_out) *clrb_out += fragInfo.annexb_sps_pps_.GetDataSize(); - configSize = fragInfo.annexb_sps_pps_.GetDataSize(); - fragInfo.annexb_sps_pps_.SetDataSize(0); - } - - // Annex-B Start pos - static AP4_Byte annexbStartCode[4] = {0x00, 0x00, 0x00, 0x01}; - data_out.AppendData(annexbStartCode, 4); - data_out.AppendData(packet_in, nalsize); - packet_in += nalsize; - if (clrb_out) *clrb_out += (4 - fragInfo.nal_length_size_); - ++nalunitcount; - - if (!iv) - { - nalunitsum = 0; - } - else if (nalsize + fragInfo.nal_length_size_ + nalunitsum >= *bytes_of_cleartext_data + *bytes_of_encrypted_data) - { - AP4_UI32 summedBytes(0); - do - { - summedBytes += *bytes_of_cleartext_data + *bytes_of_encrypted_data; - ++bytes_of_cleartext_data; - ++bytes_of_encrypted_data; - ++clrb_out; - --subsample_count; - } while (subsample_count && nalsize + fragInfo.nal_length_size_ + nalunitsum > summedBytes); - - if (nalsize + fragInfo.nal_length_size_ + nalunitsum > summedBytes) - { - LOG::LogF(SSDERROR, "NAL Unit exceeds subsample definition (nls: %u) %u -> %u ", - static_cast(fragInfo.nal_length_size_), - static_cast(nalsize + fragInfo.nal_length_size_ + nalunitsum), - summedBytes); - return AP4_ERROR_NOT_SUPPORTED; - } - nalunitsum = 0; - } - else - nalunitsum += nalsize + fragInfo.nal_length_size_; - } - if (packet_in != packet_in_e || subsample_count) - { - LOG::LogF(SSDERROR, "NAL Unit definition incomplete (nls: %d) %d -> %u ", - fragInfo.nal_length_size_, (int)(packet_in_e - packet_in), subsample_count); - return AP4_ERROR_NOT_SUPPORTED; - } - } - else - { - data_out.AppendData(data_in.GetData(), data_in.GetDataSize()); - fragInfo.annexb_sps_pps_.SetDataSize(0); - } - } - else - data_out.SetDataSize(0); - return AP4_SUCCESS; -} - -/***********************************************************************************/ - -class WVDecrypter : public SSD_DECRYPTER, public jni::CJNIMediaDrmOnEventListener -{ -public: - WVDecrypter(const CJNIClassLoader *classLoader) - : CJNIMediaDrmOnEventListener(classLoader) - , key_system_(NONE) - , cdmsession_(nullptr) - { -#ifdef DRMTHREAD - std::unique_lock lk(jniMutex_); - jniWorker = new std::thread(&WVDecrypter::JNIThread, this, reinterpret_cast(GLOBAL::Host->GetJNIEnv())); - jniCondition_.wait(lk); -#endif - if (xbmc_jnienv()->ExceptionCheck()) - { - LOG::LogF(SSDERROR, "Failed to load MediaDrmOnEventListener"); - xbmc_jnienv()->ExceptionDescribe(); - xbmc_jnienv()->ExceptionClear(); - return; - } - LOG::Log(SSDDEBUG, "WVDecrypter constructed"); - }; - - ~WVDecrypter() - { - delete cdmsession_; - cdmsession_ = nullptr; - -#ifdef DRMTHREAD - jniCondition_.notify_one(); - jniWorker->join(); - delete jniWorker; -#endif - - LOG::Log(SSDDEBUG, "WVDecrypter destructed"); - }; - -#ifdef DRMTHREAD - void JNIThread(JavaVM *vm) - { - jniCondition_.notify_one(); - std::unique_lock lk(jniMutex_); - jniCondition_.wait(lk); - - LOG::Log(SSDDEBUG, "JNI thread terminated"); - } -#endif - - virtual const char *SelectKeySytem(const char* keySystem) override - { - LOG::Log(SSDDEBUG, "Key system request: %s", keySystem); - if (strcmp(keySystem, "com.widevine.alpha") == 0) - { - key_system_ = WIDEVINE; - return "urn:uuid:EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED"; - } - else if (strcmp(keySystem, "com.huawei.wiseplay") == 0) - { - key_system_ = WISEPLAY; - return "urn:uuid:3D5E6D35-9B9A-41E8-B843-DD3C6E72C42C"; - } - else if (strcmp(keySystem, "com.microsoft.playready") == 0) - { - key_system_ = PLAYREADY; - return "urn:uuid:9A04F079-9840-4286-AB92-E65BE0885F95"; - } - else - return nullptr; - } - - virtual bool OpenDRMSystem(const char *licenseURL, const AP4_DataBuffer &serverCertificate, const uint8_t config) override - { - if (key_system_ == NONE) - return false; - - cdmsession_ = new WV_DRM(key_system_, licenseURL, serverCertificate, this); - - return cdmsession_->GetMediaDrm(); - } - - virtual Adaptive_CencSingleSampleDecrypter* CreateSingleSampleDecrypter( - AP4_DataBuffer& pssh, - const char* optionalKeyParameter, - std::string_view defaultkeyid, - bool skipSessionMessage, - CryptoMode cryptoMode) override - { - WV_CencSingleSampleDecrypter *decrypter = new WV_CencSingleSampleDecrypter(*cdmsession_, pssh, optionalKeyParameter, defaultkeyid); - - { - std::lock_guard lk(decrypterListMutex); - decrypterList.push_back(decrypter); - } - - if (!(*decrypter->GetSessionId() && decrypter->StartSession(skipSessionMessage))) - { - DestroySingleSampleDecrypter(decrypter); - return nullptr; - } - return decrypter; - } - - virtual void DestroySingleSampleDecrypter(Adaptive_CencSingleSampleDecrypter* decrypter) override - { - if (decrypter) - { - std::vector::const_iterator res = std::find(decrypterList.begin(),decrypterList.end(), decrypter); - if (res != decrypterList.end()) - { - std::lock_guard lk(decrypterListMutex); - decrypterList.erase(res); - } - delete static_cast(decrypter); - } - } - - virtual void GetCapabilities(Adaptive_CencSingleSampleDecrypter* decrypter, - const uint8_t* keyid, - uint32_t media, - SSD_DECRYPTER::SSD_CAPS& caps) override - { - if (decrypter) - static_cast(decrypter)->GetCapabilities(keyid,media,caps); - else - caps = { 0, 0, 0}; - } - - virtual bool HasLicenseKey(Adaptive_CencSingleSampleDecrypter* decrypter, - const uint8_t* keyid) override - { - if (decrypter) - return static_cast(decrypter)->HasLicenseKey(keyid); - return false; - } - - virtual std::string GetChallengeB64Data(Adaptive_CencSingleSampleDecrypter* decrypter) override - { - if (!decrypter) - return ""; - - std::vector challengeData = static_cast(decrypter)->GetChallengeData(); - return BASE64::Encode(challengeData.data(), challengeData.size()); - } - - virtual bool HasCdmSession() override - { - return cdmsession_ != nullptr; - } - - virtual bool OpenVideoDecoder(Adaptive_CencSingleSampleDecrypter* decrypter, - const SSD_VIDEOINITDATA* initData) override - { - return false; - } - - virtual SSD_DECODE_RETVAL DecryptAndDecodeVideo(void* hostInstance, SSD_SAMPLE* sample) override - { - return VC_ERROR; - } - - virtual SSD_DECODE_RETVAL VideoFrameDataToPicture(void* hostInstance, - SSD_PICTURE* picture) override - { - return VC_ERROR; - } - - virtual void ResetVideo() override - { - } - - virtual void onEvent(const jni::CJNIMediaDrm &mediaDrm, const std::vector &sessionId, int event, int extra, const std::vector &data) override - { - LOG::LogF(SSDDEBUG, "%d arrived, #decrypter: %lu", event, decrypterList.size()); - //we have only one DRM system running (cdmsession_) so there is no need to compare mediaDrm - std::lock_guard lk(decrypterListMutex); - for (std::vector::iterator b(decrypterList.begin()), e(decrypterList.end()); b != e; ++b) - if (sessionId.empty() || (*b)->GetSessionIdRaw() == sessionId) - { - switch (event) - { - case jni::CJNIMediaDrm::EVENT_KEY_REQUIRED: - (*b)->RequestNewKeys(); - break; - default:; - } - } - else - { - LOG::LogF(SSDDEBUG, "Session does not match: sizes: %lu -> %lu", sessionId.size(), (*b)->GetSessionIdRaw().size()); - } - } - -private: - WV_KEYSYSTEM key_system_; - WV_DRM *cdmsession_; - std::vector decrypterList; - std::mutex decrypterListMutex; -#ifdef DRMTHREAD - std::mutex jniMutex_; - std::condition_variable jniCondition_; - std::thread *jniWorker; -#endif -}; - -JNIEnv* xbmc_jnienv() -{ - return static_cast(GLOBAL::Host->GetJNIEnv()); -} - -extern "C" { - -#ifdef _WIN32 -#define MODULE_API __declspec(dllexport) -#else -#define MODULE_API -#endif - - CJNIClassLoader *classLoader; - - SSD_DECRYPTER MODULE_API *CreateDecryptorInstance(class SSD_HOST *h, uint32_t host_version) - { - if (host_version != SSD_HOST::version) - return nullptr; - - GLOBAL::Host = h; - - CJNIBase::SetSDKVersion(GLOBAL::Host->GetSDKVersion()); - CJNIBase::SetBaseClassName(GLOBAL::Host->GetClassName()); - - LOG::Log(SSDDEBUG, "WVDecrypter JNI, SDK version: %d, class: %s", CJNIBase::GetSDKVersion(), - CJNIBase::GetBaseClassName().c_str()); - - const char *apkEnv = getenv("XBMC_ANDROID_APK"); - if (!apkEnv) - apkEnv = getenv("KODI_ANDROID_APK"); - - if (!apkEnv) - return nullptr; - - std::string apkPath = apkEnv; - - classLoader = new CJNIClassLoader(apkPath); - if (xbmc_jnienv()->ExceptionCheck()) - { - LOG::LogF(SSDERROR, "Failed to create JNI::ClassLoader"); - xbmc_jnienv()->ExceptionDescribe(); - xbmc_jnienv()->ExceptionClear(); - - delete classLoader, classLoader = nullptr; - - return nullptr; - } - return new WVDecrypter(classLoader); - }; - - void MODULE_API DeleteDecryptorInstance(class SSD_DECRYPTER *d) - { - delete classLoader, classLoader = nullptr; - delete static_cast(d); - } -};