blob: fbc11e5bd15587562cc76be72a9846cec22034be [file] [log] [blame]
// Copyright 2014 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 "modules/encryptedmedia/HTMLMediaElementEncryptedMedia.h"
#include "bindings/core/v8/ExceptionState.h"
#include "bindings/core/v8/ScriptPromise.h"
#include "bindings/core/v8/ScriptPromiseResolver.h"
#include "bindings/core/v8/V8ThrowDOMException.h"
#include "core/dom/DOMException.h"
#include "core/dom/ExceptionCode.h"
#include "core/html/media/HTMLMediaElement.h"
#include "core/inspector/ConsoleMessage.h"
#include "core/typed_arrays/DOMTypedArray.h"
#include "modules/encryptedmedia/ContentDecryptionModuleResultPromise.h"
#include "modules/encryptedmedia/EncryptedMediaUtils.h"
#include "modules/encryptedmedia/MediaEncryptedEvent.h"
#include "modules/encryptedmedia/MediaKeys.h"
#include "platform/ContentDecryptionModuleResult.h"
#include "platform/bindings/ScriptState.h"
#include "platform/wtf/Functional.h"
#include "platform/wtf/text/StringBuilder.h"
#include "public/platform/TaskType.h"
#define EME_LOG_LEVEL 3
namespace blink {
// This class allows MediaKeys to be set asynchronously.
class SetMediaKeysHandler : public ScriptPromiseResolver {
WTF_MAKE_NONCOPYABLE(SetMediaKeysHandler);
public:
static ScriptPromise Create(ScriptState*, HTMLMediaElement&, MediaKeys*);
~SetMediaKeysHandler() override;
void Trace(blink::Visitor*) override;
private:
SetMediaKeysHandler(ScriptState*, HTMLMediaElement&, MediaKeys*);
void TimerFired(TimerBase*);
void ClearExistingMediaKeys();
void SetNewMediaKeys();
void Finish();
void Fail(ExceptionCode, const String& error_message);
void ClearFailed(ExceptionCode, const String& error_message);
void SetFailed(ExceptionCode, const String& error_message);
// Keep media element alive until promise is fulfilled
Member<HTMLMediaElement> element_;
Member<MediaKeys> new_media_keys_;
bool made_reservation_;
TaskRunnerTimer<SetMediaKeysHandler> timer_;
};
typedef base::OnceCallback<void()> SuccessCallback;
typedef base::OnceCallback<void(ExceptionCode, const String&)> FailureCallback;
// Represents the result used when setContentDecryptionModule() is called.
// Calls |success| if result is resolved, |failure| if result is rejected.
class SetContentDecryptionModuleResult final
: public ContentDecryptionModuleResult {
public:
SetContentDecryptionModuleResult(SuccessCallback success,
FailureCallback failure)
: success_callback_(std::move(success)),
failure_callback_(std::move(failure)) {}
// ContentDecryptionModuleResult implementation.
void Complete() override {
DVLOG(EME_LOG_LEVEL) << __func__ << ": promise resolved.";
std::move(success_callback_).Run();
}
void CompleteWithContentDecryptionModule(
WebContentDecryptionModule*) override {
NOTREACHED();
std::move(failure_callback_)
.Run(kInvalidStateError, "Unexpected completion.");
}
void CompleteWithSession(
WebContentDecryptionModuleResult::SessionStatus status) override {
NOTREACHED();
std::move(failure_callback_)
.Run(kInvalidStateError, "Unexpected completion.");
}
void CompleteWithKeyStatus(
WebEncryptedMediaKeyInformation::KeyStatus key_status) override {
NOTREACHED();
std::move(failure_callback_)
.Run(kInvalidStateError, "Unexpected completion.");
}
void CompleteWithError(WebContentDecryptionModuleException code,
unsigned long system_code,
const WebString& message) override {
// Non-zero |systemCode| is appended to the |message|. If the |message|
// is empty, we'll report "Rejected with system code (systemCode)".
StringBuilder result;
result.Append(message);
if (system_code != 0) {
if (result.IsEmpty())
result.Append("Rejected with system code");
result.Append(" (");
result.AppendNumber(system_code);
result.Append(')');
}
DVLOG(EME_LOG_LEVEL) << __func__ << ": promise rejected with code " << code
<< " and message: " << result.ToString();
std::move(failure_callback_)
.Run(WebCdmExceptionToExceptionCode(code), result.ToString());
}
private:
SuccessCallback success_callback_;
FailureCallback failure_callback_;
};
ScriptPromise SetMediaKeysHandler::Create(ScriptState* script_state,
HTMLMediaElement& element,
MediaKeys* media_keys) {
SetMediaKeysHandler* handler =
new SetMediaKeysHandler(script_state, element, media_keys);
handler->PauseIfNeeded();
handler->KeepAliveWhilePending();
return handler->Promise();
}
SetMediaKeysHandler::SetMediaKeysHandler(ScriptState* script_state,
HTMLMediaElement& element,
MediaKeys* media_keys)
: ScriptPromiseResolver(script_state),
element_(element),
new_media_keys_(media_keys),
made_reservation_(false),
timer_(ExecutionContext::From(script_state)
->GetTaskRunner(TaskType::kMiscPlatformAPI),
this,
&SetMediaKeysHandler::TimerFired) {
DVLOG(EME_LOG_LEVEL) << __func__;
// 5. Run the following steps in parallel.
timer_.StartOneShot(TimeDelta(), FROM_HERE);
}
SetMediaKeysHandler::~SetMediaKeysHandler() = default;
void SetMediaKeysHandler::TimerFired(TimerBase*) {
ClearExistingMediaKeys();
}
void SetMediaKeysHandler::ClearExistingMediaKeys() {
DVLOG(EME_LOG_LEVEL) << __func__;
HTMLMediaElementEncryptedMedia& this_element =
HTMLMediaElementEncryptedMedia::From(*element_);
// 5.1 If mediaKeys is not null, the CDM instance represented by
// mediaKeys is already in use by another media element, and
// the user agent is unable to use it with this element, let
// this object's attaching media keys value be false and
// reject promise with a QuotaExceededError.
if (new_media_keys_) {
if (!new_media_keys_->ReserveForMediaElement(element_.Get())) {
this_element.is_attaching_media_keys_ = false;
Fail(kQuotaExceededError,
"The MediaKeys object is already in use by another media element.");
return;
}
// Note that |m_newMediaKeys| is now considered reserved for
// |m_element|, so it needs to be accepted or cancelled.
made_reservation_ = true;
}
// 5.2 If the mediaKeys attribute is not null, run the following steps:
if (this_element.media_keys_) {
WebMediaPlayer* media_player = element_->GetWebMediaPlayer();
if (media_player) {
// 5.2.1 If the user agent or CDM do not support removing the
// association, let this object's attaching media keys
// value be false and reject promise with a NotSupportedError.
// 5.2.2 If the association cannot currently be removed,
// let this object's attaching media keys value be false
// and reject promise with an InvalidStateError.
// 5.2.3 Stop using the CDM instance represented by the mediaKeys
// attribute to decrypt media data and remove the association
// with the media element.
// (All 3 steps handled as needed in Chromium.)
SuccessCallback success_callback = WTF::Bind(
&SetMediaKeysHandler::SetNewMediaKeys, WrapPersistent(this));
FailureCallback failure_callback =
WTF::Bind(&SetMediaKeysHandler::ClearFailed, WrapPersistent(this));
ContentDecryptionModuleResult* result =
new SetContentDecryptionModuleResult(std::move(success_callback),
std::move(failure_callback));
media_player->SetContentDecryptionModule(nullptr, result->Result());
// Don't do anything more until |result| is resolved (or rejected).
return;
}
}
// MediaKeys not currently set or no player connected, so continue on.
SetNewMediaKeys();
}
void SetMediaKeysHandler::SetNewMediaKeys() {
DVLOG(EME_LOG_LEVEL) << __func__;
// 5.3 If mediaKeys is not null, run the following steps:
if (new_media_keys_) {
// 5.3.1 Associate the CDM instance represented by mediaKeys with the
// media element for decrypting media data.
// 5.3.2 If the preceding step failed, run the following steps:
// (done in setFailed()).
// 5.3.3 Queue a task to run the Attempt to Resume Playback If Necessary
// algorithm on the media element.
// (Handled in Chromium).
if (element_->GetWebMediaPlayer()) {
SuccessCallback success_callback =
WTF::Bind(&SetMediaKeysHandler::Finish, WrapPersistent(this));
FailureCallback failure_callback =
WTF::Bind(&SetMediaKeysHandler::SetFailed, WrapPersistent(this));
ContentDecryptionModuleResult* result =
new SetContentDecryptionModuleResult(std::move(success_callback),
std::move(failure_callback));
element_->GetWebMediaPlayer()->SetContentDecryptionModule(
new_media_keys_->ContentDecryptionModule(), result->Result());
// Don't do anything more until |result| is resolved (or rejected).
return;
}
}
// MediaKeys doesn't need to be set on the player, so continue on.
Finish();
}
void SetMediaKeysHandler::Finish() {
DVLOG(EME_LOG_LEVEL) << __func__;
HTMLMediaElementEncryptedMedia& this_element =
HTMLMediaElementEncryptedMedia::From(*element_);
// 5.4 Set the mediaKeys attribute to mediaKeys.
if (this_element.media_keys_)
this_element.media_keys_->ClearMediaElement();
this_element.media_keys_ = new_media_keys_;
if (made_reservation_)
new_media_keys_->AcceptReservation();
// 5.5 Let this object's attaching media keys value be false.
this_element.is_attaching_media_keys_ = false;
// 5.6 Resolve promise with undefined.
Resolve();
}
void SetMediaKeysHandler::Fail(ExceptionCode code,
const String& error_message) {
// Reset ownership of |m_newMediaKeys|.
if (made_reservation_)
new_media_keys_->CancelReservation();
// Make sure attaching media keys value is false.
DCHECK(!HTMLMediaElementEncryptedMedia::From(*element_)
.is_attaching_media_keys_);
// Reject promise with an appropriate error.
ScriptState::Scope scope(GetScriptState());
v8::Isolate* isolate = GetScriptState()->GetIsolate();
Reject(V8ThrowDOMException::CreateDOMException(isolate, code, error_message));
}
void SetMediaKeysHandler::ClearFailed(ExceptionCode code,
const String& error_message) {
DVLOG(EME_LOG_LEVEL) << __func__ << "(" << code << ", " << error_message
<< ")";
HTMLMediaElementEncryptedMedia& this_element =
HTMLMediaElementEncryptedMedia::From(*element_);
// 5.2.4 If the preceding step failed, let this object's attaching media
// keys value be false and reject promise with an appropriate
// error name.
this_element.is_attaching_media_keys_ = false;
Fail(code, error_message);
}
void SetMediaKeysHandler::SetFailed(ExceptionCode code,
const String& error_message) {
DVLOG(EME_LOG_LEVEL) << __func__ << "(" << code << ", " << error_message
<< ")";
HTMLMediaElementEncryptedMedia& this_element =
HTMLMediaElementEncryptedMedia::From(*element_);
// 5.3.2 If the preceding step failed (in setContentDecryptionModule()
// called from setNewMediaKeys()), run the following steps:
// 5.3.2.1 Set the mediaKeys attribute to null.
this_element.media_keys_.Clear();
// 5.3.2.2 Let this object's attaching media keys value be false.
this_element.is_attaching_media_keys_ = false;
// 5.3.2.3 Reject promise with a new DOMException whose name is the
// appropriate error name.
Fail(code, error_message);
}
void SetMediaKeysHandler::Trace(blink::Visitor* visitor) {
visitor->Trace(element_);
visitor->Trace(new_media_keys_);
ScriptPromiseResolver::Trace(visitor);
}
HTMLMediaElementEncryptedMedia::HTMLMediaElementEncryptedMedia(
HTMLMediaElement& element)
: media_element_(&element),
is_waiting_for_key_(false),
is_attaching_media_keys_(false) {}
HTMLMediaElementEncryptedMedia::~HTMLMediaElementEncryptedMedia() {
DVLOG(EME_LOG_LEVEL) << __func__;
}
const char* HTMLMediaElementEncryptedMedia::SupplementName() {
return "HTMLMediaElementEncryptedMedia";
}
HTMLMediaElementEncryptedMedia& HTMLMediaElementEncryptedMedia::From(
HTMLMediaElement& element) {
HTMLMediaElementEncryptedMedia* supplement =
static_cast<HTMLMediaElementEncryptedMedia*>(
Supplement<HTMLMediaElement>::From(element, SupplementName()));
if (!supplement) {
supplement = new HTMLMediaElementEncryptedMedia(element);
ProvideTo(element, SupplementName(), supplement);
}
return *supplement;
}
MediaKeys* HTMLMediaElementEncryptedMedia::mediaKeys(
HTMLMediaElement& element) {
HTMLMediaElementEncryptedMedia& this_element =
HTMLMediaElementEncryptedMedia::From(element);
return this_element.media_keys_.Get();
}
ScriptPromise HTMLMediaElementEncryptedMedia::setMediaKeys(
ScriptState* script_state,
HTMLMediaElement& element,
MediaKeys* media_keys) {
HTMLMediaElementEncryptedMedia& this_element =
HTMLMediaElementEncryptedMedia::From(element);
DVLOG(EME_LOG_LEVEL) << __func__ << ": current("
<< this_element.media_keys_.Get() << "), new("
<< media_keys << ")";
// From http://w3c.github.io/encrypted-media/#setMediaKeys
// 1. If this object's attaching media keys value is true, return a
// promise rejected with an InvalidStateError.
if (this_element.is_attaching_media_keys_) {
return ScriptPromise::RejectWithDOMException(
script_state, DOMException::Create(kInvalidStateError,
"Another request is in progress."));
}
// 2. If mediaKeys and the mediaKeys attribute are the same object,
// return a resolved promise.
if (this_element.media_keys_ == media_keys)
return ScriptPromise::CastUndefined(script_state);
// 3. Let this object's attaching media keys value be true.
this_element.is_attaching_media_keys_ = true;
// 4. Let promise be a new promise. Remaining steps done in handler.
return SetMediaKeysHandler::Create(script_state, element, media_keys);
}
// Create a MediaEncryptedEvent for WD EME.
static Event* CreateEncryptedEvent(WebEncryptedMediaInitDataType init_data_type,
const unsigned char* init_data,
unsigned init_data_length) {
MediaEncryptedEventInit initializer;
initializer.setInitDataType(
EncryptedMediaUtils::ConvertFromInitDataType(init_data_type));
initializer.setInitData(DOMArrayBuffer::Create(init_data, init_data_length));
initializer.setBubbles(false);
initializer.setCancelable(false);
return MediaEncryptedEvent::Create(EventTypeNames::encrypted, initializer);
}
void HTMLMediaElementEncryptedMedia::Encrypted(
WebEncryptedMediaInitDataType init_data_type,
const unsigned char* init_data,
unsigned init_data_length) {
DVLOG(EME_LOG_LEVEL) << __func__;
Event* event;
if (media_element_->IsMediaDataCORSSameOrigin(
media_element_->GetExecutionContext()->GetSecurityOrigin())) {
event = CreateEncryptedEvent(init_data_type, init_data, init_data_length);
} else {
// Current page is not allowed to see content from the media file,
// so don't return the initData. However, they still get an event.
event = CreateEncryptedEvent(WebEncryptedMediaInitDataType::kUnknown,
nullptr, 0);
media_element_->GetExecutionContext()->AddConsoleMessage(
ConsoleMessage::Create(kJSMessageSource, kWarningMessageLevel,
"Media element must be CORS-same-origin with "
"the embedding page. If cross-origin, you "
"should use the `crossorigin` attribute and "
"make sure CORS headers on the media data "
"response are CORS-same-origin."));
}
event->SetTarget(media_element_);
media_element_->ScheduleEvent(event);
}
void HTMLMediaElementEncryptedMedia::DidBlockPlaybackWaitingForKey() {
DVLOG(EME_LOG_LEVEL) << __func__;
// From https://w3c.github.io/encrypted-media/#queue-waitingforkey:
// It should only be called when the HTMLMediaElement object is potentially
// playing and its readyState is equal to HAVE_FUTURE_DATA or greater.
// FIXME: Is this really required?
// 1. Let the media element be the specified HTMLMediaElement object.
// 2. If the media element's waiting for key value is false, queue a task
// to fire a simple event named waitingforkey at the media element.
if (!is_waiting_for_key_) {
Event* event = Event::Create(EventTypeNames::waitingforkey);
event->SetTarget(media_element_);
media_element_->ScheduleEvent(event);
}
// 3. Set the media element's waiting for key value to true.
is_waiting_for_key_ = true;
// 4. Suspend playback.
// (Already done on the Chromium side by the decryptors.)
}
void HTMLMediaElementEncryptedMedia::DidResumePlaybackBlockedForKey() {
DVLOG(EME_LOG_LEVEL) << __func__;
// Logic is on the Chromium side to attempt to resume playback when a new
// key is available. However, |m_isWaitingForKey| needs to be cleared so
// that a later waitingForKey() call can generate the event.
is_waiting_for_key_ = false;
}
WebContentDecryptionModule*
HTMLMediaElementEncryptedMedia::ContentDecryptionModule() {
return media_keys_ ? media_keys_->ContentDecryptionModule() : nullptr;
}
void HTMLMediaElementEncryptedMedia::Trace(blink::Visitor* visitor) {
visitor->Trace(media_element_);
visitor->Trace(media_keys_);
Supplement<HTMLMediaElement>::Trace(visitor);
}
} // namespace blink