blob: 96ecffd2104f4eea16b7f23aa90068b323a34c58 [file] [log] [blame]
// Copyright 2019 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/fuchsia/cdm/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_.find(session_id) == session_map_.end())
<< "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_.find(session_id) == session_map_.end())
<< "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) {
// There's a small window app can call close twice before receiving the closed
// event, in which case we want to resolve the promise. Read
// AesDecryptor::CloseSession for more details.
//
// Resolve the promise before deleting CdmSession. CdmSession will call
// SessionClosedCB in its destructor.
promise->resolve();
session_map_.erase(session_id);
}
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