| // Copyright 2019 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "media/cdm/fuchsia/fuchsia_cdm.h" |
| |
| #include "base/fuchsia/fuchsia_logging.h" |
| #include "base/fuchsia/mem_buffer_util.h" |
| #include "base/logging.h" |
| #include "media/base/callback_registry.h" |
| #include "media/base/cdm_promise.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| |
| #define REJECT_PROMISE_AND_RETURN_IF_BAD_CDM(promise, cdm) \ |
| if (!cdm) { \ |
| promise->reject(CdmPromise::Exception::INVALID_STATE_ERROR, 0, \ |
| "CDM channel is disconnected."); \ |
| return; \ |
| } |
| |
| namespace media { |
| |
| namespace { |
| |
| std::string GetInitDataTypeName(EmeInitDataType type) { |
| switch (type) { |
| case EmeInitDataType::WEBM: |
| return "webm"; |
| case EmeInitDataType::CENC: |
| return "cenc"; |
| case EmeInitDataType::KEYIDS: |
| return "keyids"; |
| case EmeInitDataType::UNKNOWN: |
| return "unknown"; |
| } |
| } |
| |
| fuchsia::media::drm::LicenseInitData CreateLicenseInitData( |
| EmeInitDataType type, |
| const std::vector<uint8_t>& data) { |
| fuchsia::media::drm::LicenseInitData init_data; |
| init_data.type = GetInitDataTypeName(type); |
| init_data.data = data; |
| return init_data; |
| } |
| |
| fuchsia::media::drm::LicenseServerMessage CreateLicenseServerMessage( |
| const std::vector<uint8_t>& response) { |
| fuchsia::media::drm::LicenseServerMessage message; |
| message.message = base::MemBufferFromString( |
| base::StringPiece(reinterpret_cast<const char*>(response.data()), |
| response.size()), |
| "cr-drm-license-server-message"); |
| return message; |
| } |
| |
| fuchsia::media::drm::LicenseSessionType ToFuchsiaLicenseSessionType( |
| CdmSessionType session_type) { |
| switch (session_type) { |
| case CdmSessionType::kTemporary: |
| return fuchsia::media::drm::LicenseSessionType::TEMPORARY; |
| case CdmSessionType::kPersistentLicense: |
| return fuchsia::media::drm::LicenseSessionType::PERSISTENT_LICENSE; |
| } |
| } |
| |
| CdmMessageType ToCdmMessageType(fuchsia::media::drm::LicenseMessageType type) { |
| switch (type) { |
| case fuchsia::media::drm::LicenseMessageType::REQUEST: |
| return CdmMessageType::LICENSE_REQUEST; |
| case fuchsia::media::drm::LicenseMessageType::RENEWAL: |
| return CdmMessageType::LICENSE_RENEWAL; |
| case fuchsia::media::drm::LicenseMessageType::RELEASE: |
| return CdmMessageType::LICENSE_RELEASE; |
| } |
| } |
| |
| CdmKeyInformation::KeyStatus ToCdmKeyStatus( |
| fuchsia::media::drm::KeyStatus status) { |
| switch (status) { |
| case fuchsia::media::drm::KeyStatus::USABLE: |
| return CdmKeyInformation::USABLE; |
| case fuchsia::media::drm::KeyStatus::EXPIRED: |
| return CdmKeyInformation::EXPIRED; |
| case fuchsia::media::drm::KeyStatus::RELEASED: |
| return CdmKeyInformation::RELEASED; |
| case fuchsia::media::drm::KeyStatus::OUTPUT_RESTRICTED: |
| return CdmKeyInformation::OUTPUT_RESTRICTED; |
| case fuchsia::media::drm::KeyStatus::OUTPUT_DOWNSCALED: |
| return CdmKeyInformation::OUTPUT_DOWNSCALED; |
| case fuchsia::media::drm::KeyStatus::STATUS_PENDING: |
| return CdmKeyInformation::KEY_STATUS_PENDING; |
| case fuchsia::media::drm::KeyStatus::INTERNAL_ERROR: |
| return CdmKeyInformation::INTERNAL_ERROR; |
| } |
| } |
| |
| CdmPromise::Exception ToCdmPromiseException(fuchsia::media::drm::Error error) { |
| switch (error) { |
| case fuchsia::media::drm::Error::TYPE: |
| return CdmPromise::Exception::TYPE_ERROR; |
| case fuchsia::media::drm::Error::NOT_SUPPORTED: |
| return CdmPromise::Exception::NOT_SUPPORTED_ERROR; |
| case fuchsia::media::drm::Error::INVALID_STATE: |
| return CdmPromise::Exception::INVALID_STATE_ERROR; |
| case fuchsia::media::drm::Error::QUOTA_EXCEEDED: |
| return CdmPromise::Exception::QUOTA_EXCEEDED_ERROR; |
| |
| case fuchsia::media::drm::Error::NOT_PROVISIONED: |
| // FuchsiaCdmManager is supposed to provision CDM. |
| NOTREACHED(); |
| return CdmPromise::Exception::INVALID_STATE_ERROR; |
| |
| case fuchsia::media::drm::Error::INTERNAL: |
| DLOG(ERROR) << "CDM failed due to an internal error."; |
| return CdmPromise::Exception::INVALID_STATE_ERROR; |
| } |
| } |
| |
| } // namespace |
| |
| class FuchsiaCdm::CdmSession { |
| public: |
| using ResultCB = |
| base::OnceCallback<void(absl::optional<CdmPromise::Exception>)>; |
| using SessionReadyCB = base::OnceCallback<void(bool success)>; |
| |
| CdmSession(const FuchsiaCdm::SessionCallbacks* callbacks, |
| base::RepeatingClosure on_new_key) |
| : session_callbacks_(callbacks), on_new_key_(on_new_key) { |
| // License session events, e.g. license request message, key status change. |
| // Fuchsia CDM service guarantees callback of functions (e.g. |
| // GenerateLicenseRequest) are called before event callbacks. So it's safe |
| // to rely on this to resolve the EME promises and send session events to |
| // JS. EME requires promises are resolved before session message. |
| session_.events().OnLicenseMessageGenerated = |
| fit::bind_member(this, &CdmSession::OnLicenseMessageGenerated); |
| session_.events().OnKeyStatesChanged = |
| fit::bind_member(this, &CdmSession::OnKeyStatesChanged); |
| |
| session_.set_error_handler( |
| fit::bind_member(this, &CdmSession::OnSessionError)); |
| } |
| |
| CdmSession(const CdmSession&) = delete; |
| CdmSession& operator=(const CdmSession&) = delete; |
| |
| ~CdmSession() { |
| if (!session_id_.empty()) { |
| session_callbacks_->closed_cb.Run(session_id_, |
| CdmSessionClosedReason::kInternalError); |
| } |
| } |
| |
| fidl::InterfaceRequest<fuchsia::media::drm::LicenseSession> NewRequest() { |
| return session_.NewRequest(); |
| } |
| |
| void GenerateLicenseRequest(EmeInitDataType init_data_type, |
| const std::vector<uint8_t>& init_data, |
| ResultCB generate_license_request_cb) { |
| DCHECK(!result_cb_); |
| result_cb_ = std::move(generate_license_request_cb); |
| session_->GenerateLicenseRequest( |
| CreateLicenseInitData(init_data_type, init_data), |
| [this](fuchsia::media::drm::LicenseSession_GenerateLicenseRequest_Result |
| result) { ProcessResult(result); }); |
| } |
| |
| void GenerateLicenseRelease(ResultCB generate_license_release_cb) { |
| DCHECK(!result_cb_); |
| result_cb_ = std::move(generate_license_release_cb); |
| pending_release_ = true; |
| session_->GenerateLicenseRelease( |
| [this](fuchsia::media::drm::LicenseSession_GenerateLicenseRelease_Result |
| result) { ProcessResult(result); }); |
| } |
| |
| void ProcessLicenseResponse(const std::vector<uint8_t>& response, |
| ResultCB process_license_response_cb) { |
| DCHECK(!result_cb_); |
| result_cb_ = std::move(process_license_response_cb); |
| session_->ProcessLicenseResponse( |
| CreateLicenseServerMessage(response), |
| [this](fuchsia::media::drm::LicenseSession_ProcessLicenseResponse_Result |
| result) { ProcessResult(result); }); |
| } |
| |
| void set_session_id(const std::string& session_id) { |
| session_id_ = session_id; |
| } |
| const std::string& session_id() const { return session_id_; } |
| |
| void set_session_ready_cb(SessionReadyCB session_ready_cb) { |
| session_ready_cb_ = std::move(session_ready_cb); |
| session_.events().OnReady = |
| fit::bind_member(this, &CdmSession::OnSessionReady); |
| } |
| |
| bool pending_release() const { return pending_release_; } |
| |
| private: |
| void OnSessionReady() { |
| DCHECK(session_ready_cb_); |
| std::move(session_ready_cb_).Run(true); |
| } |
| |
| void OnLicenseMessageGenerated(fuchsia::media::drm::LicenseMessage message) { |
| DCHECK(!session_id_.empty()); |
| absl::optional<std::string> session_msg = |
| base::StringFromMemBuffer(message.message); |
| |
| if (!session_msg) { |
| LOG(ERROR) << "Failed to generate message for session " << session_id_; |
| return; |
| } |
| |
| session_callbacks_->message_cb.Run( |
| session_id_, ToCdmMessageType(message.type), |
| std::vector<uint8_t>(session_msg->begin(), session_msg->end())); |
| } |
| |
| void OnKeyStatesChanged( |
| std::vector<fuchsia::media::drm::KeyState> key_states) { |
| bool has_additional_usable_key = false; |
| CdmKeysInfo keys_info; |
| for (const auto& key_state : key_states) { |
| if (!key_state.has_key_id() || !key_state.has_status()) { |
| continue; |
| } |
| CdmKeyInformation::KeyStatus status = ToCdmKeyStatus(key_state.status()); |
| has_additional_usable_key |= (status == CdmKeyInformation::USABLE); |
| keys_info.emplace_back( |
| new CdmKeyInformation(key_state.key_id(), status, 0)); |
| } |
| |
| session_callbacks_->keys_change_cb.Run( |
| session_id_, has_additional_usable_key, std::move(keys_info)); |
| |
| if (has_additional_usable_key) { |
| on_new_key_.Run(); |
| } |
| } |
| |
| void OnSessionError(zx_status_t status) { |
| ZX_LOG(ERROR, status) << "Session error."; |
| |
| if (session_ready_cb_) { |
| std::move(session_ready_cb_).Run(false); |
| } |
| |
| if (result_cb_) { |
| std::move(result_cb_).Run(CdmPromise::Exception::TYPE_ERROR); |
| } |
| } |
| |
| template <typename T> |
| void ProcessResult(const T& result) { |
| DCHECK(result_cb_); |
| std::move(result_cb_) |
| .Run(result.is_err() |
| ? absl::make_optional(ToCdmPromiseException(result.err())) |
| : absl::nullopt); |
| } |
| |
| const SessionCallbacks* const session_callbacks_; |
| base::RepeatingClosure on_new_key_; |
| |
| fuchsia::media::drm::LicenseSessionPtr session_; |
| std::string session_id_; |
| |
| // Callback for OnReady. |
| SessionReadyCB session_ready_cb_; |
| |
| // Callback for license operation. |
| ResultCB result_cb_; |
| |
| // `GenerateLicenseRelease` has been called and the session is waiting for |
| // license release response from server. |
| bool pending_release_ = false; |
| }; |
| |
| FuchsiaCdm::SessionCallbacks::SessionCallbacks() = default; |
| FuchsiaCdm::SessionCallbacks::SessionCallbacks(SessionCallbacks&&) = default; |
| FuchsiaCdm::SessionCallbacks::~SessionCallbacks() = default; |
| FuchsiaCdm::SessionCallbacks& FuchsiaCdm::SessionCallbacks::operator=( |
| SessionCallbacks&&) = default; |
| |
| FuchsiaCdm::FuchsiaCdm(fuchsia::media::drm::ContentDecryptionModulePtr cdm, |
| ReadyCB ready_cb, |
| SessionCallbacks callbacks) |
| : cdm_(std::move(cdm)), |
| ready_cb_(std::move(ready_cb)), |
| session_callbacks_(std::move(callbacks)), |
| decryptor_(this) { |
| DCHECK(cdm_); |
| cdm_.events().OnProvisioned = |
| fit::bind_member(this, &FuchsiaCdm::OnProvisioned); |
| cdm_.set_error_handler([this](zx_status_t status) { |
| ZX_LOG(ERROR, status) << "The fuchsia.media.drm.ContentDecryptionModule" |
| << " channel was terminated."; |
| |
| // Reject all the pending promises. |
| promises_.Clear(CdmPromiseAdapter::ClearReason::kConnectionError); |
| |
| // If the channel closed prior to invoking the ready_cb_, we should invoke |
| // it here with failure. |
| if (ready_cb_) { |
| std::move(ready_cb_).Run( |
| false, "ContentDecryptionModule closed prior to being ready"); |
| } |
| }); |
| } |
| |
| FuchsiaCdm::~FuchsiaCdm() = default; |
| |
| std::unique_ptr<SysmemBufferStream> FuchsiaCdm::CreateStreamDecryptor( |
| bool secure_mode) { |
| fuchsia::media::drm::DecryptorParams params; |
| params.set_require_secure_mode(secure_mode); |
| params.mutable_input_details()->set_format_details_version_ordinal(0); |
| |
| fuchsia::media::StreamProcessorPtr stream_processor; |
| cdm_->CreateDecryptor(std::move(params), stream_processor.NewRequest()); |
| |
| auto decryptor = |
| std::make_unique<FuchsiaStreamDecryptor>(std::move(stream_processor)); |
| |
| // Save callback to use to notify the decryptor about a new key. |
| auto new_key_cb = decryptor->GetOnNewKeyClosure(); |
| { |
| base::AutoLock auto_lock(new_key_callbacks_lock_); |
| new_key_callbacks_.push_back(std::move(new_key_cb)); |
| } |
| |
| return decryptor; |
| } |
| |
| void FuchsiaCdm::SetServerCertificate( |
| const std::vector<uint8_t>& certificate, |
| std::unique_ptr<SimpleCdmPromise> promise) { |
| REJECT_PROMISE_AND_RETURN_IF_BAD_CDM(promise, cdm_); |
| |
| uint32_t promise_id = promises_.SavePromise(std::move(promise)); |
| cdm_->SetServerCertificate( |
| certificate, |
| [this, promise_id]( |
| fuchsia::media::drm:: |
| ContentDecryptionModule_SetServerCertificate_Result result) { |
| if (result.is_err()) { |
| promises_.RejectPromise(promise_id, |
| ToCdmPromiseException(result.err()), 0, |
| "Fail to set server cert."); |
| return; |
| } |
| |
| promises_.ResolvePromise(promise_id); |
| }); |
| } |
| |
| void FuchsiaCdm::CreateSessionAndGenerateRequest( |
| CdmSessionType session_type, |
| EmeInitDataType init_data_type, |
| const std::vector<uint8_t>& init_data, |
| std::unique_ptr<NewSessionCdmPromise> promise) { |
| if (init_data_type == EmeInitDataType::UNKNOWN) { |
| promise->reject(CdmPromise::Exception::NOT_SUPPORTED_ERROR, 0, |
| "init data type is not supported."); |
| return; |
| } |
| |
| REJECT_PROMISE_AND_RETURN_IF_BAD_CDM(promise, cdm_); |
| |
| uint32_t promise_id = promises_.SavePromise(std::move(promise)); |
| |
| auto session = std::make_unique<CdmSession>( |
| &session_callbacks_, |
| base::BindRepeating(&FuchsiaCdm::OnNewKey, base::Unretained(this))); |
| CdmSession* session_ptr = session.get(); |
| |
| cdm_->CreateLicenseSession( |
| ToFuchsiaLicenseSessionType(session_type), session_ptr->NewRequest(), |
| [this, promise_id, |
| session = std::move(session)](std::string session_id) mutable { |
| OnCreateSession(std::move(session), promise_id, session_id); |
| }); |
| |
| // It's safe to pass raw pointer |session_ptr| because |session| owns the |
| // callback so it's guaranteed to outlive the callback. |
| session_ptr->GenerateLicenseRequest( |
| init_data_type, init_data, |
| base::BindOnce(&FuchsiaCdm::OnGenerateLicenseRequestStatus, |
| base::Unretained(this), session_ptr, promise_id)); |
| } |
| |
| void FuchsiaCdm::OnProvisioned() { |
| if (ready_cb_) { |
| std::move(ready_cb_).Run(true, ""); |
| } |
| } |
| |
| void FuchsiaCdm::OnCreateSession(std::unique_ptr<CdmSession> session, |
| uint32_t promise_id, |
| const std::string& session_id) { |
| if (session_id.empty()) { |
| promises_.RejectPromise(promise_id, |
| CdmPromise::Exception::NOT_SUPPORTED_ERROR, 0, |
| "fail to create license session."); |
| return; |
| } |
| |
| session->set_session_id(session_id); |
| DCHECK(!session_map_.contains(session_id)) |
| << "Duplicated session id " << session_id; |
| session_map_[session_id] = std::move(session); |
| } |
| |
| void FuchsiaCdm::OnGenerateLicenseRequestStatus( |
| CdmSession* session, |
| uint32_t promise_id, |
| absl::optional<CdmPromise::Exception> exception) { |
| DCHECK(session); |
| std::string session_id = session->session_id(); |
| |
| if (exception.has_value()) { |
| promises_.RejectPromise(promise_id, exception.value(), 0, |
| "fail to generate license."); |
| session_map_.erase(session_id); |
| return; |
| } |
| |
| DCHECK(!session_id.empty()); |
| promises_.ResolvePromise(promise_id, session_id); |
| } |
| |
| void FuchsiaCdm::LoadSession(CdmSessionType session_type, |
| const std::string& session_id, |
| std::unique_ptr<NewSessionCdmPromise> promise) { |
| DCHECK_NE(session_type, CdmSessionType::kTemporary); |
| DCHECK(!session_id.empty()); |
| REJECT_PROMISE_AND_RETURN_IF_BAD_CDM(promise, cdm_); |
| |
| if (session_map_.contains(session_id)) { |
| promise->reject(CdmPromise::Exception::QUOTA_EXCEEDED_ERROR, 0, |
| "session already exists."); |
| return; |
| } |
| |
| uint32_t promise_id = promises_.SavePromise(std::move(promise)); |
| |
| auto session = std::make_unique<CdmSession>( |
| &session_callbacks_, |
| base::BindRepeating(&FuchsiaCdm::OnNewKey, base::Unretained(this))); |
| CdmSession* session_ptr = session.get(); |
| |
| session_ptr->set_session_id(session_id); |
| session_ptr->set_session_ready_cb( |
| base::BindOnce(&FuchsiaCdm::OnSessionLoaded, base::Unretained(this), |
| std::move(session), promise_id)); |
| |
| cdm_->LoadLicenseSession(session_id, session_ptr->NewRequest()); |
| } |
| |
| void FuchsiaCdm::OnSessionLoaded(std::unique_ptr<CdmSession> session, |
| uint32_t promise_id, |
| bool loaded) { |
| if (!loaded) { |
| promises_.ResolvePromise(promise_id, std::string()); |
| return; |
| } |
| |
| std::string session_id = session->session_id(); |
| DCHECK(!session_map_.contains(session_id)) |
| << "Duplicated session id " << session_id; |
| |
| session_map_.emplace(session_id, std::move(session)); |
| |
| promises_.ResolvePromise(promise_id, session_id); |
| } |
| |
| void FuchsiaCdm::UpdateSession(const std::string& session_id, |
| const std::vector<uint8_t>& response, |
| std::unique_ptr<SimpleCdmPromise> promise) { |
| auto it = session_map_.find(session_id); |
| if (it == session_map_.end()) { |
| promise->reject(CdmPromise::Exception::INVALID_STATE_ERROR, 0, |
| "session doesn't exist."); |
| return; |
| } |
| |
| REJECT_PROMISE_AND_RETURN_IF_BAD_CDM(promise, cdm_); |
| |
| // Caller should NOT pass in an empty response. |
| DCHECK(!response.empty()); |
| |
| uint32_t promise_id = promises_.SavePromise(std::move(promise)); |
| |
| CdmSession* session = it->second.get(); |
| DCHECK(session); |
| |
| session->ProcessLicenseResponse( |
| response, base::BindOnce(&FuchsiaCdm::OnProcessLicenseServerMessageStatus, |
| base::Unretained(this), session_id, promise_id)); |
| } |
| |
| void FuchsiaCdm::OnProcessLicenseServerMessageStatus( |
| const std::string& session_id, |
| uint32_t promise_id, |
| absl::optional<CdmPromise::Exception> exception) { |
| if (exception.has_value()) { |
| promises_.RejectPromise(promise_id, exception.value(), 0, |
| "fail to process license."); |
| return; |
| } |
| |
| promises_.ResolvePromise(promise_id); |
| |
| auto it = session_map_.find(session_id); |
| if (it == session_map_.end()) { |
| return; |
| } |
| |
| // Close the session if the session is waiting for license release ack. |
| CdmSession* session = it->second.get(); |
| DCHECK(session); |
| |
| if (!session->pending_release()) { |
| return; |
| } |
| |
| session_map_.erase(it); |
| } |
| |
| void FuchsiaCdm::CloseSession(const std::string& session_id, |
| std::unique_ptr<SimpleCdmPromise> promise) { |
| // CdmSession will call SessionClosedCB in its destruct. This should be done |
| // before the promise is resolved. |
| session_map_.erase(session_id); |
| |
| promise->resolve(); |
| } |
| |
| void FuchsiaCdm::RemoveSession(const std::string& session_id, |
| std::unique_ptr<SimpleCdmPromise> promise) { |
| auto it = session_map_.find(session_id); |
| if (it == session_map_.end()) { |
| promise->reject(CdmPromise::Exception::INVALID_STATE_ERROR, 0, |
| "session doesn't exist."); |
| return; |
| } |
| |
| REJECT_PROMISE_AND_RETURN_IF_BAD_CDM(promise, cdm_); |
| |
| uint32_t promise_id = promises_.SavePromise(std::move(promise)); |
| |
| CdmSession* session = it->second.get(); |
| DCHECK(session); |
| |
| // For a temporary session, the API will remove the keys and close the |
| // session. For a persistent license session, the API will invalidate the keys |
| // and generates a license release message. |
| session->GenerateLicenseRelease( |
| base::BindOnce(&FuchsiaCdm::OnGenerateLicenseReleaseStatus, |
| base::Unretained(this), session_id, promise_id)); |
| } |
| |
| void FuchsiaCdm::OnGenerateLicenseReleaseStatus( |
| const std::string& session_id, |
| uint32_t promise_id, |
| absl::optional<CdmPromise::Exception> exception) { |
| if (exception.has_value()) { |
| promises_.RejectPromise(promise_id, exception.value(), 0, |
| "Failed to release license."); |
| session_map_.erase(session_id); |
| return; |
| } |
| |
| DCHECK(!session_id.empty()); |
| promises_.ResolvePromise(promise_id); |
| } |
| |
| CdmContext* FuchsiaCdm::GetCdmContext() { |
| return this; |
| } |
| |
| std::unique_ptr<CallbackRegistration> FuchsiaCdm::RegisterEventCB( |
| EventCB event_cb) { |
| return event_callbacks_.Register(std::move(event_cb)); |
| } |
| |
| Decryptor* FuchsiaCdm::GetDecryptor() { |
| return &decryptor_; |
| } |
| |
| FuchsiaCdmContext* FuchsiaCdm::GetFuchsiaCdmContext() { |
| return this; |
| } |
| |
| void FuchsiaCdm::OnNewKey() { |
| event_callbacks_.Notify(Event::kHasAdditionalUsableKey); |
| { |
| base::AutoLock auto_lock(new_key_callbacks_lock_); |
| |
| // Remove cancelled callbacks. |
| new_key_callbacks_.erase( |
| std::remove_if( |
| new_key_callbacks_.begin(), new_key_callbacks_.end(), |
| [](const base::RepeatingClosure& cb) { return cb.IsCancelled(); }), |
| new_key_callbacks_.end()); |
| |
| for (auto& cb : new_key_callbacks_) { |
| cb.Run(); |
| } |
| } |
| } |
| |
| } // namespace media |