blob: f01ae7fbfefd7c3e8d4fe700314d18241b7807b7 [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 "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/DOMException.h"
#include "core/dom/ExceptionCode.h"
#include "core/events/Event.h"
#include "core/events/GenericEventQueue.h"
#include "core/html/MediaKeyError.h"
#include "modules/encryptedmedia/ContentDecryptionModuleResultPromise.h"
#include "modules/encryptedmedia/EncryptedMediaUtils.h"
#include "modules/encryptedmedia/MediaKeyMessageEvent.h"
#include "modules/encryptedmedia/MediaKeys.h"
#include "modules/encryptedmedia/SimpleContentDecryptionModuleResultPromise.h"
#include "platform/ContentDecryptionModuleResult.h"
#include "platform/ContentType.h"
#include "platform/Logging.h"
#include "platform/Timer.h"
#include "public/platform/WebContentDecryptionModule.h"
#include "public/platform/WebContentDecryptionModuleException.h"
#include "public/platform/WebContentDecryptionModuleSession.h"
#include "public/platform/WebEncryptedMediaKeyInformation.h"
#include "public/platform/WebString.h"
#include "public/platform/WebURL.h"
#include "wtf/ASCIICType.h"
#include <cmath>
#include <limits>
namespace {
// Minimum and maximum length for session ids.
enum {
MinSessionIdLength = 1,
MaxSessionIdLength = 512
};
} // namespace
namespace blink {
// 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;
}
static String ConvertKeyStatusToString(const WebEncryptedMediaKeyInformation::KeyStatus status)
{
switch (status) {
case WebEncryptedMediaKeyInformation::KeyStatus::Usable:
return "usable";
case WebEncryptedMediaKeyInformation::KeyStatus::Expired:
return "expired";
case WebEncryptedMediaKeyInformation::KeyStatus::Released:
return "released";
case WebEncryptedMediaKeyInformation::KeyStatus::OutputRestricted:
return "output-restricted";
case WebEncryptedMediaKeyInformation::KeyStatus::OutputDownscaled:
return "output-downscaled";
case WebEncryptedMediaKeyInformation::KeyStatus::StatusPending:
return "status-pending";
case WebEncryptedMediaKeyInformation::KeyStatus::InternalError:
return "internal-error";
}
ASSERT_NOT_REACHED();
return "internal-error";
}
static ScriptPromise CreateRejectedPromiseNotCallable(ScriptState* scriptState)
{
return ScriptPromise::rejectWithDOMException(
scriptState, DOMException::create(InvalidStateError, "The session is not callable."));
}
static ScriptPromise CreateRejectedPromiseAlreadyInitialized(ScriptState* scriptState)
{
return ScriptPromise::rejectWithDOMException(
scriptState, DOMException::create(InvalidStateError, "The session is already initialized."));
}
// 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;
}
WebEncryptedMediaInitDataType initDataType() const
{
ASSERT(m_type == GenerateRequest);
return m_initDataType;
}
const String& sessionId() const
{
ASSERT(m_type == Load);
return m_stringData;
}
static PendingAction* CreatePendingGenerateRequest(ContentDecryptionModuleResult* result, WebEncryptedMediaInitDataType initDataType, PassRefPtr<DOMArrayBuffer> initData)
{
ASSERT(result);
ASSERT(initData);
return new PendingAction(GenerateRequest, result, initDataType, initData, String());
}
static PendingAction* CreatePendingLoadRequest(ContentDecryptionModuleResult* result, const String& sessionId)
{
ASSERT(result);
return new PendingAction(Load, result, WebEncryptedMediaInitDataType::Unknown, PassRefPtr<DOMArrayBuffer>(), sessionId);
}
static PendingAction* CreatePendingUpdate(ContentDecryptionModuleResult* result, PassRefPtr<DOMArrayBuffer> data)
{
ASSERT(result);
ASSERT(data);
return new PendingAction(Update, result, WebEncryptedMediaInitDataType::Unknown, data, String());
}
static PendingAction* CreatePendingClose(ContentDecryptionModuleResult* result)
{
ASSERT(result);
return new PendingAction(Close, result, WebEncryptedMediaInitDataType::Unknown, PassRefPtr<DOMArrayBuffer>(), String());
}
static PendingAction* CreatePendingRemove(ContentDecryptionModuleResult* result)
{
ASSERT(result);
return new PendingAction(Remove, result, WebEncryptedMediaInitDataType::Unknown, PassRefPtr<DOMArrayBuffer>(), String());
}
~PendingAction()
{
}
DEFINE_INLINE_TRACE()
{
visitor->trace(m_result);
}
private:
PendingAction(Type type, ContentDecryptionModuleResult* result, WebEncryptedMediaInitDataType initDataType, PassRefPtr<DOMArrayBuffer> data, const String& stringData)
: m_type(type)
, m_result(result)
, m_initDataType(initDataType)
, m_data(data)
, m_stringData(stringData)
{
}
const Type m_type;
const Member<ContentDecryptionModuleResult> m_result;
const WebEncryptedMediaInitDataType m_initDataType;
const RefPtr<DOMArrayBuffer> m_data;
const String m_stringData;
};
// 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 NewSessionResultPromise : public ContentDecryptionModuleResultPromise {
public:
NewSessionResultPromise(ScriptState* scriptState, MediaKeySession* session)
: ContentDecryptionModuleResultPromise(scriptState)
, m_session(session)
{
}
~NewSessionResultPromise() override
{
}
// ContentDecryptionModuleResult implementation.
void completeWithSession(WebContentDecryptionModuleResult::SessionStatus status) override
{
if (status != WebContentDecryptionModuleResult::NewSession) {
ASSERT_NOT_REACHED();
reject(InvalidStateError, "Unexpected completion.");
}
m_session->finishGenerateRequest();
resolve();
}
DEFINE_INLINE_TRACE()
{
visitor->trace(m_session);
ContentDecryptionModuleResultPromise::trace(visitor);
}
private:
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 LoadSessionResultPromise : public ContentDecryptionModuleResultPromise {
public:
LoadSessionResultPromise(ScriptState* scriptState, MediaKeySession* session)
: ContentDecryptionModuleResultPromise(scriptState)
, m_session(session)
{
}
~LoadSessionResultPromise() override
{
}
// ContentDecryptionModuleResult implementation.
void completeWithSession(WebContentDecryptionModuleResult::SessionStatus status) override
{
switch (status) {
case WebContentDecryptionModuleResult::NewSession:
m_session->finishLoad();
resolve(true);
return;
case WebContentDecryptionModuleResult::SessionNotFound:
resolve(false);
return;
case WebContentDecryptionModuleResult::SessionAlreadyExists:
ASSERT_NOT_REACHED();
reject(InvalidStateError, "Unexpected completion.");
return;
}
ASSERT_NOT_REACHED();
}
DEFINE_INLINE_TRACE()
{
visitor->trace(m_session);
ContentDecryptionModuleResultPromise::trace(visitor);
}
private:
Member<MediaKeySession> m_session;
};
MediaKeySession* MediaKeySession::create(ScriptState* scriptState, MediaKeys* mediaKeys, WebEncryptedMediaSessionType sessionType)
{
RefPtrWillBeRawPtr<MediaKeySession> session = new MediaKeySession(scriptState, mediaKeys, sessionType);
session->suspendIfNeeded();
return session.get();
}
MediaKeySession::MediaKeySession(ScriptState* scriptState, MediaKeys* mediaKeys, WebEncryptedMediaSessionType sessionType)
: ActiveDOMObject(scriptState->executionContext())
, m_asyncEventQueue(GenericEventQueue::create(this))
, m_mediaKeys(mediaKeys)
, m_sessionType(sessionType)
, m_expiration(std::numeric_limits<double>::quiet_NaN())
, m_keyStatusesMap(new MediaKeyStatusMap())
, 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);
// From https://w3c.github.io/encrypted-media/#createSession:
// MediaKeys::createSession(), step 3.
// 3.1 Let the sessionId attribute be the empty string.
ASSERT(sessionId().isEmpty());
// 3.2 Let the expiration attribute be NaN.
ASSERT(std::isnan(m_expiration));
// 3.3 Let the closed attribute be a new promise.
ASSERT(!closed(scriptState).isUndefinedOrNull());
// 3.4 Let the keyStatuses attribute be empty.
ASSERT(m_keyStatusesMap->size() == 0);
// 3.5 Let the session type be sessionType.
ASSERT(m_sessionType != WebEncryptedMediaSessionType::Unknown);
// 3.6 Let uninitialized be true.
ASSERT(m_isUninitialized);
// 3.7 Let callable be false.
ASSERT(!m_isCallable);
// 3.8 Let the use distinctive identifier value be this object's
// use distinctive identifier.
// FIXME: Implement this (http://crbug.com/448922).
// 3.9 Let the cdm implementation value be this object's cdm implementation.
// 3.10 Let the cdm instance value be this object's cdm instance.
}
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
}
String MediaKeySession::sessionId() const
{
return m_session->sessionId();
}
ScriptPromise MediaKeySession::closed(ScriptState* scriptState)
{
return m_closedPromise->promise(scriptState->world());
}
MediaKeyStatusMap* MediaKeySession::keyStatuses()
{
return m_keyStatusesMap;
}
ScriptPromise MediaKeySession::generateRequest(ScriptState* scriptState, const String& initDataTypeString, const DOMArrayPiece& initData)
{
WTF_LOG(Media, "MediaKeySession(%p)::generateRequest %s", this, initDataTypeString.ascii().data());
// From https://w3c.github.io/encrypted-media/#generateRequest:
// Generates a request based on the initData. When this method is invoked,
// the user agent 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 CreateRejectedPromiseAlreadyInitialized(scriptState);
// 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 (initDataTypeString.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. If the Key System implementation represented by this object's cdm
// implementation value 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.
// (blink side doesn't know what the CDM supports, so the proper check
// will be done on the Chromium side. However, we can verify that
// |initDataType| is one of the registered values.)
WebEncryptedMediaInitDataType initDataType = EncryptedMediaUtils::convertToInitDataType(initDataTypeString);
if (initDataType == WebEncryptedMediaInitDataType::Unknown) {
return ScriptPromise::rejectWithDOMException(
scriptState, DOMException::create(NotSupportedError, "The initialization data type '" + initDataTypeString + "' is not supported."));
}
// 6. Let init data be a copy of the contents of the initData parameter.
RefPtr<DOMArrayBuffer> initDataBuffer = DOMArrayBuffer::create(initData.data(), initData.byteLength());
// 7. Let session type be this object's session type.
// (Done in constructor.)
// 8. Let promise be a new promise.
NewSessionResultPromise* result = new NewSessionResultPromise(scriptState, this);
ScriptPromise promise = result->promise();
// 9. Run the following steps asynchronously (documented in
// actionTimerFired())
m_pendingActions.append(PendingAction::CreatePendingGenerateRequest(result, initDataType, initDataBuffer.release()));
ASSERT(!m_actionTimer.isActive());
m_actionTimer.startOneShot(0, BLINK_FROM_HERE);
// 10. 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://w3c.github.io/encrypted-media/#load:
// Loads the data stored for the specified session into this object. When
// this method is invoked, the user agent 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 CreateRejectedPromiseAlreadyInitialized(scriptState);
// 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-license" or
// "persistent-release-message", return a promise rejected with a
// new DOMException whose name is InvalidAccessError.
if (m_sessionType != WebEncryptedMediaSessionType::PersistentLicense && m_sessionType != WebEncryptedMediaSessionType::PersistentReleaseMessage) {
return ScriptPromise::rejectWithDOMException(
scriptState, DOMException::create(InvalidAccessError, "The session type is not persistent."));
}
// 5. If the Key System implementation represented by this object's cdm
// implementation value does not support loading previous sessions,
// return a promise rejected with a new DOMException whose name is
// NotSupportedError.
// FIXME: Implement this (http://crbug.com/448922).
// 6. Let origin be the origin of this object's Document.
// (Available as executionContext()->securityOrigin() anytime.)
// 7. Let promise be a new promise.
LoadSessionResultPromise* result = new LoadSessionResultPromise(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, BLINK_FROM_HERE);
// 9. Return promise.
return promise;
}
ScriptPromise MediaKeySession::update(ScriptState* scriptState, const DOMArrayPiece& response)
{
WTF_LOG(Media, "MediaKeySession(%p)::update", this);
ASSERT(!m_isClosed);
// From https://w3c.github.io/encrypted-media/#update:
// Provides messages, including licenses, to the CDM. When this method is
// invoked, the user agent 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 CreateRejectedPromiseNotCallable(scriptState);
// 2. If response is an empty array, return a promise rejected with a
// new DOMException whose name is InvalidAccessError.
if (!response.byteLength()) {
return ScriptPromise::rejectWithDOMException(
scriptState, DOMException::create(InvalidAccessError, "The response parameter is empty."));
}
// 3. Let response copy be a copy of the contents of the response parameter.
RefPtr<DOMArrayBuffer> responseCopy = DOMArrayBuffer::create(response.data(), response.byteLength());
// 4. Let promise be a new promise.
SimpleContentDecryptionModuleResultPromise* result = new SimpleContentDecryptionModuleResultPromise(scriptState);
ScriptPromise promise = result->promise();
// 5. Run the following steps asynchronously (documented in
// actionTimerFired())
m_pendingActions.append(PendingAction::CreatePendingUpdate(result, responseCopy.release()));
if (!m_actionTimer.isActive())
m_actionTimer.startOneShot(0, BLINK_FROM_HERE);
// 6. Return promise.
return promise;
}
ScriptPromise MediaKeySession::close(ScriptState* scriptState)
{
WTF_LOG(Media, "MediaKeySession(%p)::close", this);
// From https://w3c.github.io/encrypted-media/#close:
// Indicates that the application no longer needs the session and the CDM
// should release any resources associated with this object and close it.
// When this method is invoked, the user agent 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 CreateRejectedPromiseNotCallable(scriptState);
// 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.
SimpleContentDecryptionModuleResultPromise* result = new SimpleContentDecryptionModuleResultPromise(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, BLINK_FROM_HERE);
// 5. Return promise.
return promise;
}
ScriptPromise MediaKeySession::remove(ScriptState* scriptState)
{
WTF_LOG(Media, "MediaKeySession(%p)::remove", this);
// From https://w3c.github.io/encrypted-media/#remove:
// Removes stored session data associated with this object. When this
// method is invoked, the user agent 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 CreateRejectedPromiseNotCallable(scriptState);
// 2. If this object's session type is not "persistent-license" or
// "persistent-release-message", return a promise rejected with a
// new DOMException whose name is InvalidAccessError.
if (m_sessionType != WebEncryptedMediaSessionType::PersistentLicense && m_sessionType != WebEncryptedMediaSessionType::PersistentReleaseMessage) {
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.
SimpleContentDecryptionModuleResultPromise* result = new SimpleContentDecryptionModuleResultPromise(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, BLINK_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:
// NOTE: Continue step 9 of MediaKeySession::generateRequest().
WTF_LOG(Media, "MediaKeySession(%p)::actionTimerFired: GenerateRequest", this);
// initializeNewSession() in Chromium will execute steps 9.1 to 9.7.
m_session->initializeNewSession(action->initDataType(), static_cast<unsigned char*>(action->data()->data()), action->data()->byteLength(), m_sessionType, action->result()->result());
// Remaining steps (from 9.8) executed in finishGenerateRequest(),
// called when |result| is resolved.
break;
case PendingAction::Load:
// NOTE: Continue step 8 of MediaKeySession::load().
WTF_LOG(Media, "MediaKeySession(%p)::actionTimerFired: Load", this);
// 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 If there is an unclosed session in the object's Document
// whose sessionId attribute is sanitized session ID, reject
// promise with a new DOMException whose name is
// QuotaExceededError. In other words, do not create a session
// if a non-closed session, regardless of type, already exists
// for this sanitized session ID in this browsing context.
// (Done in the CDM.)
// 8.4 Let expiration time be NaN.
// (Done in the constructor.)
ASSERT(std::isnan(m_expiration));
// load() in Chromium will execute steps 8.5 through 8.8.
m_session->load(action->sessionId(), action->result()->result());
// Remaining steps (from 8.9) executed in finishLoad(), called
// when |result| is resolved.
break;
case PendingAction::Update:
// NOTE: Continue step 5 of MediaKeySession::update().
WTF_LOG(Media, "MediaKeySession(%p)::actionTimerFired: Update", this);
// update() in Chromium will execute steps 5.1 through 5.8.
m_session->update(static_cast<unsigned char*>(action->data()->data()), action->data()->byteLength(), action->result()->result());
// Last step (5.9 Resolve promise) will be done when |result| is
// resolved.
break;
case PendingAction::Close:
// NOTE: Continue step 4 of MediaKeySession::close().
WTF_LOG(Media, "MediaKeySession(%p)::actionTimerFired: Close", this);
// close() in Chromium will execute steps 4.1 through 4.2.
m_session->close(action->result()->result());
// Last step (4.3 Resolve promise) will be done when |result| is
// resolved.
break;
case PendingAction::Remove:
// NOTE: Continue step 5 of MediaKeySession::remove().
WTF_LOG(Media, "MediaKeySession(%p)::actionTimerFired: Remove", this);
// remove() in Chromium will execute steps 5.1 through 5.3.
m_session->remove(action->result()->result());
// Last step (5.3.3 Resolve promise) will be done when |result| is
// resolved.
break;
}
}
}
void MediaKeySession::finishGenerateRequest()
{
// 9.8 If any of the preceding steps failed, reject promise with a
// new DOMException whose name is the appropriate error name.
// (Done by CDM calling result.completeWithError() as appropriate.)
// 9.9 Set the sessionId attribute to session id.
ASSERT(!sessionId().isEmpty());
// 9.10 Let this object's callable be true.
m_isCallable = true;
// 9.11 Run the queue a "message" event algorithm on the session,
// providing "license-request" and message.
// (Done by the CDM.)
// 9.12 Resolve promise.
// (Done by NewSessionResultPromise.)
}
void MediaKeySession::finishLoad()
{
// 8.9 If any of the preceding steps failed, reject promise with a new
// DOMException whose name is the appropriate error name.
// (Done by CDM calling result.completeWithError() as appropriate.)
// 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 information about any keys (there
// are known keys), run the update key statuses algorithm on the
// session, providing each key's key ID along with the appropriate
// MediaKeyStatus. Should additional processing be necessary to
// determine with certainty the status of a key, use the non-"usable"
// MediaKeyStatus value that corresponds to the reason for the
// additional processing. Once the additional processing for one or
// more keys has completed, run the update key statuses algorithm
// again if any of the statuses has changed.
// (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.)
// 8.15 Resolve promise with true.
// (Done by LoadSessionResultPromise.)
}
// Queue a task to fire a simple event named keymessage at the new object.
void MediaKeySession::message(MessageType messageType, const unsigned char* message, size_t messageLength)
{
WTF_LOG(Media, "MediaKeySession(%p)::message", this);
// Verify that 'message' not fired before session initialization is complete.
ASSERT(m_isCallable);
// From https://w3c.github.io/encrypted-media/#queue-message:
// The following steps are run:
// 1. Let the session be the specified MediaKeySession object.
// 2. Queue a task to fire a simple event named message at the session.
// The event is of type MediaKeyMessageEvent and has:
// -> messageType = the specified message type
// -> message = the specified message
MediaKeyMessageEventInit init;
switch (messageType) {
case WebContentDecryptionModuleSession::Client::MessageType::LicenseRequest:
init.setMessageType("license-request");
break;
case WebContentDecryptionModuleSession::Client::MessageType::LicenseRenewal:
init.setMessageType("license-renewal");
break;
case WebContentDecryptionModuleSession::Client::MessageType::LicenseRelease:
init.setMessageType("license-release");
break;
}
init.setMessage(DOMArrayBuffer::create(static_cast<const void*>(message), messageLength));
RefPtrWillBeRawPtr<MediaKeyMessageEvent> event = MediaKeyMessageEvent::create(EventTypeNames::message, init);
event->setTarget(this);
m_asyncEventQueue->enqueueEvent(event.release());
}
void MediaKeySession::close()
{
WTF_LOG(Media, "MediaKeySession(%p)::close", this);
// From https://w3c.github.io/encrypted-media/#session-close:
// The following steps are run:
// 1. Let the session be the associated MediaKeySession object.
// 2. Let promise be the closed attribute of the session.
// 3. Resolve promise.
m_closedPromise->resolve(ToV8UndefinedGenerator());
// 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;
}
void MediaKeySession::expirationChanged(double updatedExpiryTimeInMS)
{
WTF_LOG(Media, "MediaKeySession(%p)::expirationChanged %f", this, updatedExpiryTimeInMS);
// From https://w3c.github.io/encrypted-media/#update-expiration:
// The following steps are run:
// 1. Let the session be the associated MediaKeySession object.
// 2. Let expiration time be NaN.
double expirationTime = std::numeric_limits<double>::quiet_NaN();
// 3. If the new expiration time is not NaN, let expiration time be the
// new expiration time in milliseconds since 01 January 1970 UTC.
// (Note that Chromium actually passes 0 to indicate no expiry.)
// FIXME: Get Chromium to pass NaN.
if (!std::isnan(updatedExpiryTimeInMS) && updatedExpiryTimeInMS != 0.0)
expirationTime = updatedExpiryTimeInMS;
// 4. Set the session's expiration attribute to expiration time.
m_expiration = expirationTime;
}
void MediaKeySession::keysStatusesChange(const WebVector<WebEncryptedMediaKeyInformation>& keys, bool hasAdditionalUsableKey)
{
WTF_LOG(Media, "MediaKeySession(%p)::keysStatusesChange with %zu keys and usable key: %d", this, keys.size(), hasAdditionalUsableKey);
// From https://w3c.github.io/encrypted-media/#update-key-statuses:
// The following steps are run:
// 1. Let the session be the associated MediaKeySession object.
// 2. Let the input statuses be the sequence of pairs key ID and
// associated MediaKeyStatus pairs.
// 3. Let the statuses be session's keyStatuses attribute.
// 4. Run the following steps to replace the contents of statuses:
// 4.1 Empty statuses.
m_keyStatusesMap->clear();
// 4.2 For each pair in input statuses.
for (size_t i = 0; i < keys.size(); ++i) {
// 4.2.1 Let pair be the pair.
const auto& key = keys[i];
// 4.2.2 Insert an entry for pair's key ID into statuses with the
// value of pair's MediaKeyStatus value.
m_keyStatusesMap->addEntry(key.id(), ConvertKeyStatusToString(key.status()));
}
// 5. Queue a task to fire a simple event named keystatuseschange
// at the session.
RefPtrWillBeRawPtr<Event> event = Event::create(EventTypeNames::keystatuseschange);
event->setTarget(this);
m_asyncEventQueue->enqueueEvent(event.release());
// 6. Queue a task to run the attempt to resume playback if necessary
// algorithm on each of the media element(s) whose mediaKeys attribute
// is the MediaKeys object that created the session. The user agent
// may choose to skip this step if it knows resuming will fail.
// FIXME: Attempt to resume playback if |hasAdditionalUsableKey| is true.
// http://crbug.com/413413
}
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,
ScriptWrappable::hasPendingActivity() ? " ScriptWrappable::hasPendingActivity()" : "",
!m_pendingActions.isEmpty() ? " !m_pendingActions.isEmpty()" : "",
m_asyncEventQueue->hasPendingEvents() ? " m_asyncEventQueue->hasPendingEvents()" : "",
(m_mediaKeys && !m_isClosed) ? " m_mediaKeys && !m_isClosed" : "");
return ScriptWrappable::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();
}
DEFINE_TRACE(MediaKeySession)
{
visitor->trace(m_asyncEventQueue);
visitor->trace(m_pendingActions);
visitor->trace(m_mediaKeys);
visitor->trace(m_keyStatusesMap);
visitor->trace(m_closedPromise);
RefCountedGarbageCollectedEventTargetWithInlineData<MediaKeySession>::trace(visitor);
ActiveDOMObject::trace(visitor);
}
} // namespace blink