blob: 54417cea2cccef7a985df4030b40b4afee349a95 [file] [log] [blame]
/*
* Copyright (C) 2013 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "modules/encryptedmedia/MediaKeySession.h"
#include "bindings/core/v8/DOMWrapperWorld.h"
#include "bindings/core/v8/ScriptPromise.h"
#include "bindings/core/v8/ScriptPromiseResolver.h"
#include "bindings/core/v8/ScriptState.h"
#include "core/dom/DOMArrayBuffer.h"
#include "core/dom/DOMArrayBufferView.h"
#include "core/dom/ExceptionCode.h"
#include "core/events/Event.h"
#include "core/events/GenericEventQueue.h"
#include "core/html/MediaKeyError.h"
#include "modules/encryptedmedia/MediaKeyMessageEvent.h"
#include "modules/encryptedmedia/MediaKeys.h"
#include "modules/encryptedmedia/SimpleContentDecryptionModuleResult.h"
#include "platform/ContentDecryptionModuleResult.h"
#include "platform/ContentType.h"
#include "platform/Logging.h"
#include "platform/MIMETypeRegistry.h"
#include "platform/Timer.h"
#include "public/platform/WebContentDecryptionModule.h"
#include "public/platform/WebContentDecryptionModuleException.h"
#include "public/platform/WebContentDecryptionModuleSession.h"
#include "public/platform/WebString.h"
#include "public/platform/WebURL.h"
#include "wtf/ASCIICType.h"
#include <cmath>
#include <limits>
namespace {
// The list of possible values for |sessionType|.
const char* kTemporary = "temporary";
const char* kPersistent = "persistent";
// Minimum and maximum length for session ids.
enum {
MinSessionIdLength = 1,
MaxSessionIdLength = 512
};
} // namespace
namespace blink {
static bool isKeySystemSupportedWithInitDataType(const String& keySystem, const String& initDataType)
{
ASSERT(!keySystem.isEmpty());
// FIXME: initDataType != contentType. Implement this properly.
// http://crbug.com/385874.
String contentType = initDataType;
if (initDataType == "webm") {
contentType = "video/webm";
} else if (initDataType == "cenc") {
contentType = "video/mp4";
}
ContentType type(contentType);
return MIMETypeRegistry::isSupportedEncryptedMediaMIMEType(keySystem, type.type(), type.parameter("codecs"));
}
// Checks that |sessionId| looks correct and returns whether all checks pass.
static bool isValidSessionId(const String& sessionId)
{
if ((sessionId.length() < MinSessionIdLength) || (sessionId.length() > MaxSessionIdLength))
return false;
if (!sessionId.containsOnlyASCII())
return false;
// Check that the sessionId only contains alphanumeric characters.
for (unsigned i = 0; i < sessionId.length(); ++i) {
if (!isASCIIAlphanumeric(sessionId[i]))
return false;
}
return true;
}
// A class holding a pending action.
class MediaKeySession::PendingAction : public GarbageCollectedFinalized<MediaKeySession::PendingAction> {
public:
enum Type {
GenerateRequest,
Load,
Update,
Close,
Remove
};
Type type() const { return m_type; }
const Persistent<ContentDecryptionModuleResult> result() const
{
return m_result;
}
const PassRefPtr<DOMArrayBuffer> data() const
{
ASSERT(m_type == GenerateRequest || m_type == Update);
return m_data;
}
const String& initDataType() const
{
ASSERT(m_type == GenerateRequest);
return m_stringData;
}
const String& sessionId() const
{
ASSERT(m_type == Load);
return m_stringData;
}
static PendingAction* CreatePendingGenerateRequest(ContentDecryptionModuleResult* result, const String& initDataType, PassRefPtr<DOMArrayBuffer> initData)
{
ASSERT(result);
ASSERT(initData);
return new PendingAction(GenerateRequest, result, initDataType, initData);
}
static PendingAction* CreatePendingLoadRequest(ContentDecryptionModuleResult* result, const String& sessionId)
{
ASSERT(result);
return new PendingAction(Load, result, sessionId, PassRefPtr<DOMArrayBuffer>());
}
static PendingAction* CreatePendingUpdate(ContentDecryptionModuleResult* result, PassRefPtr<DOMArrayBuffer> data)
{
ASSERT(result);
ASSERT(data);
return new PendingAction(Update, result, String(), data);
}
static PendingAction* CreatePendingClose(ContentDecryptionModuleResult* result)
{
ASSERT(result);
return new PendingAction(Close, result, String(), PassRefPtr<DOMArrayBuffer>());
}
static PendingAction* CreatePendingRemove(ContentDecryptionModuleResult* result)
{
ASSERT(result);
return new PendingAction(Remove, result, String(), PassRefPtr<DOMArrayBuffer>());
}
~PendingAction()
{
}
void trace(Visitor* visitor)
{
visitor->trace(m_result);
}
private:
PendingAction(Type type, ContentDecryptionModuleResult* result, const String& stringData, PassRefPtr<DOMArrayBuffer> data)
: m_type(type)
, m_result(result)
, m_stringData(stringData)
, m_data(data)
{
}
const Type m_type;
const Member<ContentDecryptionModuleResult> m_result;
const String m_stringData;
const RefPtr<DOMArrayBuffer> m_data;
};
// This class wraps the promise resolver used when initializing a new session
// and is passed to Chromium to fullfill the promise. This implementation of
// completeWithSession() will resolve the promise with void, while
// completeWithError() will reject the promise with an exception. complete()
// is not expected to be called, and will reject the promise.
class NewSessionResult : public ContentDecryptionModuleResult {
public:
NewSessionResult(ScriptState* scriptState, MediaKeySession* session)
: m_resolver(ScriptPromiseResolver::create(scriptState))
, m_session(session)
{
WTF_LOG(Media, "NewSessionResult(%p)", this);
}
virtual ~NewSessionResult()
{
WTF_LOG(Media, "~NewSessionResult(%p)", this);
}
// ContentDecryptionModuleResult implementation.
virtual void complete() override
{
ASSERT_NOT_REACHED();
completeWithDOMException(InvalidStateError, "Unexpected completion.");
}
virtual void completeWithSession(WebContentDecryptionModuleResult::SessionStatus status) override
{
if (status != WebContentDecryptionModuleResult::NewSession) {
ASSERT_NOT_REACHED();
completeWithDOMException(InvalidStateError, "Unexpected completion.");
}
m_session->finishGenerateRequest();
m_resolver->resolve();
m_resolver.clear();
}
virtual void completeWithError(WebContentDecryptionModuleException exceptionCode, unsigned long systemCode, const WebString& errorMessage) override
{
completeWithDOMException(WebCdmExceptionToExceptionCode(exceptionCode), errorMessage);
}
// It is only valid to call this before completion.
ScriptPromise promise() { return m_resolver->promise(); }
void trace(Visitor* visitor)
{
visitor->trace(m_session);
ContentDecryptionModuleResult::trace(visitor);
}
private:
// Reject the promise with a DOMException.
void completeWithDOMException(ExceptionCode code, const String& errorMessage)
{
m_resolver->reject(DOMException::create(code, errorMessage));
m_resolver.clear();
}
RefPtr<ScriptPromiseResolver> m_resolver;
Member<MediaKeySession> m_session;
};
// This class wraps the promise resolver used when loading a session
// and is passed to Chromium to fullfill the promise. This implementation of
// completeWithSession() will resolve the promise with true/false, while
// completeWithError() will reject the promise with an exception. complete()
// is not expected to be called, and will reject the promise.
class LoadSessionResult : public ContentDecryptionModuleResult {
public:
LoadSessionResult(ScriptState* scriptState, MediaKeySession* session)
: m_resolver(ScriptPromiseResolver::create(scriptState))
, m_session(session)
{
WTF_LOG(Media, "LoadSessionResult(%p)", this);
}
virtual ~LoadSessionResult()
{
WTF_LOG(Media, "~LoadSessionResult(%p)", this);
}
// ContentDecryptionModuleResult implementation.
virtual void complete() override
{
ASSERT_NOT_REACHED();
completeWithDOMException(InvalidStateError, "Unexpected completion.");
}
virtual void completeWithSession(WebContentDecryptionModuleResult::SessionStatus status) override
{
bool result = false;
switch (status) {
case WebContentDecryptionModuleResult::NewSession:
result = true;
break;
case WebContentDecryptionModuleResult::SessionNotFound:
result = false;
break;
case WebContentDecryptionModuleResult::SessionAlreadyExists:
ASSERT_NOT_REACHED();
completeWithDOMException(InvalidStateError, "Unexpected completion.");
return;
}
m_session->finishLoad();
m_resolver->resolve(result);
m_resolver.clear();
}
virtual void completeWithError(WebContentDecryptionModuleException exceptionCode, unsigned long systemCode, const WebString& errorMessage) override
{
completeWithDOMException(WebCdmExceptionToExceptionCode(exceptionCode), errorMessage);
}
// It is only valid to call this before completion.
ScriptPromise promise() { return m_resolver->promise(); }
void trace(Visitor* visitor)
{
visitor->trace(m_session);
ContentDecryptionModuleResult::trace(visitor);
}
private:
// Reject the promise with a DOMException.
void completeWithDOMException(ExceptionCode code, const String& errorMessage)
{
m_resolver->reject(DOMException::create(code, errorMessage));
m_resolver.clear();
}
RefPtr<ScriptPromiseResolver> m_resolver;
Member<MediaKeySession> m_session;
};
MediaKeySession* MediaKeySession::create(ScriptState* scriptState, MediaKeys* mediaKeys, const String& sessionType)
{
ASSERT(sessionType == kTemporary || sessionType == kPersistent);
RefPtrWillBeRawPtr<MediaKeySession> session = new MediaKeySession(scriptState, mediaKeys, sessionType);
session->suspendIfNeeded();
return session.get();
}
bool MediaKeySession::isValidSessionType(const String& sessionType)
{
return (sessionType == kTemporary || sessionType == kPersistent);
}
MediaKeySession::MediaKeySession(ScriptState* scriptState, MediaKeys* mediaKeys, const String& sessionType)
: ActiveDOMObject(scriptState->executionContext())
, m_keySystem(mediaKeys->keySystem())
, m_asyncEventQueue(GenericEventQueue::create(this))
, m_mediaKeys(mediaKeys)
, m_sessionType(sessionType)
, m_expiration(std::numeric_limits<double>::quiet_NaN())
, m_isUninitialized(true)
, m_isCallable(false)
, m_isClosed(false)
, m_closedPromise(new ClosedPromise(scriptState->executionContext(), this, ClosedPromise::Closed))
, m_actionTimer(this, &MediaKeySession::actionTimerFired)
{
WTF_LOG(Media, "MediaKeySession(%p)::MediaKeySession", this);
// Create the matching Chromium object. It will not be usable until
// initializeNewSession() is called in response to the user calling
// generateRequest().
WebContentDecryptionModule* cdm = mediaKeys->contentDecryptionModule();
m_session = adoptPtr(cdm->createSession());
m_session->setClientInterface(this);
// MediaKeys::createSession(), step 2.
// 2.1 Let the sessionId attribute be the empty string.
ASSERT(sessionId().isEmpty());
// 2.2 Let the expiration attribute be NaN.
ASSERT(std::isnan(m_expiration));
// 2.3 Let the closed attribute be a new promise.
ASSERT(!closed(scriptState).isUndefinedOrNull());
// 2.4 Let the session type be sessionType.
ASSERT(isValidSessionType(sessionType));
// 2.5 Let uninitialized be true.
ASSERT(m_isUninitialized);
// 2.6 Let callable be false.
ASSERT(!m_isCallable);
}
MediaKeySession::~MediaKeySession()
{
WTF_LOG(Media, "MediaKeySession(%p)::~MediaKeySession", this);
m_session.clear();
#if !ENABLE(OILPAN)
// MediaKeySession and m_asyncEventQueue always become unreachable
// together. So MediaKeySession and m_asyncEventQueue are destructed in the
// same GC. We don't need to call cancelAllEvents explicitly in Oilpan.
m_asyncEventQueue->cancelAllEvents();
#endif
}
void MediaKeySession::setError(MediaKeyError* error)
{
m_error = error;
}
String MediaKeySession::sessionId() const
{
return m_session->sessionId();
}
ScriptPromise MediaKeySession::closed(ScriptState* scriptState)
{
return m_closedPromise->promise(scriptState->world());
}
ScriptPromise MediaKeySession::generateRequest(ScriptState* scriptState, const String& initDataType, DOMArrayBuffer* initData)
{
RefPtr<DOMArrayBuffer> initDataCopy = DOMArrayBuffer::create(initData->data(), initData->byteLength());
return generateRequestInternal(scriptState, initDataType, initDataCopy.release());
}
ScriptPromise MediaKeySession::generateRequest(ScriptState* scriptState, const String& initDataType, DOMArrayBufferView* initData)
{
RefPtr<DOMArrayBuffer> initDataCopy = DOMArrayBuffer::create(initData->baseAddress(), initData->byteLength());
return generateRequestInternal(scriptState, initDataType, initDataCopy.release());
}
ScriptPromise MediaKeySession::generateRequestInternal(ScriptState* scriptState, const String& initDataType, PassRefPtr<DOMArrayBuffer> initData)
{
WTF_LOG(Media, "MediaKeySession(%p)::generateRequest %s", this, initDataType.ascii().data());
// From https://dvcs.w3.org/hg/html-media/raw-file/default/encrypted-media/encrypted-media.html#dom-generaterequest:
// The generateRequest(initDataType, initData) method creates a new session
// for the specified initData. It must run the following steps:
// 1. If this object's uninitialized value is false, return a promise
// rejected with a new DOMException whose name is "InvalidStateError".
if (!m_isUninitialized) {
return ScriptPromise::rejectWithDOMException(
scriptState, DOMException::create(InvalidStateError, "The session is already initialized."));
}
// 2. Let this object's uninitialized be false.
m_isUninitialized = false;
// 3. If initDataType is an empty string, return a promise rejected with a
// new DOMException whose name is "InvalidAccessError".
if (initDataType.isEmpty()) {
return ScriptPromise::rejectWithDOMException(
scriptState, DOMException::create(InvalidAccessError, "The initDataType parameter is empty."));
}
// 4. If initData is an empty array, return a promise rejected with a new
// DOMException whose name is"InvalidAccessError".
if (!initData->byteLength()) {
return ScriptPromise::rejectWithDOMException(
scriptState, DOMException::create(InvalidAccessError, "The initData parameter is empty."));
}
// 5. Let media keys be the MediaKeys object that created this object.
// (Use m_mediaKey, which was set in the constructor.)
// 6. If the content decryption module corresponding to media keys's
// keySystem attribute does not support initDataType as an initialization
// data type, return a promise rejected with a new DOMException whose
// name is "NotSupportedError". String comparison is case-sensitive.
if (!isKeySystemSupportedWithInitDataType(m_keySystem, initDataType)) {
return ScriptPromise::rejectWithDOMException(
scriptState, DOMException::create(NotSupportedError, "The initialization data type '" + initDataType + "' is not supported by the key system."));
}
// 7. Let init data be a copy of the contents of the initData parameter.
// (Done before calling this method.)
// 8. Let session type be this object's session type.
// (Done in constructor.)
// 9. Let promise be a new promise.
NewSessionResult* result = new NewSessionResult(scriptState, this);
ScriptPromise promise = result->promise();
// 10. Run the following steps asynchronously (documented in
// actionTimerFired())
m_pendingActions.append(PendingAction::CreatePendingGenerateRequest(result, initDataType, initData));
ASSERT(!m_actionTimer.isActive());
m_actionTimer.startOneShot(0, FROM_HERE);
// 11. Return promise.
return promise;
}
ScriptPromise MediaKeySession::load(ScriptState* scriptState, const String& sessionId)
{
WTF_LOG(Media, "MediaKeySession(%p)::load %s", this, sessionId.ascii().data());
// From https://dvcs.w3.org/hg/html-media/raw-file/default/encrypted-media/encrypted-media.html#dom-load:
// The load(sessionId) method loads the data stored for the sessionId into
// the session represented by the object. It must run the following steps:
// 1. If this object's uninitialized value is false, return a promise
// rejected with a new DOMException whose name is "InvalidStateError".
if (!m_isUninitialized) {
return ScriptPromise::rejectWithDOMException(
scriptState, DOMException::create(InvalidStateError, "The session is already initialized."));
}
// 2. Let this object's uninitialized be false.
m_isUninitialized = false;
// 3. If sessionId is an empty string, return a promise rejected with a
// new DOMException whose name is "InvalidAccessError".
if (sessionId.isEmpty()) {
return ScriptPromise::rejectWithDOMException(
scriptState, DOMException::create(InvalidAccessError, "The sessionId parameter is empty."));
}
// 4. If this object's session type is not "persistent", return a promise
// rejected with a new DOMException whose name is "InvalidAccessError".
if (m_sessionType != kPersistent) {
return ScriptPromise::rejectWithDOMException(
scriptState, DOMException::create(InvalidAccessError, "The session type is not 'persistent'."));
}
// 5. Let media keys be the MediaKeys object that created this object.
// (Done in constructor.)
ASSERT(m_mediaKeys);
// 6. If the content decryption module corresponding to media keys's
// keySystem attribute does not support loading previous sessions,
// return a promise rejected with a new DOMException whose name is
// "NotSupportedError".
// (Done by CDM.)
// 7. Let promise be a new promise.
LoadSessionResult* result = new LoadSessionResult(scriptState, this);
ScriptPromise promise = result->promise();
// 8. Run the following steps asynchronously (documented in
// actionTimerFired())
m_pendingActions.append(PendingAction::CreatePendingLoadRequest(result, sessionId));
ASSERT(!m_actionTimer.isActive());
m_actionTimer.startOneShot(0, FROM_HERE);
// 9. Return promise.
return promise;
}
ScriptPromise MediaKeySession::update(ScriptState* scriptState, DOMArrayBuffer* response)
{
RefPtr<DOMArrayBuffer> responseCopy = DOMArrayBuffer::create(response->data(), response->byteLength());
return updateInternal(scriptState, responseCopy.release());
}
ScriptPromise MediaKeySession::update(ScriptState* scriptState, DOMArrayBufferView* response)
{
RefPtr<DOMArrayBuffer> responseCopy = DOMArrayBuffer::create(response->baseAddress(), response->byteLength());
return updateInternal(scriptState, responseCopy.release());
}
ScriptPromise MediaKeySession::updateInternal(ScriptState* scriptState, PassRefPtr<DOMArrayBuffer> response)
{
WTF_LOG(Media, "MediaKeySession(%p)::update", this);
ASSERT(!m_isClosed);
// From <https://dvcs.w3.org/hg/html-media/raw-file/default/encrypted-media/encrypted-media.html#dom-update>:
// The update(response) method provides messages, including licenses, to the
// CDM. It must run the following steps:
//
// 1. If response is an empty array, return a promise rejected with a new
// DOMException whose name is "InvalidAccessError" and that has the
// message "The response parameter is empty."
if (!response->byteLength()) {
return ScriptPromise::rejectWithDOMException(
scriptState, DOMException::create(InvalidAccessError, "The response parameter is empty."));
}
// 2. Let message be a copy of the contents of the response parameter.
// (Copied in the caller.)
// 3. Let promise be a new promise.
SimpleContentDecryptionModuleResult* result = new SimpleContentDecryptionModuleResult(scriptState);
ScriptPromise promise = result->promise();
// 4. Run the following steps asynchronously (documented in
// actionTimerFired())
m_pendingActions.append(PendingAction::CreatePendingUpdate(result, response));
if (!m_actionTimer.isActive())
m_actionTimer.startOneShot(0, FROM_HERE);
// 5. Return promise.
return promise;
}
ScriptPromise MediaKeySession::close(ScriptState* scriptState)
{
WTF_LOG(Media, "MediaKeySession(%p)::close", this);
// From https://dvcs.w3.org/hg/html-media/raw-file/default/encrypted-media/encrypted-media.html#dom-close:
// The close() method allows an application to indicate that it no longer
// needs the session and the CDM should release any resources associated
// with this object and close it. The returned promise is resolved when the
// request has been processed, and the closed attribute promise is resolved
// when the session is closed. It must run the following steps:
//
// 1. If this object's callable value is false, return a promise rejected
// with a new DOMException whose name is "InvalidStateError".
if (!m_isCallable) {
return ScriptPromise::rejectWithDOMException(
scriptState, DOMException::create(InvalidStateError, "The session is not callable."));
}
// 2. If the Session Close algorithm has been run on this object,
// return a resolved promise.
if (m_isClosed)
return ScriptPromise::cast(scriptState, ScriptValue());
// 3. Let promise be a new promise.
SimpleContentDecryptionModuleResult* result = new SimpleContentDecryptionModuleResult(scriptState);
ScriptPromise promise = result->promise();
// 4. Run the following steps asynchronously (documented in
// actionTimerFired()).
m_pendingActions.append(PendingAction::CreatePendingClose(result));
if (!m_actionTimer.isActive())
m_actionTimer.startOneShot(0, FROM_HERE);
// 5. Return promise.
return promise;
}
ScriptPromise MediaKeySession::remove(ScriptState* scriptState)
{
WTF_LOG(Media, "MediaKeySession(%p)::remove", this);
// From https://dvcs.w3.org/hg/html-media/raw-file/default/encrypted-media/encrypted-media.html#dom-remove:
// The remove() method allows an application to remove stored session data
// associated with this object. It must run the following steps:
// 1. If this object's callable value is false, return a promise rejected
// with a new DOMException whose name is "InvalidStateError".
if (!m_isCallable) {
return ScriptPromise::rejectWithDOMException(
scriptState, DOMException::create(InvalidStateError, "The session is not callable."));
}
// 2. If this object's session type is not "persistent", return a promise
// rejected with a new DOMException whose name is "InvalidAccessError".
if (m_sessionType != kPersistent) {
return ScriptPromise::rejectWithDOMException(
scriptState, DOMException::create(InvalidAccessError, "The session type is not 'persistent'."));
}
// 3. If the Session Close algorithm has been run on this object, return a
// promise rejected with a new DOMException whose name is
// "InvalidStateError".
if (m_isClosed) {
return ScriptPromise::rejectWithDOMException(
scriptState, DOMException::create(InvalidStateError, "The session is already closed."));
}
// 4. Let promise be a new promise.
SimpleContentDecryptionModuleResult* result = new SimpleContentDecryptionModuleResult(scriptState);
ScriptPromise promise = result->promise();
// 5. Run the following steps asynchronously (documented in
// actionTimerFired()).
m_pendingActions.append(PendingAction::CreatePendingRemove(result));
if (!m_actionTimer.isActive())
m_actionTimer.startOneShot(0, FROM_HERE);
// 6. Return promise.
return promise;
}
void MediaKeySession::actionTimerFired(Timer<MediaKeySession>*)
{
ASSERT(m_pendingActions.size());
// Resolving promises now run synchronously and may result in additional
// actions getting added to the queue. As a result, swap the queue to
// a local copy to avoid problems if this happens.
HeapDeque<Member<PendingAction> > pendingActions;
pendingActions.swap(m_pendingActions);
while (!pendingActions.isEmpty()) {
PendingAction* action = pendingActions.takeFirst();
switch (action->type()) {
case PendingAction::GenerateRequest:
WTF_LOG(Media, "MediaKeySession(%p)::actionTimerFired: GenerateRequest", this);
// 10.1 Let request be null.
// 10.2 Let cdm be the CDM loaded during the initialization of
// media keys.
// 10.3 Use the cdm to execute the following steps:
// 10.3.1 If the init data is not valid for initDataType, reject
// promise with a new DOMException whose name is
// "InvalidAccessError".
// 10.3.2 If the init data is not supported by the cdm, reject
// promise with a new DOMException whose name is
// "NotSupportedError".
// 10.3.3 Let request be a request (e.g. a license request)
// generated based on the init data, which is interpreted
// per initDataType, and session type.
m_session->initializeNewSession(action->initDataType(), static_cast<unsigned char*>(action->data()->data()), action->data()->byteLength(), m_sessionType, action->result()->result());
// Remainder of steps executed in finishGenerateRequest(), called
// when |result| is resolved.
break;
case PendingAction::Load:
// NOTE: Continue step 8 of MediaKeySession::load().
// 8.1 Let sanitized session ID be a validated and/or sanitized
// version of sessionId. The user agent should thoroughly
// validate the sessionId value before passing it to the CDM.
// At a minimum, this should include checking that the length
// and value (e.g. alphanumeric) are reasonable.
// 8.2 If the previous step failed, reject promise with a new
// DOMException whose name is "InvalidAccessError".
if (!isValidSessionId(action->sessionId())) {
action->result()->completeWithError(WebContentDecryptionModuleExceptionInvalidAccessError, 0, "Invalid sessionId");
return;
}
// 8.3 Let expiration time be NaN.
// (Done in the constructor.)
ASSERT(std::isnan(m_expiration));
// 8.4 Let message be null.
// 8.5 Let message type be null.
// (Will be provided by the CDM if needed.)
// 8.6 Let origin be the origin of this object's Document.
// (Obtained previously when CDM created.)
// 8.7 Let cdm be the CDM loaded during the initialization of media
// keys.
// 8.8 Use the cdm to execute the following steps:
// 8.8.1 If there is no data stored for the sanitized session ID in
// the origin, resolve promise with false.
// 8.8.2 Let session data be the data stored for the sanitized
// session ID in the origin. This must not include data from
// other origin(s) or that is not associated with an origin.
// 8.8.3 If there is an unclosed "persistent" session in any
// Document representing the session data, reject promise
// with a new DOMException whose name is "QuotaExceededError".
// 8.8.4 In other words, do not create a session if a non-closed
// persistent session already exists for this sanitized
// session ID in any browsing context.
// 8.8.5 Load the session data.
// 8.8.6 If the session data indicates an expiration time for the
// session, let expiration time be the expiration time
// in milliseconds since 01 January 1970 UTC.
// 8.8.6 If the CDM needs to send a message:
// 8.8.6.1 Let message be a message generated by the CDM based on
// the session data.
// 8.8.6.2 Let message type be the appropriate MediaKeyMessageType
// for the message.
// 8.9 If any of the preceding steps failed, reject promise with a
// new DOMException whose name is the appropriate error name.
m_session->load(action->sessionId(), action->result()->result());
// Remainder of steps executed in finishLoad(), called
// when |result| is resolved.
break;
case PendingAction::Update:
WTF_LOG(Media, "MediaKeySession(%p)::actionTimerFired: Update", this);
// NOTE: Continued from step 4 of MediaKeySession::update().
// Continue the update call by passing message to the cdm. Once
// completed, it will resolve/reject the promise.
m_session->update(static_cast<unsigned char*>(action->data()->data()), action->data()->byteLength(), action->result()->result());
break;
case PendingAction::Close:
WTF_LOG(Media, "MediaKeySession(%p)::actionTimerFired: Close", this);
// NOTE: Continued from step 4 of MediaKeySession::close().
// 4.1 Let cdm be the CDM loaded during the initialization of the
// MediaKeys object that created this object.
// (Already captured when creating m_session).
// 4.2 Use the cdm to execute the following steps:
// 4.2.1 Process the close request. Do not remove stored session
// data.
// 4.2.3 If the previous step caused the session to be closed,
// run the Session Close algorithm on this object.
// 4.3 Resolve promise.
m_session->close(action->result()->result());
break;
case PendingAction::Remove:
WTF_LOG(Media, "MediaKeySession(%p)::actionTimerFired: Remove", this);
// NOTE: Continued from step 5 of MediaKeySession::remove().
// 5.1 Let cdm be the CDM loaded during the initialization of the
// MediaKeys object that created this object.
// (Already captured when creating m_session).
// 5.2 Use the cdm to execute the following steps:
// 5.2.1 Process the remove request. This may involve exchanging
// message(s) with the application. Unless this step fails,
// the CDM must have cleared all stored session data
// associated with this object, including the sessionId,
// before proceeding to the next step. (A subsequent call
// to load() with sessionId would fail because there is no
// data stored for the sessionId.)
// 5.3 Run the following steps asynchronously once the above step
// has completed:
// 5.3.1 If any of the preceding steps failed, reject promise
// with a new DOMException whose name is the appropriate
// error name.
// 5.3.2 Run the Session Close algorithm on this object.
// 5.3.3 Resolve promise.
m_session->remove(action->result()->result());
break;
}
}
}
void MediaKeySession::finishGenerateRequest()
{
// 10.4 Set the sessionId attribute to a unique Session ID string.
// It may be obtained from cdm.
ASSERT(!sessionId().isEmpty());
// 10.5 If any of the preceding steps failed, reject promise with a new
// DOMException whose name is the appropriate error name.
// (Done by call to completeWithError()).
// 10.6 Add an entry for the value of the sessionId attribute to
// media keys's list of active session IDs.
// FIXME: Is this required?
// https://www.w3.org/Bugs/Public/show_bug.cgi?id=26758
// 10.7 Run the Queue a "message" Event algorithm on the session,
// providing request and null.
// (Done by the CDM).
// 10.8 Let this object's callable be true.
m_isCallable = true;
}
void MediaKeySession::finishLoad()
{
// 8.10 Set the sessionId attribute to sanitized session ID.
ASSERT(!sessionId().isEmpty());
// 8.11 Let this object's callable be true.
m_isCallable = true;
// 8.12 If the loaded session contains usable keys, run the Usable
// Keys Changed algorithm on the session. The algorithm may
// also be run later should additional processing be necessary
// to determine with certainty whether one or more keys is
// usable.
// (Done by the CDM.)
// 8.13 Run the Update Expiration algorithm on the session,
// providing expiration time.
// (Done by the CDM.)
// 8.14 If message is not null, run the Queue a "message" Event
// algorithm on the session, providing message type and
// message.
// (Done by the CDM.)
}
// Queue a task to fire a simple event named keymessage at the new object.
void MediaKeySession::message(const unsigned char* message, size_t messageLength, const WebURL& destinationURL)
{
WTF_LOG(Media, "MediaKeySession(%p)::message", this);
// Verify that 'message' not fired before session initialization is complete.
ASSERT(m_isCallable);
MediaKeyMessageEventInit init;
init.bubbles = false;
init.cancelable = false;
init.message = DOMArrayBuffer::create(static_cast<const void*>(message), messageLength);
init.destinationURL = destinationURL.string();
RefPtrWillBeRawPtr<MediaKeyMessageEvent> event = MediaKeyMessageEvent::create(EventTypeNames::message, init);
event->setTarget(this);
m_asyncEventQueue->enqueueEvent(event.release());
}
void MediaKeySession::ready()
{
WTF_LOG(Media, "MediaKeySession(%p)::ready", this);
RefPtrWillBeRawPtr<Event> event = Event::create(EventTypeNames::ready);
event->setTarget(this);
m_asyncEventQueue->enqueueEvent(event.release());
}
void MediaKeySession::close()
{
WTF_LOG(Media, "MediaKeySession(%p)::close", this);
// Once closed, the session can no longer be the target of events from
// the CDM so this object can be garbage collected.
m_isClosed = true;
// Resolve the closed promise.
m_closedPromise->resolve(V8UndefinedType());
}
// Queue a task to fire a simple event named keyadded at the MediaKeySession object.
void MediaKeySession::error(MediaKeyErrorCode errorCode, unsigned long systemCode)
{
WTF_LOG(Media, "MediaKeySession(%p)::error: errorCode=%d, systemCode=%lu", this, errorCode, systemCode);
MediaKeyError::Code mediaKeyErrorCode = MediaKeyError::MEDIA_KEYERR_UNKNOWN;
switch (errorCode) {
case MediaKeyErrorCodeUnknown:
mediaKeyErrorCode = MediaKeyError::MEDIA_KEYERR_UNKNOWN;
break;
case MediaKeyErrorCodeClient:
mediaKeyErrorCode = MediaKeyError::MEDIA_KEYERR_CLIENT;
break;
}
// 1. Create a new MediaKeyError object with the following attributes:
// code = the appropriate MediaKeyError code
// systemCode = a Key System-specific value, if provided, and 0 otherwise
// 2. Set the MediaKeySession object's error attribute to the error object created in the previous step.
m_error = MediaKeyError::create(mediaKeyErrorCode, systemCode);
// 3. queue a task to fire a simple event named keyerror at the MediaKeySession object.
RefPtrWillBeRawPtr<Event> event = Event::create(EventTypeNames::error);
event->setTarget(this);
m_asyncEventQueue->enqueueEvent(event.release());
}
void MediaKeySession::error(WebContentDecryptionModuleException exception, unsigned long systemCode, const WebString& errorMessage)
{
WTF_LOG(Media, "MediaKeySession::error: exception=%d, systemCode=%lu", exception, systemCode);
// FIXME: EME-WD MediaKeyError now derives from DOMException. Figure out how
// to implement this without breaking prefixed EME, which has a totally
// different definition. The spec may also change to be just a DOMException.
// For now, simply generate an existing MediaKeyError.
MediaKeyErrorCode errorCode;
switch (exception) {
case WebContentDecryptionModuleExceptionClientError:
errorCode = MediaKeyErrorCodeClient;
break;
default:
// All other exceptions get converted into Unknown.
errorCode = MediaKeyErrorCodeUnknown;
break;
}
error(errorCode, systemCode);
}
void MediaKeySession::expirationChanged(double updatedExpiryTimeInMS)
{
m_expiration = updatedExpiryTimeInMS;
}
const AtomicString& MediaKeySession::interfaceName() const
{
return EventTargetNames::MediaKeySession;
}
ExecutionContext* MediaKeySession::executionContext() const
{
return ActiveDOMObject::executionContext();
}
bool MediaKeySession::hasPendingActivity() const
{
// Remain around if there are pending events or MediaKeys is still around
// and we're not closed.
WTF_LOG(Media, "MediaKeySession(%p)::hasPendingActivity %s%s%s%s", this,
ActiveDOMObject::hasPendingActivity() ? " ActiveDOMObject::hasPendingActivity()" : "",
!m_pendingActions.isEmpty() ? " !m_pendingActions.isEmpty()" : "",
m_asyncEventQueue->hasPendingEvents() ? " m_asyncEventQueue->hasPendingEvents()" : "",
(m_mediaKeys && !m_isClosed) ? " m_mediaKeys && !m_isClosed" : "");
return ActiveDOMObject::hasPendingActivity()
|| !m_pendingActions.isEmpty()
|| m_asyncEventQueue->hasPendingEvents()
|| (m_mediaKeys && !m_isClosed);
}
void MediaKeySession::stop()
{
// Stop the CDM from firing any more events for this session.
m_session.clear();
m_isClosed = true;
if (m_actionTimer.isActive())
m_actionTimer.stop();
m_pendingActions.clear();
m_asyncEventQueue->close();
}
void MediaKeySession::trace(Visitor* visitor)
{
visitor->trace(m_error);
visitor->trace(m_asyncEventQueue);
visitor->trace(m_pendingActions);
visitor->trace(m_mediaKeys);
visitor->trace(m_closedPromise);
EventTargetWithInlineData::trace(visitor);
}
} // namespace blink