| // Copyright 2015 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/cdm/cdm_adapter.h" |
| |
| #include <stddef.h> |
| #include <iomanip> |
| #include <memory> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/callback_helpers.h" |
| #include "base/logging.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "base/time/time.h" |
| #include "base/trace_event/trace_event.h" |
| #include "media/base/audio_decoder_config.h" |
| #include "media/base/callback_registry.h" |
| #include "media/base/cdm_initialized_promise.h" |
| #include "media/base/cdm_key_information.h" |
| #include "media/base/decoder_buffer.h" |
| #include "media/base/decrypt_config.h" |
| #include "media/base/key_systems.h" |
| #include "media/base/limits.h" |
| #include "media/base/video_decoder_config.h" |
| #include "media/base/video_frame.h" |
| #include "media/base/video_util.h" |
| #include "media/cdm/cdm_auxiliary_helper.h" |
| #include "media/cdm/cdm_helpers.h" |
| #include "media/cdm/cdm_type_conversion.h" |
| #include "media/cdm/cdm_wrapper.h" |
| #include "media/media_buildflags.h" |
| #include "ui/gfx/color_space.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "url/origin.h" |
| |
| namespace media { |
| |
| namespace { |
| |
| // Constants for UMA reporting of file size (in KB) via |
| // UMA_HISTOGRAM_CUSTOM_COUNTS. Note that the histogram is log-scaled (rather |
| // than linear). |
| constexpr int kSizeKBMin = 1; |
| constexpr int kSizeKBMax = 512 * 1024; // 512MB |
| constexpr int kSizeKBBuckets = 100; |
| |
| // Only support version 1 of Storage Id. However, the "latest" version can also |
| // be requested. |
| constexpr uint32_t kRequestLatestStorageIdVersion = 0; |
| constexpr uint32_t kCurrentStorageIdVersion = 1; |
| static_assert(kCurrentStorageIdVersion < 0x80000000, |
| "Versions 0x80000000 and above are reserved."); |
| |
| // Verify that OutputProtection types matches those in CDM interface. |
| // Cannot use conversion function because these are used in bit masks. |
| // See CdmAdapter::EnableOutputProtection and |
| // CdmAdapter::OnQueryOutputProtectionStatusDone() below. |
| #define ASSERT_ENUM_EQ(media_enum, cdm_enum) \ |
| static_assert( \ |
| static_cast<int32_t>(media_enum) == static_cast<int32_t>(cdm_enum), \ |
| "Mismatched enum: " #media_enum " != " #cdm_enum) |
| |
| ASSERT_ENUM_EQ(OutputProtection::LinkTypes::NONE, cdm::kLinkTypeNone); |
| ASSERT_ENUM_EQ(OutputProtection::LinkTypes::UNKNOWN, cdm::kLinkTypeUnknown); |
| ASSERT_ENUM_EQ(OutputProtection::LinkTypes::INTERNAL, cdm::kLinkTypeInternal); |
| ASSERT_ENUM_EQ(OutputProtection::LinkTypes::VGA, cdm::kLinkTypeVGA); |
| ASSERT_ENUM_EQ(OutputProtection::LinkTypes::HDMI, cdm::kLinkTypeHDMI); |
| ASSERT_ENUM_EQ(OutputProtection::LinkTypes::DVI, cdm::kLinkTypeDVI); |
| ASSERT_ENUM_EQ(OutputProtection::LinkTypes::DISPLAYPORT, |
| cdm::kLinkTypeDisplayPort); |
| ASSERT_ENUM_EQ(OutputProtection::LinkTypes::NETWORK, cdm::kLinkTypeNetwork); |
| ASSERT_ENUM_EQ(OutputProtection::ProtectionType::NONE, cdm::kProtectionNone); |
| ASSERT_ENUM_EQ(OutputProtection::ProtectionType::HDCP, cdm::kProtectionHDCP); |
| |
| std::string CdmStatusToString(cdm::Status status) { |
| switch (status) { |
| case cdm::kSuccess: |
| return "kSuccess"; |
| case cdm::kNoKey: |
| return "kNoKey"; |
| case cdm::kNeedMoreData: |
| return "kNeedMoreData"; |
| case cdm::kDecryptError: |
| return "kDecryptError"; |
| case cdm::kDecodeError: |
| return "kDecodeError"; |
| case cdm::kInitializationError: |
| return "kInitializationError"; |
| case cdm::kDeferredInitialization: |
| return "kDeferredInitialization"; |
| } |
| |
| NOTREACHED(); |
| return "Invalid Status!"; |
| } |
| |
| inline std::ostream& operator<<(std::ostream& out, cdm::Status status) { |
| return out << CdmStatusToString(status); |
| } |
| |
| std::string GetHexKeyId(const cdm::InputBuffer_2& buffer) { |
| if (buffer.key_id_size == 0) |
| return "N/A"; |
| |
| return base::HexEncode(buffer.key_id, buffer.key_id_size); |
| } |
| |
| std::string GetHexMask(uint32_t mask) { |
| std::stringstream hex_string; |
| hex_string << "0x" << std::setfill('0') << std::setw(8) << std::hex << mask; |
| return hex_string.str(); |
| } |
| |
| void* GetCdmHost(int host_interface_version, void* user_data) { |
| if (!host_interface_version || !user_data) |
| return nullptr; |
| |
| static_assert( |
| CheckSupportedCdmHostVersions(cdm::Host_10::kVersion, |
| cdm::Host_11::kVersion), |
| "Mismatch between GetCdmHost() and IsSupportedCdmHostVersion()"); |
| |
| DCHECK(IsSupportedCdmHostVersion(host_interface_version)); |
| |
| CdmAdapter* cdm_adapter = static_cast<CdmAdapter*>(user_data); |
| DVLOG(1) << "Create CDM Host with version " << host_interface_version; |
| switch (host_interface_version) { |
| case cdm::Host_10::kVersion: |
| return static_cast<cdm::Host_10*>(cdm_adapter); |
| case cdm::Host_11::kVersion: |
| return static_cast<cdm::Host_11*>(cdm_adapter); |
| default: |
| NOTREACHED() << "Unexpected host interface version " |
| << host_interface_version; |
| return nullptr; |
| } |
| } |
| |
| void ReportSystemCodeUMA(const std::string& key_system, uint32_t system_code) { |
| base::UmaHistogramSparse( |
| "Media.EME." + GetKeySystemNameForUMA(key_system) + ".SystemCode", |
| system_code); |
| } |
| |
| // These are reported to UMA server. Do not renumber or reuse values. |
| enum OutputProtectionStatus { |
| kQueried = 0, |
| kNoExternalLink = 1, |
| kAllExternalLinksProtected = 2, |
| // Note: Only add new values immediately before this line. |
| kStatusCount |
| }; |
| |
| void ReportOutputProtectionUMA(OutputProtectionStatus status) { |
| UMA_HISTOGRAM_ENUMERATION("Media.EME.OutputProtection", status, |
| OutputProtectionStatus::kStatusCount); |
| } |
| |
| crash_reporter::CrashKeyString<256> g_origin_crash_key("cdm-origin"); |
| using crash_reporter::ScopedCrashKeyString; |
| |
| } // namespace |
| |
| // static |
| void CdmAdapter::Create( |
| const std::string& key_system, |
| const CdmConfig& cdm_config, |
| CreateCdmFunc create_cdm_func, |
| std::unique_ptr<CdmAuxiliaryHelper> helper, |
| const SessionMessageCB& session_message_cb, |
| const SessionClosedCB& session_closed_cb, |
| const SessionKeysChangeCB& session_keys_change_cb, |
| const SessionExpirationUpdateCB& session_expiration_update_cb, |
| CdmCreatedCB cdm_created_cb) { |
| DCHECK(!key_system.empty()); |
| DCHECK(session_message_cb); |
| DCHECK(session_closed_cb); |
| DCHECK(session_keys_change_cb); |
| DCHECK(session_expiration_update_cb); |
| |
| scoped_refptr<CdmAdapter> cdm = |
| new CdmAdapter(key_system, cdm_config, create_cdm_func, std::move(helper), |
| session_message_cb, session_closed_cb, |
| session_keys_change_cb, session_expiration_update_cb); |
| |
| // |cdm| ownership passed to the promise. |
| cdm->Initialize( |
| std::make_unique<CdmInitializedPromise>(std::move(cdm_created_cb), cdm)); |
| } |
| |
| CdmAdapter::CdmAdapter( |
| const std::string& key_system, |
| const CdmConfig& cdm_config, |
| CreateCdmFunc create_cdm_func, |
| std::unique_ptr<CdmAuxiliaryHelper> helper, |
| const SessionMessageCB& session_message_cb, |
| const SessionClosedCB& session_closed_cb, |
| const SessionKeysChangeCB& session_keys_change_cb, |
| const SessionExpirationUpdateCB& session_expiration_update_cb) |
| : key_system_(key_system), |
| cdm_config_(cdm_config), |
| create_cdm_func_(create_cdm_func), |
| helper_(std::move(helper)), |
| 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), |
| cdm_origin_(helper_->GetCdmOrigin().Serialize()), |
| scoped_crash_key_(&g_origin_crash_key, cdm_origin_), |
| task_runner_(base::ThreadTaskRunnerHandle::Get()), |
| pool_(new AudioBufferMemoryPool()) { |
| DVLOG(1) << __func__; |
| |
| DCHECK(!key_system_.empty()); |
| DCHECK(create_cdm_func_); |
| DCHECK(helper_); |
| DCHECK(session_message_cb_); |
| DCHECK(session_closed_cb_); |
| DCHECK(session_keys_change_cb_); |
| DCHECK(session_expiration_update_cb_); |
| |
| helper_->SetFileReadCB( |
| base::BindRepeating(&CdmAdapter::OnFileRead, weak_factory_.GetWeakPtr())); |
| } |
| |
| CdmAdapter::~CdmAdapter() { |
| DVLOG(1) << __func__; |
| |
| // Reject any outstanding promises and close all the existing sessions. |
| cdm_promise_adapter_.Clear(CdmPromiseAdapter::ClearReason::kDestruction); |
| |
| if (audio_init_cb_) |
| std::move(audio_init_cb_).Run(false); |
| if (video_init_cb_) |
| std::move(video_init_cb_).Run(false); |
| } |
| |
| CdmWrapper* CdmAdapter::CreateCdmInstance(const std::string& key_system) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| TRACE_EVENT0("media", "CdmAdapter::CreateCdmInstance"); |
| |
| CdmWrapper* cdm = CdmWrapper::Create(create_cdm_func_, key_system.data(), |
| key_system.size(), GetCdmHost, this); |
| DVLOG(1) << "CDM instance for " + key_system + (cdm ? "" : " could not be") + |
| " created."; |
| |
| if (cdm) { |
| // The interface version is relatively small. So using normal histogram |
| // instead of a sparse histogram is okay. The following DCHECK asserts this. |
| DCHECK(cdm->GetInterfaceVersion() <= 30); |
| UMA_HISTOGRAM_ENUMERATION("Media.EME.CdmInterfaceVersion", |
| cdm->GetInterfaceVersion(), 30); |
| } |
| |
| return cdm; |
| } |
| |
| void CdmAdapter::Initialize(std::unique_ptr<media::SimpleCdmPromise> promise) { |
| DVLOG(1) << __func__; |
| TRACE_EVENT0("media", "CdmAdapter::Initialize"); |
| |
| cdm_.reset(CreateCdmInstance(key_system_)); |
| if (!cdm_) { |
| promise->reject(CdmPromise::Exception::INVALID_STATE_ERROR, 0, |
| "Unable to create CDM."); |
| return; |
| } |
| |
| init_promise_id_ = cdm_promise_adapter_.SavePromise(std::move(promise)); |
| |
| if (!cdm_->Initialize(cdm_config_.allow_distinctive_identifier, |
| cdm_config_.allow_persistent_state, |
| cdm_config_.use_hw_secure_codecs)) { |
| // OnInitialized() will not be called by the CDM, which is the case for |
| // CDM interfaces prior to CDM_10. |
| OnInitialized(true); |
| return; |
| } |
| |
| // OnInitialized() will be called by the CDM. |
| } |
| |
| int CdmAdapter::GetInterfaceVersion() { |
| return cdm_->GetInterfaceVersion(); |
| } |
| |
| void CdmAdapter::SetServerCertificate( |
| const std::vector<uint8_t>& certificate, |
| std::unique_ptr<SimpleCdmPromise> promise) { |
| DVLOG(2) << __func__; |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| TRACE_EVENT0("media", "CdmAdapter::SetServerCertificate"); |
| |
| if (certificate.size() < limits::kMinCertificateLength || |
| certificate.size() > limits::kMaxCertificateLength) { |
| promise->reject(CdmPromise::Exception::TYPE_ERROR, 0, |
| "Incorrect certificate."); |
| return; |
| } |
| |
| uint32_t promise_id = cdm_promise_adapter_.SavePromise(std::move(promise)); |
| cdm_->SetServerCertificate(promise_id, certificate.data(), |
| certificate.size()); |
| } |
| |
| void CdmAdapter::GetStatusForPolicy( |
| HdcpVersion min_hdcp_version, |
| std::unique_ptr<KeyStatusCdmPromise> promise) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| TRACE_EVENT0("media", "CdmAdapter::GetStatusForPolicy"); |
| |
| uint32_t promise_id = cdm_promise_adapter_.SavePromise(std::move(promise)); |
| DVLOG(2) << __func__ << ": promise_id = " << promise_id; |
| if (!cdm_->GetStatusForPolicy(promise_id, |
| ToCdmHdcpVersion(min_hdcp_version))) { |
| DVLOG(1) << __func__ << ": GetStatusForPolicy not supported"; |
| cdm_promise_adapter_.RejectPromise( |
| promise_id, CdmPromise::Exception::NOT_SUPPORTED_ERROR, 0, |
| "GetStatusForPolicy not supported."); |
| } |
| } |
| |
| void CdmAdapter::CreateSessionAndGenerateRequest( |
| CdmSessionType session_type, |
| EmeInitDataType init_data_type, |
| const std::vector<uint8_t>& init_data, |
| std::unique_ptr<NewSessionCdmPromise> promise) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| TRACE_EVENT0("media", "CdmAdapter::CreateSessionAndGenerateRequest"); |
| |
| uint32_t promise_id = cdm_promise_adapter_.SavePromise(std::move(promise)); |
| DVLOG(2) << __func__ << ": promise_id = " << promise_id; |
| |
| cdm_->CreateSessionAndGenerateRequest( |
| promise_id, ToCdmSessionType(session_type), |
| ToCdmInitDataType(init_data_type), init_data.data(), init_data.size()); |
| } |
| |
| void CdmAdapter::LoadSession(CdmSessionType session_type, |
| const std::string& session_id, |
| std::unique_ptr<NewSessionCdmPromise> promise) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| TRACE_EVENT1("media", "CdmAdapter::LoadSession", "session_id", session_id); |
| |
| uint32_t promise_id = cdm_promise_adapter_.SavePromise(std::move(promise)); |
| DVLOG(2) << __func__ << ": session_id = " << session_id |
| << ", promise_id = " << promise_id; |
| |
| cdm_->LoadSession(promise_id, ToCdmSessionType(session_type), |
| session_id.data(), session_id.size()); |
| } |
| |
| void CdmAdapter::UpdateSession(const std::string& session_id, |
| const std::vector<uint8_t>& response, |
| std::unique_ptr<SimpleCdmPromise> promise) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| DCHECK(!session_id.empty()); |
| DCHECK(!response.empty()); |
| TRACE_EVENT1("media", "CdmAdapter::UpdateSession", "session_id", session_id); |
| |
| uint32_t promise_id = cdm_promise_adapter_.SavePromise(std::move(promise)); |
| DVLOG(2) << __func__ << ": session_id = " << session_id |
| << ", promise_id = " << promise_id; |
| |
| cdm_->UpdateSession(promise_id, session_id.data(), session_id.size(), |
| response.data(), response.size()); |
| } |
| |
| void CdmAdapter::CloseSession(const std::string& session_id, |
| std::unique_ptr<SimpleCdmPromise> promise) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| DCHECK(!session_id.empty()); |
| TRACE_EVENT1("media", "CdmAdapter::CloseSession", "session_id", session_id); |
| |
| uint32_t promise_id = cdm_promise_adapter_.SavePromise(std::move(promise)); |
| DVLOG(2) << __func__ << ": session_id = " << session_id |
| << ", promise_id = " << promise_id; |
| |
| cdm_->CloseSession(promise_id, session_id.data(), session_id.size()); |
| } |
| |
| void CdmAdapter::RemoveSession(const std::string& session_id, |
| std::unique_ptr<SimpleCdmPromise> promise) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| DCHECK(!session_id.empty()); |
| TRACE_EVENT1("media", "CdmAdapter::RemoveSession", "session_id", session_id); |
| |
| uint32_t promise_id = cdm_promise_adapter_.SavePromise(std::move(promise)); |
| DVLOG(2) << __func__ << ": session_id = " << session_id |
| << ", promise_id = " << promise_id; |
| |
| cdm_->RemoveSession(promise_id, session_id.data(), session_id.size()); |
| } |
| |
| CdmContext* CdmAdapter::GetCdmContext() { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| return this; |
| } |
| |
| std::unique_ptr<CallbackRegistration> CdmAdapter::RegisterEventCB( |
| EventCB event_cb) { |
| return event_callbacks_.Register(std::move(event_cb)); |
| } |
| |
| Decryptor* CdmAdapter::GetDecryptor() { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| return this; |
| } |
| |
| absl::optional<base::UnguessableToken> CdmAdapter::GetCdmId() const { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| return absl::nullopt; |
| } |
| |
| void CdmAdapter::Decrypt(StreamType stream_type, |
| scoped_refptr<DecoderBuffer> encrypted, |
| DecryptCB decrypt_cb) { |
| DVLOG(3) << __func__ << ": " |
| << encrypted->AsHumanReadableString(/*verbose=*/true); |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| |
| cdm::InputBuffer_2 input_buffer = {}; |
| std::vector<cdm::SubsampleEntry> subsamples; |
| std::unique_ptr<DecryptedBlockImpl> decrypted_block(new DecryptedBlockImpl()); |
| |
| TRACE_EVENT_BEGIN1("media", "CdmAdapter::Decrypt", "stream_type", |
| stream_type); |
| ToCdmInputBuffer(*encrypted, &subsamples, &input_buffer); |
| cdm::Status status = cdm_->Decrypt(input_buffer, decrypted_block.get()); |
| TRACE_EVENT_END2("media", "CdmAdapter::Decrypt", "key ID", |
| GetHexKeyId(input_buffer), "status", |
| CdmStatusToString(status)); |
| |
| if (status != cdm::kSuccess) { |
| DVLOG(1) << __func__ << ": status = " << status; |
| std::move(decrypt_cb).Run(ToMediaDecryptorStatus(status), nullptr); |
| return; |
| } |
| |
| scoped_refptr<DecoderBuffer> decrypted_buffer( |
| DecoderBuffer::CopyFrom(decrypted_block->DecryptedBuffer()->Data(), |
| decrypted_block->DecryptedBuffer()->Size())); |
| decrypted_buffer->set_timestamp( |
| base::Microseconds(decrypted_block->Timestamp())); |
| std::move(decrypt_cb).Run(Decryptor::kSuccess, std::move(decrypted_buffer)); |
| } |
| |
| void CdmAdapter::CancelDecrypt(StreamType stream_type) { |
| // As the Decrypt methods are synchronous, nothing can be done here. |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| } |
| |
| void CdmAdapter::InitializeAudioDecoder(const AudioDecoderConfig& config, |
| DecoderInitCB init_cb) { |
| DVLOG(2) << __func__ << ": " << config.AsHumanReadableString(); |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| DCHECK(!audio_init_cb_); |
| TRACE_EVENT0("media", "CdmAdapter::InitializeAudioDecode"); |
| |
| auto cdm_config = ToCdmAudioDecoderConfig(config); |
| if (cdm_config.codec == cdm::kUnknownAudioCodec) { |
| DVLOG(1) << __func__ |
| << ": Unsupported config: " << config.AsHumanReadableString(); |
| std::move(init_cb).Run(false); |
| return; |
| } |
| |
| cdm::Status status = cdm_->InitializeAudioDecoder(cdm_config); |
| if (status != cdm::kSuccess && status != cdm::kDeferredInitialization) { |
| DCHECK(status == cdm::kInitializationError); |
| DVLOG(1) << __func__ << ": status = " << status; |
| std::move(init_cb).Run(false); |
| return; |
| } |
| |
| audio_samples_per_second_ = config.samples_per_second(); |
| audio_channel_layout_ = config.channel_layout(); |
| |
| if (status == cdm::kDeferredInitialization) { |
| DVLOG(1) << "Deferred initialization in " << __func__; |
| audio_init_cb_ = std::move(init_cb); |
| return; |
| } |
| |
| std::move(init_cb).Run(true); |
| } |
| |
| void CdmAdapter::InitializeVideoDecoder(const VideoDecoderConfig& config, |
| DecoderInitCB init_cb) { |
| DVLOG(2) << __func__ << ": " << config.AsHumanReadableString(); |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| DCHECK(!video_init_cb_); |
| TRACE_EVENT0("media", "CdmAdapter::InitializeVideoDecoder"); |
| |
| // Alpha decoding is not supported by the CDM. |
| if (config.alpha_mode() != VideoDecoderConfig::AlphaMode::kIsOpaque) { |
| DVLOG(1) << __func__ |
| << ": Unsupported config: " << config.AsHumanReadableString(); |
| std::move(init_cb).Run(false); |
| return; |
| } |
| |
| // cdm::kUnknownVideoCodecProfile and cdm::kUnknownVideoFormat are not checked |
| // because it's possible the container has wrong information or the demuxer |
| // doesn't parse them correctly. |
| auto cdm_config = ToCdmVideoDecoderConfig(config); |
| if (cdm_config.codec == cdm::kUnknownVideoCodec) { |
| DVLOG(1) << __func__ |
| << ": Unsupported config: " << config.AsHumanReadableString(); |
| std::move(init_cb).Run(false); |
| return; |
| } |
| |
| cdm::Status status = cdm_->InitializeVideoDecoder(cdm_config); |
| if (status != cdm::kSuccess && status != cdm::kDeferredInitialization) { |
| DCHECK(status == cdm::kInitializationError); |
| DVLOG(1) << __func__ << ": status = " << status; |
| std::move(init_cb).Run(false); |
| return; |
| } |
| |
| aspect_ratio_ = config.aspect_ratio(); |
| is_video_encrypted_ = config.is_encrypted(); |
| |
| if (status == cdm::kDeferredInitialization) { |
| DVLOG(1) << "Deferred initialization in " << __func__; |
| video_init_cb_ = std::move(init_cb); |
| return; |
| } |
| |
| std::move(init_cb).Run(true); |
| } |
| |
| void CdmAdapter::DecryptAndDecodeAudio(scoped_refptr<DecoderBuffer> encrypted, |
| AudioDecodeCB audio_decode_cb) { |
| DVLOG(3) << __func__ << ": " |
| << encrypted->AsHumanReadableString(/*verbose=*/true); |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| |
| cdm::InputBuffer_2 input_buffer = {}; |
| std::vector<cdm::SubsampleEntry> subsamples; |
| std::unique_ptr<AudioFramesImpl> audio_frames(new AudioFramesImpl()); |
| |
| TRACE_EVENT_BEGIN0("media", "CdmAdapter::DecryptAndDecodeAudio"); |
| ToCdmInputBuffer(*encrypted, &subsamples, &input_buffer); |
| cdm::Status status = |
| cdm_->DecryptAndDecodeSamples(input_buffer, audio_frames.get()); |
| TRACE_EVENT_END2("media", "CdmAdapter::DecryptAndDecodeAudio", "key ID", |
| GetHexKeyId(input_buffer), "status", |
| CdmStatusToString(status)); |
| |
| const Decryptor::AudioFrames empty_frames; |
| if (status != cdm::kSuccess) { |
| DVLOG(1) << __func__ << ": status = " << status; |
| std::move(audio_decode_cb) |
| .Run(ToMediaDecryptorStatus(status), empty_frames); |
| return; |
| } |
| |
| Decryptor::AudioFrames audio_frame_list; |
| DCHECK(audio_frames->FrameBuffer()); |
| if (!AudioFramesDataToAudioFrames(std::move(audio_frames), |
| &audio_frame_list)) { |
| DVLOG(1) << __func__ << " unable to convert Audio Frames"; |
| std::move(audio_decode_cb).Run(Decryptor::kError, empty_frames); |
| return; |
| } |
| |
| std::move(audio_decode_cb).Run(Decryptor::kSuccess, audio_frame_list); |
| } |
| |
| void CdmAdapter::DecryptAndDecodeVideo(scoped_refptr<DecoderBuffer> encrypted, |
| VideoDecodeCB video_decode_cb) { |
| DVLOG(3) << __func__ << ": " |
| << encrypted->AsHumanReadableString(/*verbose=*/true); |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| |
| cdm::InputBuffer_2 input_buffer = {}; |
| std::vector<cdm::SubsampleEntry> subsamples; |
| std::unique_ptr<VideoFrameImpl> video_frame = helper_->CreateCdmVideoFrame(); |
| |
| TRACE_EVENT_BEGIN1( |
| "media", "CdmAdapter::DecryptAndDecodeVideo", "buffer type", |
| encrypted->end_of_stream() |
| ? "end of stream" |
| : (encrypted->is_key_frame() ? "key frame" : "non-key frame")); |
| ToCdmInputBuffer(*encrypted, &subsamples, &input_buffer); |
| cdm::Status status = |
| cdm_->DecryptAndDecodeFrame(input_buffer, video_frame.get()); |
| TRACE_EVENT_END2("media", "CdmAdapter::DecryptAndDecodeVideo", "key ID", |
| GetHexKeyId(input_buffer), "status", |
| CdmStatusToString(status)); |
| |
| if (status != cdm::kSuccess) { |
| DVLOG(1) << __func__ << ": status = " << status; |
| std::move(video_decode_cb).Run(ToMediaDecryptorStatus(status), nullptr); |
| return; |
| } |
| |
| gfx::Rect visible_rect(video_frame->Size().width, video_frame->Size().height); |
| scoped_refptr<VideoFrame> decoded_frame = video_frame->TransformToVideoFrame( |
| aspect_ratio_.GetNaturalSize(visible_rect)); |
| if (!decoded_frame) { |
| DLOG(ERROR) << __func__ << ": TransformToVideoFrame failed."; |
| std::move(video_decode_cb).Run(Decryptor::kError, nullptr); |
| return; |
| } |
| |
| decoded_frame->metadata().protected_video = is_video_encrypted_; |
| |
| std::move(video_decode_cb).Run(Decryptor::kSuccess, decoded_frame); |
| } |
| |
| void CdmAdapter::ResetDecoder(StreamType stream_type) { |
| DVLOG(2) << __func__ << ": stream_type = " << stream_type; |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| TRACE_EVENT1("media", "CdmAdapter::ResetDecoder", "stream_type", stream_type); |
| |
| cdm_->ResetDecoder(ToCdmStreamType(stream_type)); |
| } |
| |
| void CdmAdapter::DeinitializeDecoder(StreamType stream_type) { |
| DVLOG(2) << __func__ << ": stream_type = " << stream_type; |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| TRACE_EVENT1("media", "CdmAdapter::DeinitializeDecoder", "stream_type", |
| stream_type); |
| |
| cdm_->DeinitializeDecoder(ToCdmStreamType(stream_type)); |
| |
| // Reset the saved values from initializing the decoder. |
| switch (stream_type) { |
| case Decryptor::kAudio: |
| audio_samples_per_second_ = 0; |
| audio_channel_layout_ = CHANNEL_LAYOUT_NONE; |
| break; |
| case Decryptor::kVideo: |
| aspect_ratio_ = VideoAspectRatio(); |
| break; |
| } |
| } |
| |
| cdm::Buffer* CdmAdapter::Allocate(uint32_t capacity) { |
| DVLOG(3) << __func__ << ": capacity = " << capacity; |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| TRACE_EVENT1("media", "CdmAdapter::Allocate", "capacity", capacity); |
| |
| return helper_->CreateCdmBuffer(capacity); |
| } |
| |
| void CdmAdapter::SetTimer(int64_t delay_ms, void* context) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| |
| auto delay = base::Milliseconds(delay_ms); |
| DVLOG(3) << __func__ << ": delay = " << delay << ", context = " << context; |
| TRACE_EVENT2("media", "CdmAdapter::SetTimer", "delay_ms", delay_ms, "context", |
| context); |
| |
| task_runner_->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&CdmAdapter::TimerExpired, weak_factory_.GetWeakPtr(), |
| context), |
| delay); |
| } |
| |
| void CdmAdapter::TimerExpired(void* context) { |
| DVLOG(3) << __func__ << ": context = " << context; |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| TRACE_EVENT1("media", "CdmAdapter::TimerExpired", "context", context); |
| |
| cdm_->TimerExpired(context); |
| } |
| |
| cdm::Time CdmAdapter::GetCurrentWallTime() { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| return base::Time::Now().ToDoubleT(); |
| } |
| |
| void CdmAdapter::OnInitialized(bool success) { |
| DVLOG(3) << __func__ << ": success = " << success; |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| DCHECK_NE(init_promise_id_, CdmPromiseAdapter::kInvalidPromiseId); |
| |
| if (!success) { |
| cdm_promise_adapter_.RejectPromise( |
| init_promise_id_, CdmPromise::Exception::INVALID_STATE_ERROR, 0, |
| "Unable to create CDM."); |
| } else { |
| cdm_promise_adapter_.ResolvePromise(init_promise_id_); |
| } |
| |
| init_promise_id_ = CdmPromiseAdapter::kInvalidPromiseId; |
| } |
| |
| void CdmAdapter::OnResolveKeyStatusPromise(uint32_t promise_id, |
| cdm::KeyStatus key_status) { |
| DVLOG(2) << __func__ << ": promise_id = " << promise_id |
| << ", key_status = " << key_status; |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| cdm_promise_adapter_.ResolvePromise(promise_id, ToMediaKeyStatus(key_status)); |
| } |
| |
| void CdmAdapter::OnResolvePromise(uint32_t promise_id) { |
| DVLOG(2) << __func__ << ": promise_id = " << promise_id; |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| cdm_promise_adapter_.ResolvePromise(promise_id); |
| } |
| |
| void CdmAdapter::OnResolveNewSessionPromise(uint32_t promise_id, |
| const char* session_id, |
| uint32_t session_id_size) { |
| DVLOG(2) << __func__ << ": promise_id = " << promise_id; |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| cdm_promise_adapter_.ResolvePromise(promise_id, |
| std::string(session_id, session_id_size)); |
| } |
| |
| void CdmAdapter::OnRejectPromise(uint32_t promise_id, |
| cdm::Exception exception, |
| uint32_t system_code, |
| const char* error_message, |
| uint32_t error_message_size) { |
| std::string error_message_str(error_message, error_message_size); |
| DVLOG(2) << __func__ << ": promise_id = " << promise_id |
| << ", exception = " << exception << ", system_code = " << system_code |
| << ", error_message = " << error_message_str; |
| |
| // This is the central place for library CDM promise rejection. Cannot report |
| // this in more generic classes like CdmPromise or CdmPromiseAdapter because |
| // they may be used multiple times in one promise chain that involves IPC. |
| ReportSystemCodeUMA(key_system_, system_code); |
| |
| // UMA to help track file related errors. See http://crbug.com/410630 |
| if (system_code == 0x27) { |
| UMA_HISTOGRAM_CUSTOM_COUNTS("Media.EME.CdmFileIO.FileSizeKBOnError", |
| last_read_file_size_kb_, kSizeKBMin, kSizeKBMax, |
| kSizeKBBuckets); |
| } |
| |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| cdm_promise_adapter_.RejectPromise(promise_id, |
| ToMediaCdmPromiseException(exception), |
| system_code, error_message_str); |
| } |
| |
| void CdmAdapter::OnSessionMessage(const char* session_id, |
| uint32_t session_id_size, |
| cdm::MessageType message_type, |
| const char* message, |
| uint32_t message_size) { |
| std::string session_id_str(session_id, session_id_size); |
| DVLOG(2) << __func__ << ": session_id = " << session_id_str; |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| |
| TRACE_EVENT2("media", "CdmAdapter::OnSessionMessage", "session_id", |
| session_id_str, "message_type", message_type); |
| |
| const uint8_t* message_ptr = reinterpret_cast<const uint8_t*>(message); |
| session_message_cb_.Run( |
| session_id_str, ToMediaMessageType(message_type), |
| std::vector<uint8_t>(message_ptr, message_ptr + message_size)); |
| } |
| |
| void CdmAdapter::OnSessionKeysChange(const char* session_id, |
| uint32_t session_id_size, |
| bool has_additional_usable_key, |
| const cdm::KeyInformation* keys_info, |
| uint32_t keys_info_count) { |
| std::string session_id_str(session_id, session_id_size); |
| DVLOG(2) << __func__ << ": session_id = " << session_id_str; |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| |
| TRACE_EVENT2("media", "CdmAdapter::OnSessionKeysChange", "session_id", |
| session_id_str, "has_additional_usable_key", |
| has_additional_usable_key); |
| |
| CdmKeysInfo keys; |
| keys.reserve(keys_info_count); |
| for (uint32_t i = 0; i < keys_info_count; ++i) { |
| const auto& info = keys_info[i]; |
| keys.push_back(std::make_unique<CdmKeyInformation>( |
| info.key_id, info.key_id_size, ToMediaKeyStatus(info.status), |
| info.system_code)); |
| } |
| |
| if (has_additional_usable_key) |
| event_callbacks_.Notify(Event::kHasAdditionalUsableKey); |
| |
| session_keys_change_cb_.Run(session_id_str, has_additional_usable_key, |
| std::move(keys)); |
| } |
| |
| void CdmAdapter::OnExpirationChange(const char* session_id, |
| uint32_t session_id_size, |
| cdm::Time new_expiry_time) { |
| std::string session_id_str(session_id, session_id_size); |
| DVLOG(2) << __func__ << ": session_id = " << session_id_str |
| << ", new_expiry_time = " << new_expiry_time; |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| |
| base::Time expiration = base::Time::FromDoubleT(new_expiry_time); |
| TRACE_EVENT2("media", "CdmAdapter::OnExpirationChange", "session_id", |
| session_id_str, "new_expiry_time", expiration); |
| session_expiration_update_cb_.Run(session_id_str, expiration); |
| } |
| |
| void CdmAdapter::OnSessionClosed(const char* session_id, |
| uint32_t session_id_size) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| |
| std::string session_id_str(session_id, session_id_size); |
| TRACE_EVENT1("media", "CdmAdapter::OnSessionClosed", "session_id", |
| session_id_str); |
| // Library CDMs typically only close sessions as a result of `CloseSession()`. |
| session_closed_cb_.Run(session_id_str, CdmSessionClosedReason::kClose); |
| } |
| |
| void CdmAdapter::SendPlatformChallenge(const char* service_id, |
| uint32_t service_id_size, |
| const char* challenge, |
| uint32_t challenge_size) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| |
| if (!cdm_config_.allow_distinctive_identifier) { |
| task_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce(&CdmAdapter::OnChallengePlatformDone, |
| weak_factory_.GetWeakPtr(), false, "", "", "")); |
| return; |
| } |
| |
| helper_->ChallengePlatform( |
| std::string(service_id, service_id_size), |
| std::string(challenge, challenge_size), |
| base::BindOnce(&CdmAdapter::OnChallengePlatformDone, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void CdmAdapter::OnChallengePlatformDone( |
| bool success, |
| const std::string& signed_data, |
| const std::string& signed_data_signature, |
| const std::string& platform_key_certificate) { |
| DVLOG(2) << __func__ << ": success = " << success; |
| TRACE_EVENT1("media", "CdmAdapter::OnChallengePlatformDone", "success", |
| success); |
| |
| cdm::PlatformChallengeResponse platform_challenge_response = {}; |
| if (success) { |
| platform_challenge_response.signed_data = |
| reinterpret_cast<const uint8_t*>(signed_data.data()); |
| platform_challenge_response.signed_data_length = signed_data.length(); |
| platform_challenge_response.signed_data_signature = |
| reinterpret_cast<const uint8_t*>(signed_data_signature.data()); |
| platform_challenge_response.signed_data_signature_length = |
| signed_data_signature.length(); |
| platform_challenge_response.platform_key_certificate = |
| reinterpret_cast<const uint8_t*>(platform_key_certificate.data()); |
| platform_challenge_response.platform_key_certificate_length = |
| platform_key_certificate.length(); |
| } |
| |
| cdm_->OnPlatformChallengeResponse(platform_challenge_response); |
| } |
| |
| void CdmAdapter::EnableOutputProtection(uint32_t desired_protection_mask) { |
| DVLOG(1) << __func__; |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| TRACE_EVENT1("media", "CdmAdapter::EnableOutputProtection", |
| "desired_protection_mask", GetHexMask(desired_protection_mask)); |
| |
| helper_->EnableProtection( |
| desired_protection_mask, |
| base::BindOnce(&CdmAdapter::OnEnableOutputProtectionDone, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void CdmAdapter::OnEnableOutputProtectionDone(bool success) { |
| // CDM needs to call QueryOutputProtectionStatus() to see if it took effect |
| // or not. |
| DVLOG(1) << __func__ << ": success = " << success; |
| TRACE_EVENT1("media", "CdmAdapter::OnEnableOutputProtectionDone", "success", |
| success); |
| } |
| |
| void CdmAdapter::QueryOutputProtectionStatus() { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| TRACE_EVENT0("media", "CdmAdapter::QueryOutputProtectionStatus"); |
| |
| ReportOutputProtectionQuery(); |
| helper_->QueryStatus( |
| base::BindOnce(&CdmAdapter::OnQueryOutputProtectionStatusDone, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void CdmAdapter::OnQueryOutputProtectionStatusDone(bool success, |
| uint32_t link_mask, |
| uint32_t protection_mask) { |
| DVLOG(2) << __func__ << ": success = " << success; |
| // Combining |link_mask| and |protection_mask| since there's no TRACE_EVENT3. |
| TRACE_EVENT2("media", "CdmAdapter::OnQueryOutputProtectionStatusDone", |
| "success", success, "link_mask, protection_mask", |
| GetHexMask(link_mask) + ", " + GetHexMask(protection_mask)); |
| |
| // The bit mask definition must be consistent between media::OutputProtection |
| // and cdm::ContentDecryptionModule* interfaces. This is statically asserted |
| // by ASSERT_ENUM_EQs above. |
| |
| // Return a query status of failure on error. |
| cdm::QueryResult query_result; |
| if (success) { |
| query_result = cdm::kQuerySucceeded; |
| ReportOutputProtectionQueryResult(link_mask, protection_mask); |
| } else { |
| DVLOG(1) << __func__ << ": query output protection status failed"; |
| query_result = cdm::kQueryFailed; |
| } |
| |
| cdm_->OnQueryOutputProtectionStatus(query_result, link_mask, protection_mask); |
| } |
| |
| void CdmAdapter::ReportOutputProtectionQuery() { |
| if (uma_for_output_protection_query_reported_) |
| return; |
| |
| ReportOutputProtectionUMA(OutputProtectionStatus::kQueried); |
| uma_for_output_protection_query_reported_ = true; |
| } |
| |
| void CdmAdapter::ReportOutputProtectionQueryResult(uint32_t link_mask, |
| uint32_t protection_mask) { |
| DCHECK(uma_for_output_protection_query_reported_); |
| |
| if (uma_for_output_protection_positive_result_reported_) |
| return; |
| |
| // Report UMAs for output protection query result. |
| |
| uint32_t external_links = (link_mask & ~cdm::kLinkTypeInternal); |
| |
| if (!external_links) { |
| ReportOutputProtectionUMA(OutputProtectionStatus::kNoExternalLink); |
| uma_for_output_protection_positive_result_reported_ = true; |
| return; |
| } |
| |
| const uint32_t kProtectableLinks = |
| cdm::kLinkTypeHDMI | cdm::kLinkTypeDVI | cdm::kLinkTypeDisplayPort; |
| bool is_unprotectable_link_connected = |
| (external_links & ~kProtectableLinks) != 0; |
| bool is_hdcp_enabled_on_all_protectable_links = |
| (protection_mask & cdm::kProtectionHDCP) != 0; |
| |
| if (!is_unprotectable_link_connected && |
| is_hdcp_enabled_on_all_protectable_links) { |
| ReportOutputProtectionUMA( |
| OutputProtectionStatus::kAllExternalLinksProtected); |
| uma_for_output_protection_positive_result_reported_ = true; |
| return; |
| } |
| |
| // Do not report a negative result because it could be a false negative. |
| // Instead, we will calculate number of negatives using the total number of |
| // queries and positive results. |
| } |
| |
| void CdmAdapter::OnDeferredInitializationDone(cdm::StreamType stream_type, |
| cdm::Status decoder_status) { |
| DVLOG(1) << __func__ << ": stream_type = " << stream_type |
| << ", decoder_status = " << decoder_status; |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| |
| switch (stream_type) { |
| case cdm::kStreamTypeAudio: |
| std::move(audio_init_cb_).Run(decoder_status == cdm::kSuccess); |
| return; |
| case cdm::kStreamTypeVideo: |
| std::move(video_init_cb_).Run(decoder_status == cdm::kSuccess); |
| return; |
| } |
| |
| NOTREACHED() << "Unexpected cdm::StreamType " << stream_type; |
| } |
| |
| cdm::FileIO* CdmAdapter::CreateFileIO(cdm::FileIOClient* client) { |
| DVLOG(3) << __func__; |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| |
| if (!cdm_config_.allow_persistent_state) { |
| DVLOG(1) << __func__ << ": Persistent state not allowed."; |
| return nullptr; |
| } |
| |
| return helper_->CreateCdmFileIO(client); |
| } |
| |
| void CdmAdapter::RequestStorageId(uint32_t version) { |
| DVLOG(2) << __func__ << ": version = " << version; |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| |
| if (!cdm_config_.allow_persistent_state || |
| !(version == kCurrentStorageIdVersion || |
| version == kRequestLatestStorageIdVersion)) { |
| DVLOG(1) << __func__ << ": Persistent state not allowed (" |
| << cdm_config_.allow_persistent_state |
| << ") or invalid storage ID version (" << version << ")."; |
| task_runner_->PostTask(FROM_HERE, |
| base::BindOnce(&CdmAdapter::OnStorageIdObtained, |
| weak_factory_.GetWeakPtr(), version, |
| std::vector<uint8_t>())); |
| return; |
| } |
| |
| helper_->GetStorageId(version, |
| base::BindOnce(&CdmAdapter::OnStorageIdObtained, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void CdmAdapter::OnStorageIdObtained(uint32_t version, |
| const std::vector<uint8_t>& storage_id) { |
| DVLOG(2) << __func__ << ": version = " << version; |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| TRACE_EVENT1("media", "CdmAdapter::OnStorageIdObtained", "version", version); |
| |
| cdm_->OnStorageId(version, storage_id.data(), storage_id.size()); |
| } |
| |
| bool CdmAdapter::AudioFramesDataToAudioFrames( |
| std::unique_ptr<AudioFramesImpl> audio_frames, |
| Decryptor::AudioFrames* result_frames) { |
| const uint8_t* data = audio_frames->FrameBuffer()->Data(); |
| const size_t data_size = audio_frames->FrameBuffer()->Size(); |
| size_t bytes_left = data_size; |
| const SampleFormat sample_format = |
| ToMediaSampleFormat(audio_frames->Format()); |
| const int audio_channel_count = |
| ChannelLayoutToChannelCount(audio_channel_layout_); |
| const int audio_bytes_per_frame = |
| SampleFormatToBytesPerChannel(sample_format) * audio_channel_count; |
| if (audio_bytes_per_frame <= 0) |
| return false; |
| |
| // Allocate space for the channel pointers given to AudioBuffer. |
| std::vector<const uint8_t*> channel_ptrs(audio_channel_count, nullptr); |
| do { |
| // AudioFrames can contain multiple audio output buffers, which are |
| // serialized into this format: |
| // |<------------------- serialized audio buffer ------------------->| |
| // | int64_t timestamp | int64_t length | length bytes of audio data | |
| int64_t timestamp = 0; |
| int64_t frame_size = -1; |
| const size_t kHeaderSize = sizeof(timestamp) + sizeof(frame_size); |
| if (bytes_left < kHeaderSize) |
| return false; |
| |
| memcpy(×tamp, data, sizeof(timestamp)); |
| memcpy(&frame_size, data + sizeof(timestamp), sizeof(frame_size)); |
| data += kHeaderSize; |
| bytes_left -= kHeaderSize; |
| |
| // We should *not* have empty frames in the list. |
| if (frame_size <= 0 || |
| bytes_left < base::checked_cast<size_t>(frame_size)) { |
| return false; |
| } |
| |
| // Setup channel pointers. AudioBuffer::CopyFrom() will only use the first |
| // one in the case of interleaved data. |
| const int size_per_channel = frame_size / audio_channel_count; |
| for (int i = 0; i < audio_channel_count; ++i) |
| channel_ptrs[i] = data + i * size_per_channel; |
| |
| const int frame_count = frame_size / audio_bytes_per_frame; |
| scoped_refptr<media::AudioBuffer> frame = media::AudioBuffer::CopyFrom( |
| sample_format, audio_channel_layout_, audio_channel_count, |
| audio_samples_per_second_, frame_count, &channel_ptrs[0], |
| base::Microseconds(timestamp), pool_); |
| result_frames->push_back(frame); |
| |
| data += frame_size; |
| bytes_left -= frame_size; |
| } while (bytes_left > 0); |
| |
| return true; |
| } |
| |
| void CdmAdapter::OnFileRead(int file_size_bytes) { |
| DCHECK_GE(file_size_bytes, 0); |
| last_read_file_size_kb_ = file_size_bytes / 1024; |
| |
| if (file_size_uma_reported_) |
| return; |
| |
| UMA_HISTOGRAM_CUSTOM_COUNTS("Media.EME.CdmFileIO.FileSizeKBOnFirstRead", |
| last_read_file_size_kb_, kSizeKBMin, kSizeKBMax, |
| kSizeKBBuckets); |
| file_size_uma_reported_ = true; |
| } |
| |
| } // namespace media |