blob: 7c2f1ae23f82e74fa9a98c62ef4ac74724f23920 [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/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/Logging.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/ArrayBuffer.h"
#include "wtf/ArrayBufferView.h"
namespace blink {
// A class holding a pending action.
class MediaKeySession::PendingAction : public GarbageCollectedFinalized<MediaKeySession::PendingAction> {
public:
enum Type {
Update,
Release,
Message
};
Type type() const { return m_type; }
const Persistent<ContentDecryptionModuleResult> result() const
{
ASSERT(m_type == Update || m_type == Release);
return m_result;
}
const RefPtr<ArrayBuffer> data() const
{
ASSERT(m_type == Update);
return m_data;
}
RefPtrWillBeRawPtr<Event> event()
{
ASSERT(m_type == Message);
return m_event;
}
static PendingAction* CreatePendingUpdate(ContentDecryptionModuleResult* result, PassRefPtr<ArrayBuffer> data)
{
ASSERT(result);
ASSERT(data);
return new PendingAction(Update, result, data);
}
static PendingAction* CreatePendingRelease(ContentDecryptionModuleResult* result)
{
ASSERT(result);
return new PendingAction(Release, result, PassRefPtr<ArrayBuffer>());
}
static PendingAction* CreatePendingMessage(PassRefPtrWillBeRawPtr<Event> event)
{
ASSERT(event);
return new PendingAction(Message, event);
}
~PendingAction()
{
}
void trace(Visitor* visitor)
{
visitor->trace(m_result);
visitor->trace(m_event);
}
private:
PendingAction(Type type, ContentDecryptionModuleResult* result, PassRefPtr<ArrayBuffer> data)
: m_type(type)
, m_result(result)
, m_data(data)
{
}
PendingAction(Type type, PassRefPtrWillBeRawPtr<Event> event)
: m_type(type)
, m_event(event)
{
}
const Type m_type;
const Member<ContentDecryptionModuleResult> m_result;
const RefPtr<ArrayBuffer> m_data;
const RefPtrWillBeMember<Event> m_event;
};
// This class allows a MediaKeySession object to be created asynchronously.
class MediaKeySessionInitializer : public ScriptPromiseResolver {
WTF_MAKE_NONCOPYABLE(MediaKeySessionInitializer);
public:
static ScriptPromise create(ScriptState*, MediaKeys*, const String& initDataType, PassRefPtr<ArrayBuffer> initData, const String& sessionType);
virtual ~MediaKeySessionInitializer();
void completeWithSession(WebContentDecryptionModuleResult::SessionStatus);
void completeWithDOMException(ExceptionCode, const String& errorMessage);
private:
MediaKeySessionInitializer(ScriptState*, MediaKeys*, const String& initDataType, PassRefPtr<ArrayBuffer> initData, const String& sessionType);
void timerFired(Timer<MediaKeySessionInitializer>*);
Persistent<MediaKeys> m_mediaKeys;
OwnPtr<WebContentDecryptionModuleSession> m_cdmSession;
// The next 3 values are simply the initialization data saved so that the
// asynchronous creation has the data needed.
String m_initDataType;
RefPtr<ArrayBuffer> m_initData;
String m_sessionType;
Timer<MediaKeySessionInitializer> m_timer;
};
// Represents the result used when a new WebContentDecryptionModuleSession
// object has been created. Needed as MediaKeySessionInitializer can't be both
// a ScriptPromiseResolver and ContentDecryptionModuleResult at the same time.
class NewMediaKeySessionResult FINAL : public ContentDecryptionModuleResult {
public:
NewMediaKeySessionResult(MediaKeySessionInitializer* initializer)
: m_initializer(initializer)
{
}
// ContentDecryptionModuleResult implementation.
virtual void complete() OVERRIDE
{
ASSERT_NOT_REACHED();
m_initializer->completeWithDOMException(InvalidStateError, "Unexpected completion.");
}
virtual void completeWithSession(WebContentDecryptionModuleResult::SessionStatus status) OVERRIDE
{
m_initializer->completeWithSession(status);
}
virtual void completeWithError(WebContentDecryptionModuleException code, unsigned long systemCode, const WebString& message) OVERRIDE
{
m_initializer->completeWithDOMException(WebCdmExceptionToExceptionCode(code), message);
}
private:
MediaKeySessionInitializer* m_initializer;
};
ScriptPromise MediaKeySessionInitializer::create(ScriptState* scriptState, MediaKeys* mediaKeys, const String& initDataType, PassRefPtr<ArrayBuffer> initData, const String& sessionType)
{
RefPtr<MediaKeySessionInitializer> initializer = adoptRef(new MediaKeySessionInitializer(scriptState, mediaKeys, initDataType, initData, sessionType));
initializer->suspendIfNeeded();
initializer->keepAliveWhilePending();
return initializer->promise();
}
MediaKeySessionInitializer::MediaKeySessionInitializer(ScriptState* scriptState, MediaKeys* mediaKeys, const String& initDataType, PassRefPtr<ArrayBuffer> initData, const String& sessionType)
: ScriptPromiseResolver(scriptState)
, m_mediaKeys(mediaKeys)
, m_initDataType(initDataType)
, m_initData(initData)
, m_sessionType(sessionType)
, m_timer(this, &MediaKeySessionInitializer::timerFired)
{
WTF_LOG(Media, "MediaKeySessionInitializer::MediaKeySessionInitializer");
// Start the timer so that MediaKeySession can be created asynchronously.
m_timer.startOneShot(0, FROM_HERE);
}
MediaKeySessionInitializer::~MediaKeySessionInitializer()
{
WTF_LOG(Media, "MediaKeySessionInitializer::~MediaKeySessionInitializer");
}
void MediaKeySessionInitializer::timerFired(Timer<MediaKeySessionInitializer>*)
{
WTF_LOG(Media, "MediaKeySessionInitializer::timerFired");
// Continue MediaKeys::createSession() at step 7.
// 7.1 Let request be null. (Request provided by cdm in message event).
// 7.2 Let default URL be null. (Also provided by cdm in message event).
// 7.3 Let cdm be the cdm loaded in create().
WebContentDecryptionModule* cdm = m_mediaKeys->contentDecryptionModule();
// 7.4 Use the cdm to execute the following steps:
// 7.4.1 If the init data is not valid for initDataType, reject promise
// with a new DOMException whose name is "InvalidAccessError".
// 7.4.2 If the init data is not supported by the cdm, reject promise with
// a new DOMException whose name is "NotSupportedError".
// 7.4.3 Let request be a request (e.g. a license request) generated based
// on the init data, which is interpreteted per initDataType, and
// sessionType. If sessionType is "temporary", the request is for a
// temporary non-persisted license. If sessionType is "persistent",
// the request is for a persistable license.
// 7.4.4 If the init data indicates a default URL, let default URL be
// that URL. The URL may be validated and/or normalized.
m_cdmSession = adoptPtr(cdm->createSession());
NewMediaKeySessionResult* result = new NewMediaKeySessionResult(this);
m_cdmSession->initializeNewSession(m_initDataType, static_cast<unsigned char*>(m_initData->data()), m_initData->byteLength(), m_sessionType, result->result());
WTF_LOG(Media, "MediaKeySessionInitializer::timerFired done");
// Note: As soon as the promise is resolved (or rejected), the
// ScriptPromiseResolver object (|this|) is freed. So if
// initializeNewSession() is synchronous, access to any members will crash.
}
void MediaKeySessionInitializer::completeWithSession(WebContentDecryptionModuleResult::SessionStatus status)
{
WTF_LOG(Media, "MediaKeySessionInitializer::completeWithSession");
switch (status) {
case WebContentDecryptionModuleResult::NewSession: {
// Resume MediaKeys::createSession().
// 7.5 Let the session ID be a unique Session ID string. It may be
// obtained from cdm (it is).
// 7.6 Let session be a new MediaKeySession object, and initialize it.
// (Object was created previously, complete the steps for 7.6).
RefPtrWillBeRawPtr<MediaKeySession> session = adoptRefCountedGarbageCollectedWillBeNoop(new MediaKeySession(executionContext(), m_mediaKeys, m_cdmSession.release()));
session->suspendIfNeeded();
// 7.7 If any of the preceding steps failed, reject promise with a
// new DOMException whose name is the appropriate error name
// and that has an appropriate message.
// (Implemented by CDM/Chromium calling completeWithError()).
// 7.8 Add an entry for the value of the sessionId attribute to the
// list of active session IDs for this object.
// (Implemented in SessionIdAdapter).
// 7.9 Run the Queue a "message" Event algorithm on the session,
// providing request and default URL.
// (Done by the CDM).
// 7.10 Resolve promise with session.
resolve(session.release());
WTF_LOG(Media, "MediaKeySessionInitializer::completeWithSession done w/session");
return;
}
case WebContentDecryptionModuleResult::SessionNotFound:
// Step 4.7.1 of MediaKeys::loadSession(): If there is no data
// stored for the sessionId in the origin, resolve promise with
// undefined.
resolve(V8UndefinedType());
WTF_LOG(Media, "MediaKeySessionInitializer::completeWithSession done w/undefined");
return;
case WebContentDecryptionModuleResult::SessionAlreadyExists:
// If a session already exists, resolve the promise with null.
resolve(V8NullType());
WTF_LOG(Media, "MediaKeySessionInitializer::completeWithSession done w/null");
return;
}
ASSERT_NOT_REACHED();
}
void MediaKeySessionInitializer::completeWithDOMException(ExceptionCode code, const String& errorMessage)
{
WTF_LOG(Media, "MediaKeySessionInitializer::completeWithDOMException");
reject(DOMException::create(code, errorMessage));
}
ScriptPromise MediaKeySession::create(ScriptState* scriptState, MediaKeys* mediaKeys, const String& initDataType, PassRefPtr<ArrayBuffer> initData, const String& sessionType)
{
// Since creation is done asynchronously, use MediaKeySessionInitializer
// to do it.
return MediaKeySessionInitializer::create(scriptState, mediaKeys, initDataType, initData, sessionType);
}
MediaKeySession::MediaKeySession(ExecutionContext* context, MediaKeys* keys, PassOwnPtr<WebContentDecryptionModuleSession> cdmSession)
: ActiveDOMObject(context)
, m_keySystem(keys->keySystem())
, m_asyncEventQueue(GenericEventQueue::create(this))
, m_session(cdmSession)
, m_keys(keys)
, m_isClosed(false)
, m_closedPromise(new ClosedPromise(context, this, ClosedPromise::Closed))
, m_actionTimer(this, &MediaKeySession::actionTimerFired)
{
WTF_LOG(Media, "MediaKeySession(%p)::MediaKeySession", this);
ScriptWrappable::init(this);
m_session->setClientInterface(this);
// Resume MediaKeys::createSession() at step 7.6.
// 7.6.1 Set the error attribute to null.
ASSERT(!m_error);
// 7.6.2 Set the sessionId attribute to session ID.
ASSERT(!sessionId().isEmpty());
// 7.6.3 Let expiration be NaN.
// 7.6.4 Let closed be a new promise.
// 7.6.5 Let the session type be sessionType.
// FIXME: Implement the previous 3 values.
}
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::update(ScriptState* scriptState, ArrayBuffer* response)
{
RefPtr<ArrayBuffer> responseCopy = ArrayBuffer::create(response->data(), response->byteLength());
return updateInternal(scriptState, responseCopy.release());
}
ScriptPromise MediaKeySession::update(ScriptState* scriptState, ArrayBufferView* response)
{
RefPtr<ArrayBuffer> responseCopy = ArrayBuffer::create(response->baseAddress(), response->byteLength());
return updateInternal(scriptState, responseCopy.release());
}
ScriptPromise MediaKeySession::updateInternal(ScriptState* scriptState, PassRefPtr<ArrayBuffer> 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::release(ScriptState* scriptState)
{
WTF_LOG(Media, "MediaKeySession(%p)::release", this);
SimpleContentDecryptionModuleResult* result = new SimpleContentDecryptionModuleResult(scriptState);
ScriptPromise promise = result->promise();
// 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 the Session Close algorithm has been run on this object, return a
// promise fulfilled with undefined.
if (m_isClosed) {
result->complete();
return promise;
}
// 2. Let promise be a new promise.
// (Created earlier so it was available in step 1.)
// 3. Run the following steps asynchronously (documented in
// actionTimerFired()).
m_pendingActions.append(PendingAction::CreatePendingRelease(result));
if (!m_actionTimer.isActive())
m_actionTimer.startOneShot(0, FROM_HERE);
// 4. 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::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::Release:
WTF_LOG(Media, "MediaKeySession(%p)::actionTimerFired: Release", this);
// NOTE: Continued from step 3 of MediaKeySession::release().
// 3.1 Let cdm be the cdm loaded in create().
// 3.2 Use the cdm to execute the following steps:
// 3.2.1 Process the close request. Do not remove stored session data.
// 3.2.2 If the previous step caused the session to be closed, run the
// Session Close algorithm on this object.
// 3.3 Resolve promise with undefined.
m_session->release(action->result()->result());
break;
case PendingAction::Message:
WTF_LOG(Media, "MediaKeySession(%p)::actionTimerFired: Message", this);
m_asyncEventQueue->enqueueEvent(action->event().release());
break;
}
}
}
// 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);
MediaKeyMessageEventInit init;
init.bubbles = false;
init.cancelable = false;
init.message = ArrayBuffer::create(static_cast<const void*>(message), messageLength);
init.destinationURL = destinationURL.string();
RefPtrWillBeRawPtr<MediaKeyMessageEvent> event = MediaKeyMessageEvent::create(EventTypeNames::message, init);
event->setTarget(this);
if (!hasEventListeners()) {
// Since this event may be generated immediately after resolving the
// CreateSession() promise, it is possible that the JavaScript hasn't
// had time to run the .then() action and bind any necessary event
// handlers. If there are no event handlers connected, delay enqueuing
// this message to provide time for the JavaScript to run. This will
// also affect the (rare) case where there is no message handler
// attched during normal operation.
m_pendingActions.append(PendingAction::CreatePendingMessage(event.release()));
if (!m_actionTimer.isActive())
m_actionTimer.startOneShot(0, FROM_HERE);
return;
}
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);
}
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_keys && !m_isClosed) ? " m_keys && !m_isClosed" : "");
return ActiveDOMObject::hasPendingActivity()
|| !m_pendingActions.isEmpty()
|| m_asyncEventQueue->hasPendingEvents()
|| (m_keys && !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_keys);
visitor->trace(m_closedPromise);
EventTargetWithInlineData::trace(visitor);
}
} // namespace blink