| // Copyright (c) 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "media/base/android/media_drm_bridge.h" |
| |
| #include <stddef.h> |
| #include <sys/system_properties.h> |
| #include <algorithm> |
| #include <memory> |
| #include <utility> |
| |
| #include "base/android/build_info.h" |
| #include "base/android/jni_array.h" |
| #include "base/android/jni_string.h" |
| #include "base/bind.h" |
| #include "base/callback_helpers.h" |
| #include "base/cxx17_backports.h" |
| #include "base/feature_list.h" |
| #include "base/location.h" |
| #include "base/logging.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/single_thread_task_runner.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/sys_byteorder.h" |
| #include "base/system/sys_info.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "media/base/android/android_util.h" |
| #include "media/base/android/media_codec_util.h" |
| #include "media/base/android/media_drm_bridge_client.h" |
| #include "media/base/android/media_drm_bridge_delegate.h" |
| #include "media/base/android/media_jni_headers/MediaDrmBridge_jni.h" |
| #include "media/base/cdm_key_information.h" |
| #include "media/base/logging_override_if_enabled.h" |
| #include "media/base/media_drm_key_type.h" |
| #include "media/base/media_switches.h" |
| #include "media/base/provision_fetcher.h" |
| #include "third_party/widevine/cdm/widevine_cdm_common.h" |
| |
| using base::android::AttachCurrentThread; |
| using base::android::ConvertJavaStringToUTF8; |
| using base::android::ConvertUTF8ToJavaString; |
| using base::android::JavaByteArrayToByteVector; |
| using base::android::JavaByteArrayToString; |
| using base::android::JavaObjectArrayReader; |
| using base::android::JavaParamRef; |
| using base::android::ScopedJavaGlobalRef; |
| using base::android::ScopedJavaLocalRef; |
| using base::android::ToJavaByteArray; |
| |
| namespace media { |
| |
| namespace { |
| |
| using CreateMediaDrmBridgeCB = base::OnceCallback<scoped_refptr<MediaDrmBridge>( |
| const std::string& /* origin_id */)>; |
| |
| // These must be in sync with Android MediaDrm REQUEST_TYPE_XXX constants! |
| // https://developer.android.com/reference/android/media/MediaDrm.KeyRequest.html |
| enum class RequestType : uint32_t { |
| REQUEST_TYPE_INITIAL = 0, |
| REQUEST_TYPE_RENEWAL = 1, |
| REQUEST_TYPE_RELEASE = 2, |
| }; |
| |
| // These must be in sync with Android MediaDrm KEY_STATUS_XXX constants: |
| // https://developer.android.com/reference/android/media/MediaDrm.KeyStatus.html |
| enum class KeyStatus : uint32_t { |
| KEY_STATUS_USABLE = 0, |
| KEY_STATUS_EXPIRED = 1, |
| KEY_STATUS_OUTPUT_NOT_ALLOWED = 2, |
| KEY_STATUS_PENDING = 3, |
| KEY_STATUS_INTERNAL_ERROR = 4, |
| KEY_STATUS_USABLE_IN_FUTURE = 5, // Added in API level 29. |
| }; |
| |
| const uint8_t kWidevineUuid[16] = { |
| 0xED, 0xEF, 0x8B, 0xA9, 0x79, 0xD6, 0x4A, 0xCE, // |
| 0xA3, 0xC8, 0x27, 0xDC, 0xD5, 0x1D, 0x21, 0xED}; |
| |
| // Convert |init_data_type| to a string supported by MediaDRM. |
| // "audio"/"video" does not matter, so use "video". |
| std::string ConvertInitDataType(media::EmeInitDataType init_data_type) { |
| // TODO(jrummell/xhwang): EME init data types like "webm" and "cenc" are |
| // supported in API level >=21 for Widevine key system. Switch to use those |
| // strings when they are officially supported in Android for all key systems. |
| switch (init_data_type) { |
| case media::EmeInitDataType::WEBM: |
| return "video/webm"; |
| case media::EmeInitDataType::CENC: |
| return "video/mp4"; |
| case media::EmeInitDataType::KEYIDS: |
| return "keyids"; |
| default: |
| NOTREACHED(); |
| return "unknown"; |
| } |
| } |
| |
| // Convert CdmSessionType to MediaDrmKeyType supported by MediaDrm. |
| MediaDrmKeyType ConvertCdmSessionType(CdmSessionType session_type) { |
| switch (session_type) { |
| case CdmSessionType::kTemporary: |
| return MediaDrmKeyType::STREAMING; |
| case CdmSessionType::kPersistentLicense: |
| return MediaDrmKeyType::OFFLINE; |
| |
| default: |
| LOG(WARNING) << "Unsupported session type " |
| << static_cast<int>(session_type); |
| return MediaDrmKeyType::STREAMING; |
| } |
| } |
| |
| CdmMessageType GetMessageType(RequestType request_type) { |
| switch (request_type) { |
| case RequestType::REQUEST_TYPE_INITIAL: |
| return CdmMessageType::LICENSE_REQUEST; |
| case RequestType::REQUEST_TYPE_RENEWAL: |
| return CdmMessageType::LICENSE_RENEWAL; |
| case RequestType::REQUEST_TYPE_RELEASE: |
| return CdmMessageType::LICENSE_RELEASE; |
| } |
| |
| NOTREACHED(); |
| return CdmMessageType::LICENSE_REQUEST; |
| } |
| |
| CdmKeyInformation::KeyStatus ConvertKeyStatus(KeyStatus key_status, |
| bool is_key_release) { |
| switch (key_status) { |
| case KeyStatus::KEY_STATUS_USABLE: |
| return CdmKeyInformation::USABLE; |
| case KeyStatus::KEY_STATUS_EXPIRED: |
| return is_key_release ? CdmKeyInformation::RELEASED |
| : CdmKeyInformation::EXPIRED; |
| case KeyStatus::KEY_STATUS_OUTPUT_NOT_ALLOWED: |
| return CdmKeyInformation::OUTPUT_RESTRICTED; |
| case KeyStatus::KEY_STATUS_PENDING: |
| // On pre-Q versions of Android, 'status-pending' really means "usable in |
| // the future". Translate this to 'expired' as that's the only status that |
| // makes sense in this case. Starting with Android Q, 'status-pending' |
| // means what you expect. See crbug.com/889272 for explanation. |
| // TODO(jrummell): "KEY_STATUS_PENDING" should probably be renamed to |
| // "STATUS_PENDING". |
| return (base::android::BuildInfo::GetInstance()->sdk_int() <= |
| base::android::SDK_VERSION_P) |
| ? CdmKeyInformation::EXPIRED |
| : CdmKeyInformation::KEY_STATUS_PENDING; |
| case KeyStatus::KEY_STATUS_INTERNAL_ERROR: |
| return CdmKeyInformation::INTERNAL_ERROR; |
| case KeyStatus::KEY_STATUS_USABLE_IN_FUTURE: |
| // This was added in Android Q. |
| // https://developer.android.com/reference/android/media/MediaDrm.KeyStatus.html#STATUS_USABLE_IN_FUTURE |
| // notes this happens "because the start time is in the future." There is |
| // no matching EME status, so returning EXPIRED as the closest match. |
| return CdmKeyInformation::EXPIRED; |
| } |
| |
| NOTREACHED(); |
| return CdmKeyInformation::INTERNAL_ERROR; |
| } |
| |
| class KeySystemManager { |
| public: |
| KeySystemManager(); |
| UUID GetUUID(const std::string& key_system); |
| std::vector<std::string> GetPlatformKeySystemNames(); |
| |
| private: |
| using KeySystemUuidMap = MediaDrmBridgeClient::KeySystemUuidMap; |
| |
| KeySystemUuidMap key_system_uuid_map_; |
| |
| DISALLOW_COPY_AND_ASSIGN(KeySystemManager); |
| }; |
| |
| KeySystemManager::KeySystemManager() { |
| // Widevine is always supported in Android. |
| key_system_uuid_map_[kWidevineKeySystem] = |
| UUID(kWidevineUuid, kWidevineUuid + base::size(kWidevineUuid)); |
| MediaDrmBridgeClient* client = GetMediaDrmBridgeClient(); |
| if (client) |
| client->AddKeySystemUUIDMappings(&key_system_uuid_map_); |
| } |
| |
| UUID KeySystemManager::GetUUID(const std::string& key_system) { |
| KeySystemUuidMap::iterator it = key_system_uuid_map_.find(key_system); |
| if (it == key_system_uuid_map_.end()) |
| return UUID(); |
| return it->second; |
| } |
| |
| std::vector<std::string> KeySystemManager::GetPlatformKeySystemNames() { |
| std::vector<std::string> key_systems; |
| for (KeySystemUuidMap::iterator it = key_system_uuid_map_.begin(); |
| it != key_system_uuid_map_.end(); ++it) { |
| // Rule out the key system handled by Chrome explicitly. |
| if (it->first != kWidevineKeySystem) |
| key_systems.push_back(it->first); |
| } |
| return key_systems; |
| } |
| |
| KeySystemManager* GetKeySystemManager() { |
| static KeySystemManager* ksm = new KeySystemManager(); |
| return ksm; |
| } |
| |
| // Checks whether |key_system| is supported with |container_mime_type|. Only |
| // checks |key_system| support if |container_mime_type| is empty. |
| // TODO(xhwang): The |container_mime_type| is not the same as contentType in |
| // the EME spec. Revisit this once the spec issue with initData type is |
| // resolved. |
| bool IsKeySystemSupportedWithTypeImpl(const std::string& key_system, |
| const std::string& container_mime_type) { |
| DCHECK(MediaDrmBridge::IsAvailable()); |
| |
| if (key_system.empty()) { |
| NOTREACHED(); |
| return false; |
| } |
| |
| UUID scheme_uuid = GetKeySystemManager()->GetUUID(key_system); |
| if (scheme_uuid.empty()) { |
| DVLOG(1) << "Cannot get UUID for key system " << key_system; |
| return false; |
| } |
| |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jbyteArray> j_scheme_uuid = |
| base::android::ToJavaByteArray(env, &scheme_uuid[0], scheme_uuid.size()); |
| ScopedJavaLocalRef<jstring> j_container_mime_type = |
| ConvertUTF8ToJavaString(env, container_mime_type); |
| bool supported = Java_MediaDrmBridge_isCryptoSchemeSupported( |
| env, j_scheme_uuid, j_container_mime_type); |
| DVLOG_IF(1, !supported) << "Crypto scheme not supported for " << key_system |
| << " with " << container_mime_type; |
| return supported; |
| } |
| |
| MediaDrmBridge::SecurityLevel GetSecurityLevelFromString( |
| const std::string& security_level_str) { |
| if (0 == security_level_str.compare("L1")) |
| return MediaDrmBridge::SECURITY_LEVEL_1; |
| if (0 == security_level_str.compare("L3")) |
| return MediaDrmBridge::SECURITY_LEVEL_3; |
| DCHECK(security_level_str.empty()); |
| return MediaDrmBridge::SECURITY_LEVEL_DEFAULT; |
| } |
| |
| // Do not change the return values as they are part of Android MediaDrm API for |
| // Widevine. |
| std::string GetSecurityLevelString( |
| MediaDrmBridge::SecurityLevel security_level) { |
| switch (security_level) { |
| case MediaDrmBridge::SECURITY_LEVEL_DEFAULT: |
| return ""; |
| case MediaDrmBridge::SECURITY_LEVEL_1: |
| return "L1"; |
| case MediaDrmBridge::SECURITY_LEVEL_3: |
| return "L3"; |
| } |
| return ""; |
| } |
| |
| int GetFirstApiLevel() { |
| JNIEnv* env = AttachCurrentThread(); |
| int first_api_level = Java_MediaDrmBridge_getFirstApiLevel(env); |
| return first_api_level; |
| } |
| |
| } // namespace |
| |
| // MediaDrm is not generally usable without MediaCodec. Thus, both the MediaDrm |
| // APIs and MediaCodec APIs must be enabled and not blocked. |
| // static |
| bool MediaDrmBridge::IsAvailable() { |
| return MediaCodecUtil::IsMediaCodecAvailable(); |
| } |
| |
| // static |
| bool MediaDrmBridge::IsKeySystemSupported(const std::string& key_system) { |
| if (!MediaDrmBridge::IsAvailable()) |
| return false; |
| |
| return IsKeySystemSupportedWithTypeImpl(key_system, ""); |
| } |
| |
| // static |
| bool MediaDrmBridge::IsPerOriginProvisioningSupported() { |
| return base::android::BuildInfo::GetInstance()->sdk_int() >= |
| base::android::SDK_VERSION_MARSHMALLOW; |
| } |
| |
| // static |
| bool MediaDrmBridge::IsPerApplicationProvisioningSupported() { |
| // Start by checking "ro.product.first_api_level", which may not exist. |
| // If it is non-zero, then it is the API level. |
| static int first_api_level = GetFirstApiLevel(); |
| DVLOG(1) << "first_api_level = " << first_api_level; |
| if (first_api_level >= base::android::SDK_VERSION_OREO) |
| return true; |
| |
| // If "ro.product.first_api_level" does not match, then check build number. |
| DVLOG(1) << "api_level = " |
| << base::android::BuildInfo::GetInstance()->sdk_int(); |
| return base::android::BuildInfo::GetInstance()->sdk_int() >= |
| base::android::SDK_VERSION_OREO; |
| } |
| |
| // static |
| bool MediaDrmBridge::IsPersistentLicenseTypeSupported( |
| const std::string& /* key_system */) { |
| // TODO(yucliu): Check |key_system| if persistent license is supported by |
| // MediaDrm. |
| return MediaDrmBridge::IsAvailable() && |
| // In development. See http://crbug.com/493521 |
| base::FeatureList::IsEnabled(kMediaDrmPersistentLicense) && |
| IsPerOriginProvisioningSupported(); |
| } |
| |
| // static |
| bool MediaDrmBridge::IsKeySystemSupportedWithType( |
| const std::string& key_system, |
| const std::string& container_mime_type) { |
| DCHECK(!container_mime_type.empty()) << "Call IsKeySystemSupported instead"; |
| |
| if (!MediaDrmBridge::IsAvailable()) |
| return false; |
| |
| return IsKeySystemSupportedWithTypeImpl(key_system, container_mime_type); |
| } |
| |
| // static |
| std::vector<std::string> MediaDrmBridge::GetPlatformKeySystemNames() { |
| if (!MediaDrmBridge::IsAvailable()) |
| return std::vector<std::string>(); |
| |
| return GetKeySystemManager()->GetPlatformKeySystemNames(); |
| } |
| |
| // static |
| std::vector<uint8_t> MediaDrmBridge::GetUUID(const std::string& key_system) { |
| return GetKeySystemManager()->GetUUID(key_system); |
| } |
| |
| // static |
| scoped_refptr<MediaDrmBridge> MediaDrmBridge::CreateInternal( |
| const std::vector<uint8_t>& scheme_uuid, |
| const std::string& origin_id, |
| SecurityLevel security_level, |
| bool requires_media_crypto, |
| std::unique_ptr<MediaDrmStorageBridge> storage, |
| CreateFetcherCB create_fetcher_cb, |
| const SessionMessageCB& session_message_cb, |
| const SessionClosedCB& session_closed_cb, |
| const SessionKeysChangeCB& session_keys_change_cb, |
| const SessionExpirationUpdateCB& session_expiration_update_cb) { |
| // All paths requires the MediaDrmApis. |
| DCHECK(!scheme_uuid.empty()); |
| |
| // TODO(crbug.com/917527): Check that |origin_id| is specified on devices |
| // that support it. |
| |
| scoped_refptr<MediaDrmBridge> media_drm_bridge(new MediaDrmBridge( |
| scheme_uuid, origin_id, security_level, requires_media_crypto, |
| std::move(storage), std::move(create_fetcher_cb), session_message_cb, |
| session_closed_cb, session_keys_change_cb, session_expiration_update_cb)); |
| |
| if (!media_drm_bridge->j_media_drm_) |
| return nullptr; |
| |
| return media_drm_bridge; |
| } |
| |
| // static |
| scoped_refptr<MediaDrmBridge> MediaDrmBridge::CreateWithoutSessionSupport( |
| const std::string& key_system, |
| const std::string& origin_id, |
| SecurityLevel security_level, |
| CreateFetcherCB create_fetcher_cb) { |
| DVLOG(1) << __func__; |
| |
| UUID scheme_uuid = GetKeySystemManager()->GetUUID(key_system); |
| if (scheme_uuid.empty()) |
| return nullptr; |
| |
| // When created without session support, MediaCrypto is not needed. |
| const bool requires_media_crypto = false; |
| |
| return CreateInternal( |
| scheme_uuid, origin_id, security_level, requires_media_crypto, |
| std::make_unique<MediaDrmStorageBridge>(), std::move(create_fetcher_cb), |
| SessionMessageCB(), SessionClosedCB(), SessionKeysChangeCB(), |
| SessionExpirationUpdateCB()); |
| } |
| |
| void MediaDrmBridge::SetServerCertificate( |
| const std::vector<uint8_t>& certificate, |
| std::unique_ptr<media::SimpleCdmPromise> promise) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| DVLOG(2) << __func__ << "(" << certificate.size() << " bytes)"; |
| |
| DCHECK(!certificate.empty()); |
| |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jbyteArray> j_certificate = base::android::ToJavaByteArray( |
| env, certificate.data(), certificate.size()); |
| if (Java_MediaDrmBridge_setServerCertificate(env, j_media_drm_, |
| j_certificate)) { |
| promise->resolve(); |
| } else { |
| promise->reject(CdmPromise::Exception::TYPE_ERROR, 0, |
| "Set server certificate failed."); |
| } |
| } |
| |
| void MediaDrmBridge::CreateSessionAndGenerateRequest( |
| CdmSessionType session_type, |
| media::EmeInitDataType init_data_type, |
| const std::vector<uint8_t>& init_data, |
| std::unique_ptr<media::NewSessionCdmPromise> promise) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| DVLOG(2) << __func__; |
| |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jbyteArray> j_init_data; |
| ScopedJavaLocalRef<jobjectArray> j_optional_parameters; |
| |
| MediaDrmBridgeClient* client = GetMediaDrmBridgeClient(); |
| if (client) { |
| MediaDrmBridgeDelegate* delegate = |
| client->GetMediaDrmBridgeDelegate(scheme_uuid_); |
| if (delegate) { |
| std::vector<uint8_t> init_data_from_delegate; |
| std::vector<std::string> optional_parameters_from_delegate; |
| if (!delegate->OnCreateSession(init_data_type, init_data, |
| &init_data_from_delegate, |
| &optional_parameters_from_delegate)) { |
| promise->reject(CdmPromise::Exception::TYPE_ERROR, 0, |
| "Invalid init data."); |
| return; |
| } |
| if (!init_data_from_delegate.empty()) { |
| j_init_data = |
| base::android::ToJavaByteArray(env, init_data_from_delegate.data(), |
| init_data_from_delegate.size()); |
| } |
| if (!optional_parameters_from_delegate.empty()) { |
| j_optional_parameters = base::android::ToJavaArrayOfStrings( |
| env, optional_parameters_from_delegate); |
| } |
| } |
| } |
| |
| if (!j_init_data) { |
| j_init_data = |
| base::android::ToJavaByteArray(env, init_data.data(), init_data.size()); |
| } |
| |
| ScopedJavaLocalRef<jstring> j_mime = |
| ConvertUTF8ToJavaString(env, ConvertInitDataType(init_data_type)); |
| uint32_t key_type = |
| static_cast<uint32_t>(ConvertCdmSessionType(session_type)); |
| uint32_t promise_id = cdm_promise_adapter_.SavePromise(std::move(promise)); |
| Java_MediaDrmBridge_createSessionFromNative( |
| env, j_media_drm_, j_init_data, j_mime, key_type, j_optional_parameters, |
| promise_id); |
| } |
| |
| void MediaDrmBridge::LoadSession( |
| CdmSessionType session_type, |
| const std::string& session_id, |
| std::unique_ptr<media::NewSessionCdmPromise> promise) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| DVLOG(2) << __func__; |
| |
| // Key system is not used, so just pass an empty string here. |
| DCHECK(IsPersistentLicenseTypeSupported("")); |
| |
| if (session_type != CdmSessionType::kPersistentLicense) { |
| promise->reject( |
| CdmPromise::Exception::NOT_SUPPORTED_ERROR, 0, |
| "LoadSession() is only supported for 'persistent-license'."); |
| return; |
| } |
| |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jbyteArray> j_session_id = |
| ToJavaByteArray(env, session_id); |
| uint32_t promise_id = cdm_promise_adapter_.SavePromise(std::move(promise)); |
| Java_MediaDrmBridge_loadSession(env, j_media_drm_, j_session_id, promise_id); |
| } |
| |
| void MediaDrmBridge::UpdateSession( |
| const std::string& session_id, |
| const std::vector<uint8_t>& response, |
| std::unique_ptr<media::SimpleCdmPromise> promise) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| DVLOG(2) << __func__; |
| |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jbyteArray> j_response = |
| base::android::ToJavaByteArray(env, response.data(), response.size()); |
| ScopedJavaLocalRef<jbyteArray> j_session_id = |
| ToJavaByteArray(env, session_id); |
| uint32_t promise_id = cdm_promise_adapter_.SavePromise(std::move(promise)); |
| Java_MediaDrmBridge_updateSession(env, j_media_drm_, j_session_id, j_response, |
| promise_id); |
| } |
| |
| void MediaDrmBridge::CloseSession( |
| const std::string& session_id, |
| std::unique_ptr<media::SimpleCdmPromise> promise) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| DVLOG(2) << __func__; |
| |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jbyteArray> j_session_id = |
| ToJavaByteArray(env, session_id); |
| uint32_t promise_id = cdm_promise_adapter_.SavePromise(std::move(promise)); |
| Java_MediaDrmBridge_closeSession(env, j_media_drm_, j_session_id, promise_id); |
| } |
| |
| void MediaDrmBridge::RemoveSession( |
| const std::string& session_id, |
| std::unique_ptr<media::SimpleCdmPromise> promise) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| DVLOG(2) << __func__; |
| |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jbyteArray> j_session_id = |
| ToJavaByteArray(env, session_id); |
| uint32_t promise_id = cdm_promise_adapter_.SavePromise(std::move(promise)); |
| Java_MediaDrmBridge_removeSession(env, j_media_drm_, j_session_id, |
| promise_id); |
| } |
| |
| CdmContext* MediaDrmBridge::GetCdmContext() { |
| DVLOG(2) << __func__; |
| return this; |
| } |
| |
| void MediaDrmBridge::DeleteOnCorrectThread() const { |
| DVLOG(1) << __func__; |
| |
| if (!task_runner_->BelongsToCurrentThread()) { |
| // When DeleteSoon returns false, |this| will be leaked, which is okay. |
| task_runner_->DeleteSoon(FROM_HERE, this); |
| } else { |
| delete this; |
| } |
| } |
| |
| std::unique_ptr<CallbackRegistration> MediaDrmBridge::RegisterEventCB( |
| EventCB event_cb) { |
| return event_callbacks_.Register(std::move(event_cb)); |
| } |
| |
| MediaCryptoContext* MediaDrmBridge::GetMediaCryptoContext() { |
| DVLOG(2) << __func__; |
| return &media_crypto_context_; |
| } |
| |
| bool MediaDrmBridge::IsSecureCodecRequired() { |
| DCHECK(IsAvailable()); |
| |
| // For Widevine, this depends on the security level. |
| // TODO(xhwang): This is specific to Widevine. See http://crbug.com/459400. |
| // To fix it, we could call MediaCrypto.requiresSecureDecoderComponent(). |
| // See http://crbug.com/727918. |
| if (std::equal(scheme_uuid_.begin(), scheme_uuid_.end(), kWidevineUuid)) |
| return SECURITY_LEVEL_1 == GetSecurityLevel(); |
| |
| // For other key systems, assume true. |
| return true; |
| } |
| |
| void MediaDrmBridge::Provision( |
| base::OnceCallback<void(bool)> provisioning_complete_cb) { |
| DVLOG(1) << __func__; |
| DCHECK(provisioning_complete_cb); |
| DCHECK(!provisioning_complete_cb_); |
| provisioning_complete_cb_ = std::move(provisioning_complete_cb); |
| |
| JNIEnv* env = AttachCurrentThread(); |
| Java_MediaDrmBridge_provision(env, j_media_drm_); |
| } |
| |
| void MediaDrmBridge::Unprovision() { |
| DVLOG(1) << __func__; |
| |
| JNIEnv* env = AttachCurrentThread(); |
| Java_MediaDrmBridge_unprovision(env, j_media_drm_); |
| } |
| |
| void MediaDrmBridge::ResolvePromise(uint32_t promise_id) { |
| DVLOG(2) << __func__; |
| cdm_promise_adapter_.ResolvePromise(promise_id); |
| } |
| |
| void MediaDrmBridge::ResolvePromiseWithSession(uint32_t promise_id, |
| const std::string& session_id) { |
| DVLOG(2) << __func__; |
| cdm_promise_adapter_.ResolvePromise(promise_id, session_id); |
| } |
| |
| void MediaDrmBridge::RejectPromise(uint32_t promise_id, |
| const std::string& error_message) { |
| DVLOG(2) << __func__; |
| cdm_promise_adapter_.RejectPromise( |
| promise_id, CdmPromise::Exception::NOT_SUPPORTED_ERROR, 0, error_message); |
| } |
| |
| void MediaDrmBridge::SetMediaCryptoReadyCB( |
| MediaCryptoReadyCB media_crypto_ready_cb) { |
| if (!task_runner_->BelongsToCurrentThread()) { |
| task_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&MediaDrmBridge::SetMediaCryptoReadyCB, |
| weak_factory_.GetWeakPtr(), |
| std::move(media_crypto_ready_cb))); |
| return; |
| } |
| |
| DVLOG(1) << __func__; |
| |
| if (!media_crypto_ready_cb) { |
| media_crypto_ready_cb_.Reset(); |
| return; |
| } |
| |
| DCHECK(!media_crypto_ready_cb_); |
| media_crypto_ready_cb_ = std::move(media_crypto_ready_cb); |
| |
| if (!j_media_crypto_) |
| return; |
| |
| std::move(media_crypto_ready_cb_) |
| .Run(CreateJavaObjectPtr(j_media_crypto_->obj()), |
| IsSecureCodecRequired()); |
| } |
| |
| //------------------------------------------------------------------------------ |
| // The following OnXxx functions are called from Java. The implementation must |
| // only do minimal work and then post tasks to avoid reentrancy issues. |
| |
| void MediaDrmBridge::OnMediaCryptoReady( |
| JNIEnv* env, |
| const JavaParamRef<jobject>& j_media_drm, |
| const JavaParamRef<jobject>& j_media_crypto) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| DVLOG(1) << __func__; |
| |
| task_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&MediaDrmBridge::NotifyMediaCryptoReady, |
| weak_factory_.GetWeakPtr(), |
| CreateJavaObjectPtr(j_media_crypto.obj()))); |
| } |
| |
| void MediaDrmBridge::OnProvisionRequest( |
| JNIEnv* env, |
| const JavaParamRef<jobject>& j_media_drm, |
| const JavaParamRef<jstring>& j_default_url, |
| const JavaParamRef<jbyteArray>& j_request_data) { |
| DVLOG(1) << __func__; |
| |
| std::string request_data; |
| JavaByteArrayToString(env, j_request_data, &request_data); |
| std::string default_url; |
| ConvertJavaStringToUTF8(env, j_default_url, &default_url); |
| task_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&MediaDrmBridge::SendProvisioningRequest, |
| weak_factory_.GetWeakPtr(), GURL(default_url), |
| std::move(request_data))); |
| } |
| |
| void MediaDrmBridge::OnProvisioningComplete( |
| JNIEnv* env, |
| const base::android::JavaParamRef<jobject>& j_media_drm, |
| bool success) { |
| DVLOG(1) << __func__; |
| |
| // This should only be called as result of a call to Provision(). |
| DCHECK(provisioning_complete_cb_); |
| task_runner_->PostTask( |
| FROM_HERE, base::BindOnce(std::move(provisioning_complete_cb_), success)); |
| } |
| |
| void MediaDrmBridge::OnPromiseResolved(JNIEnv* env, |
| const JavaParamRef<jobject>& j_media_drm, |
| jint j_promise_id) { |
| task_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&MediaDrmBridge::ResolvePromise, |
| weak_factory_.GetWeakPtr(), j_promise_id)); |
| } |
| |
| void MediaDrmBridge::OnPromiseResolvedWithSession( |
| JNIEnv* env, |
| const JavaParamRef<jobject>& j_media_drm, |
| jint j_promise_id, |
| const JavaParamRef<jbyteArray>& j_session_id) { |
| std::string session_id; |
| JavaByteArrayToString(env, j_session_id, &session_id); |
| task_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&MediaDrmBridge::ResolvePromiseWithSession, |
| weak_factory_.GetWeakPtr(), j_promise_id, |
| std::move(session_id))); |
| } |
| |
| void MediaDrmBridge::OnPromiseRejected( |
| JNIEnv* env, |
| const JavaParamRef<jobject>& j_media_drm, |
| jint j_promise_id, |
| const JavaParamRef<jstring>& j_error_message) { |
| task_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&MediaDrmBridge::RejectPromise, |
| weak_factory_.GetWeakPtr(), j_promise_id, |
| ConvertJavaStringToUTF8(env, j_error_message))); |
| } |
| |
| void MediaDrmBridge::OnSessionMessage( |
| JNIEnv* env, |
| const JavaParamRef<jobject>& j_media_drm, |
| const JavaParamRef<jbyteArray>& j_session_id, |
| jint j_message_type, |
| const JavaParamRef<jbyteArray>& j_message) { |
| DVLOG(2) << __func__; |
| |
| std::vector<uint8_t> message; |
| JavaByteArrayToByteVector(env, j_message, &message); |
| CdmMessageType message_type = |
| GetMessageType(static_cast<RequestType>(j_message_type)); |
| |
| std::string session_id; |
| JavaByteArrayToString(env, j_session_id, &session_id); |
| task_runner_->PostTask( |
| FROM_HERE, base::BindOnce(session_message_cb_, std::move(session_id), |
| message_type, message)); |
| } |
| |
| void MediaDrmBridge::OnSessionClosed( |
| JNIEnv* env, |
| const JavaParamRef<jobject>& j_media_drm, |
| const JavaParamRef<jbyteArray>& j_session_id) { |
| DVLOG(2) << __func__; |
| std::string session_id; |
| JavaByteArrayToString(env, j_session_id, &session_id); |
| // TODO(crbug.com/1208618): Support other closed reasons. |
| task_runner_->PostTask( |
| FROM_HERE, base::BindOnce(session_closed_cb_, std::move(session_id), |
| CdmSessionClosedReason::kClose)); |
| } |
| |
| void MediaDrmBridge::OnSessionKeysChange( |
| JNIEnv* env, |
| const JavaParamRef<jobject>& j_media_drm, |
| const JavaParamRef<jbyteArray>& j_session_id, |
| const JavaParamRef<jobjectArray>& j_keys_info, |
| bool has_additional_usable_key, |
| bool is_key_release) { |
| DVLOG(2) << __func__; |
| |
| CdmKeysInfo cdm_keys_info; |
| |
| JavaObjectArrayReader<jobject> j_keys_info_array(j_keys_info); |
| DCHECK_GT(j_keys_info_array.size(), 0); |
| |
| for (auto j_key_status : j_keys_info_array) { |
| ScopedJavaLocalRef<jbyteArray> j_key_id = |
| Java_KeyStatus_getKeyId(env, j_key_status); |
| std::vector<uint8_t> key_id; |
| JavaByteArrayToByteVector(env, j_key_id, &key_id); |
| DCHECK(!key_id.empty()); |
| |
| jint j_status_code = Java_KeyStatus_getStatusCode(env, j_key_status); |
| CdmKeyInformation::KeyStatus key_status = |
| ConvertKeyStatus(static_cast<KeyStatus>(j_status_code), is_key_release); |
| |
| DVLOG(2) << __func__ << "Key status change: " |
| << base::HexEncode(&key_id[0], key_id.size()) << ", " |
| << key_status; |
| |
| cdm_keys_info.push_back( |
| std::make_unique<CdmKeyInformation>(key_id, key_status, 0)); |
| } |
| |
| std::string session_id; |
| JavaByteArrayToString(env, j_session_id, &session_id); |
| task_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce(session_keys_change_cb_, std::move(session_id), |
| has_additional_usable_key, std::move(cdm_keys_info))); |
| |
| if (has_additional_usable_key) { |
| task_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&MediaDrmBridge::OnHasAdditionalUsableKey, |
| weak_factory_.GetWeakPtr())); |
| } |
| } |
| |
| // According to MeidaDrm documentation [1], zero |expiry_time_ms| means the keys |
| // will never expire. This will be translated into a NULL base::Time() [2], |
| // which will then be mapped to a zero Java time [3]. The zero Java time is |
| // passed to Blink which will then be translated to NaN [4], which is what the |
| // spec uses to indicate that the license will never expire [5]. |
| // [1] |
| // http://developer.android.com/reference/android/media/MediaDrm.OnExpirationUpdateListener.html |
| // [2] See base::Time::FromDoubleT() |
| // [3] See base::Time::ToJavaTime() |
| // [4] See MediaKeySession::expirationChanged() |
| // [5] https://github.com/w3c/encrypted-media/issues/58 |
| void MediaDrmBridge::OnSessionExpirationUpdate( |
| JNIEnv* env, |
| const JavaParamRef<jobject>& j_media_drm, |
| const JavaParamRef<jbyteArray>& j_session_id, |
| jlong expiry_time_ms) { |
| DVLOG(2) << __func__ << ": " << expiry_time_ms << " ms"; |
| std::string session_id; |
| JavaByteArrayToString(env, j_session_id, &session_id); |
| task_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce(session_expiration_update_cb_, std::move(session_id), |
| base::Time::FromDoubleT(expiry_time_ms / 1000.0))); |
| } |
| |
| //------------------------------------------------------------------------------ |
| // The following are private methods. |
| |
| MediaDrmBridge::MediaDrmBridge( |
| const std::vector<uint8_t>& scheme_uuid, |
| const std::string& origin_id, |
| SecurityLevel security_level, |
| bool requires_media_crypto, |
| std::unique_ptr<MediaDrmStorageBridge> storage, |
| const CreateFetcherCB& create_fetcher_cb, |
| const SessionMessageCB& session_message_cb, |
| const SessionClosedCB& session_closed_cb, |
| const SessionKeysChangeCB& session_keys_change_cb, |
| const SessionExpirationUpdateCB& session_expiration_update_cb) |
| : scheme_uuid_(scheme_uuid), |
| storage_(std::move(storage)), |
| create_fetcher_cb_(create_fetcher_cb), |
| session_message_cb_(session_message_cb), |
| session_closed_cb_(session_closed_cb), |
| session_keys_change_cb_(session_keys_change_cb), |
| session_expiration_update_cb_(session_expiration_update_cb), |
| task_runner_(base::ThreadTaskRunnerHandle::Get()), |
| media_crypto_context_(this) { |
| DVLOG(1) << __func__; |
| |
| DCHECK(storage_); |
| |
| JNIEnv* env = AttachCurrentThread(); |
| CHECK(env); |
| |
| ScopedJavaLocalRef<jbyteArray> j_scheme_uuid = |
| base::android::ToJavaByteArray(env, &scheme_uuid[0], scheme_uuid.size()); |
| |
| std::string security_level_str = GetSecurityLevelString(security_level); |
| ScopedJavaLocalRef<jstring> j_security_level = |
| ConvertUTF8ToJavaString(env, security_level_str); |
| |
| bool use_origin_isolated_storage = |
| // Per-origin provisioning must be supported for origin isolated storage. |
| IsPerOriginProvisioningSupported() && |
| // origin id can be empty when MediaDrmBridge is created by |
| // CreateWithoutSessionSupport, which is used for unprovisioning. |
| !origin_id.empty(); |
| |
| ScopedJavaLocalRef<jstring> j_security_origin = ConvertUTF8ToJavaString( |
| env, use_origin_isolated_storage ? origin_id : ""); |
| |
| // Note: OnMediaCryptoReady() could be called in this call. |
| j_media_drm_.Reset(Java_MediaDrmBridge_create( |
| env, j_scheme_uuid, j_security_origin, j_security_level, |
| requires_media_crypto, reinterpret_cast<intptr_t>(this), |
| reinterpret_cast<intptr_t>(storage_.get()))); |
| } |
| |
| MediaDrmBridge::~MediaDrmBridge() { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| DVLOG(1) << __func__; |
| |
| JNIEnv* env = AttachCurrentThread(); |
| |
| // After the call to Java_MediaDrmBridge_destroy() Java won't call native |
| // methods anymore, this is ensured by MediaDrmBridge.java. |
| if (j_media_drm_) |
| Java_MediaDrmBridge_destroy(env, j_media_drm_); |
| |
| if (media_crypto_ready_cb_) { |
| std::move(media_crypto_ready_cb_).Run(CreateJavaObjectPtr(nullptr), false); |
| } |
| |
| // Rejects all pending promises. |
| cdm_promise_adapter_.Clear(CdmPromiseAdapter::ClearReason::kDestruction); |
| } |
| |
| MediaDrmBridge::SecurityLevel MediaDrmBridge::GetSecurityLevel() { |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jstring> j_security_level = |
| Java_MediaDrmBridge_getSecurityLevel(env, j_media_drm_); |
| std::string security_level_str = |
| ConvertJavaStringToUTF8(env, j_security_level.obj()); |
| return GetSecurityLevelFromString(security_level_str); |
| } |
| |
| void MediaDrmBridge::NotifyMediaCryptoReady(JavaObjectPtr j_media_crypto) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| DCHECK(j_media_crypto); |
| DCHECK(!j_media_crypto_); |
| |
| j_media_crypto_ = std::move(j_media_crypto); |
| |
| UMA_HISTOGRAM_BOOLEAN("Media.EME.MediaCryptoAvailable", |
| !j_media_crypto_->is_null()); |
| |
| if (!media_crypto_ready_cb_) |
| return; |
| |
| // We have to use scoped_ptr to pass ScopedJavaGlobalRef with a callback. |
| std::move(media_crypto_ready_cb_) |
| .Run(CreateJavaObjectPtr(j_media_crypto_->obj()), |
| IsSecureCodecRequired()); |
| } |
| |
| void MediaDrmBridge::SendProvisioningRequest(const GURL& default_url, |
| const std::string& request_data) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| DVLOG(1) << __func__; |
| |
| DCHECK(!provision_fetcher_) << "At most one provision request at any time."; |
| DCHECK(create_fetcher_cb_); |
| provision_fetcher_ = create_fetcher_cb_.Run(); |
| |
| provision_fetcher_->Retrieve( |
| default_url, request_data, |
| base::BindOnce(&MediaDrmBridge::ProcessProvisionResponse, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void MediaDrmBridge::ProcessProvisionResponse(bool success, |
| const std::string& response) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| DVLOG(1) << __func__; |
| |
| DCHECK(provision_fetcher_) << "No provision request pending."; |
| provision_fetcher_.reset(); |
| |
| if (!success) |
| VLOG(1) << "Device provision failure: can't get server response"; |
| |
| JNIEnv* env = AttachCurrentThread(); |
| |
| ScopedJavaLocalRef<jbyteArray> j_response = ToJavaByteArray(env, response); |
| |
| Java_MediaDrmBridge_processProvisionResponse(env, j_media_drm_, success, |
| j_response); |
| } |
| |
| void MediaDrmBridge::OnHasAdditionalUsableKey() { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| DVLOG(1) << __func__; |
| |
| event_callbacks_.Notify(Event::kHasAdditionalUsableKey); |
| } |
| |
| } // namespace media |