Index: Source/modules/encryptedmedia/HTMLMediaElementEncryptedMedia.cpp |
diff --git a/Source/modules/encryptedmedia/HTMLMediaElementEncryptedMedia.cpp b/Source/modules/encryptedmedia/HTMLMediaElementEncryptedMedia.cpp |
index 1a3969e9b1750239c11716657ba29156140c10f2..1378d09347b1d540e47a12ede471195f73f2a385 100644 |
--- a/Source/modules/encryptedmedia/HTMLMediaElementEncryptedMedia.cpp |
+++ b/Source/modules/encryptedmedia/HTMLMediaElementEncryptedMedia.cpp |
@@ -6,12 +6,18 @@ |
#include "modules/encryptedmedia/HTMLMediaElementEncryptedMedia.h" |
#include "bindings/core/v8/ExceptionState.h" |
+#include "bindings/core/v8/ScriptPromise.h" |
+#include "bindings/core/v8/ScriptPromiseResolver.h" |
+#include "bindings/core/v8/ScriptState.h" |
+#include "core/dom/DOMException.h" |
#include "core/dom/ExceptionCode.h" |
#include "core/html/HTMLMediaElement.h" |
#include "core/html/MediaKeyError.h" |
#include "core/html/MediaKeyEvent.h" |
#include "modules/encryptedmedia/MediaKeyNeededEvent.h" |
#include "modules/encryptedmedia/MediaKeys.h" |
+#include "modules/encryptedmedia/SimpleContentDecryptionModuleResult.h" |
+#include "platform/ContentDecryptionModuleResult.h" |
#include "platform/Logging.h" |
#include "platform/RuntimeEnabledFeatures.h" |
#include "wtf/Uint8Array.h" |
@@ -38,6 +44,191 @@ static void throwExceptionIfMediaKeyExceptionOccurred(const String& keySystem, c |
return; |
} |
+// This class allows MediaKeys to be set asynchronously. |
+class SetMediaKeysWorker : public ScriptPromiseResolver { |
+ WTF_MAKE_NONCOPYABLE(SetMediaKeysWorker); |
+ |
+public: |
+ static ScriptPromise create(ScriptState*, HTMLMediaElement&, MediaKeys*); |
+ virtual ~SetMediaKeysWorker(); |
+ |
+ void completeWithDOMException(ExceptionCode, const String& errorMessage); |
+ void timerFired(Timer<SetMediaKeysWorker>*); |
+ void proceedToNextStage(); |
+ |
+private: |
+ enum Stage { |
+ Starting, |
+ Clearing, |
+ Setting |
+ }; |
+ SetMediaKeysWorker(ScriptState*, HTMLMediaElement&, MediaKeys*); |
+ |
+ RawPtrWillBeMember<HTMLMediaElement> m_element; |
+ Persistent<MediaKeys> m_mediaKeys; |
+ |
+ Stage m_stage; |
+ Timer<SetMediaKeysWorker> m_timer; |
+}; |
+ |
+// Represents the result used when setContentDecryptionModule() is called. |
+// Needed as SetMediaKeysWorker may need to call setContentDecryptionModule() |
+// multiple times, and not resolve the promise until the very end. Any errors |
+// that happen will reject the promise immediately. |
+// |
+// This class can be used without specifying a worker to call. Useful when |
+// calling setContentDecryptionModule() while tearing down a player. |
+class SetContentDecryptionModuleResult FINAL : public ContentDecryptionModuleResult { |
+public: |
+ explicit SetContentDecryptionModuleResult(SetMediaKeysWorker* worker) |
+ : m_worker(worker) |
+ { |
+ } |
+ |
+ // ContentDecryptionModuleResult implementation. |
+ virtual void complete() OVERRIDE |
+ { |
+ if (m_worker) |
+ m_worker->proceedToNextStage(); |
+ } |
+ |
+ virtual void completeWithSession(blink::WebContentDecryptionModuleResult::SessionStatus status) OVERRIDE |
+ { |
+ ASSERT_NOT_REACHED(); |
+ if (m_worker) |
+ m_worker->completeWithDOMException(InvalidStateError, "Unexpected completion."); |
+ } |
+ |
+ virtual void completeWithError(blink::WebContentDecryptionModuleException code, unsigned long systemCode, const blink::WebString& message) OVERRIDE |
+ { |
+ if (m_worker) |
+ m_worker->completeWithDOMException(WebCdmExceptionToExceptionCode(code), message); |
+ } |
+ |
+private: |
+ SetMediaKeysWorker* m_worker; |
+}; |
+ |
+ScriptPromise SetMediaKeysWorker::create(ScriptState* scriptState, HTMLMediaElement& element, MediaKeys* mediaKeys) |
+{ |
+ RefPtr<SetMediaKeysWorker> worker = adoptRef(new SetMediaKeysWorker(scriptState, element, mediaKeys)); |
+ worker->suspendIfNeeded(); |
+ worker->keepAliveWhilePending(); |
+ return worker->promise(); |
+} |
+ |
+SetMediaKeysWorker::SetMediaKeysWorker(ScriptState* scriptState, HTMLMediaElement& element, MediaKeys* mediaKeys) |
+ : ScriptPromiseResolver(scriptState) |
+ , m_element(element) |
+ , m_mediaKeys(mediaKeys) |
+ , m_stage(Starting) |
+ , m_timer(this, &SetMediaKeysWorker::timerFired) |
+{ |
+ WTF_LOG(Media, "SetMediaKeysWorker::SetMediaKeysWorker"); |
+ |
+ // 3. Complete setting this asynchronously. |
+ m_timer.startOneShot(0, FROM_HERE); |
+} |
+ |
+SetMediaKeysWorker::~SetMediaKeysWorker() |
+{ |
+} |
+ |
+void SetMediaKeysWorker::completeWithDOMException(ExceptionCode code, const String& errorMessage) |
+{ |
+ reject(DOMException::create(code, errorMessage)); |
+} |
+ |
+void SetMediaKeysWorker::timerFired(Timer<SetMediaKeysWorker>*) |
+{ |
+ ASSERT(m_stage == Starting); |
+ proceedToNextStage(); |
+} |
+ |
+void SetMediaKeysWorker::proceedToNextStage() |
+{ |
+ WTF_LOG(Media, "SetMediaKeysWorker::proceedToNextStage(%d)", m_stage); |
+ HTMLMediaElementEncryptedMedia& thisElement = HTMLMediaElementEncryptedMedia::from(*m_element); |
+ |
+ switch (m_stage) { |
+ case Starting: |
+ // 3.1 If mediaKeys is not null, it is already in use by another media |
+ // element, and the user agent is unable to use it with this element, |
+ // reject promise with a new DOMException whose name is |
+ // "QuotaExceededError". (Currently no restrictions on using the same |
+ // MediaKeys with multiple media elements.) |
+ // 3.2 If the mediaKeys attribute is not null, run the following steps: |
+ m_stage = Clearing; |
+ if (thisElement.m_mediaKeys) { |
+ // 3.2.1 If the user agent or CDM do not support removing the |
+ // association, return a promise rejected with a new DOMException |
+ // whose name is "NotSupportedError". |
+ // 3.2.2 If the association cannot currently be removed (i.e. during |
+ // playback), return a promise rejected with a new DOMException |
+ // whose name is "InvalidStateError". |
+ if (m_element->isPlaying()) { |
+ completeWithDOMException(InvalidStateError, "The existing MediaKeys object cannot be removed at this time."); |
+ return; |
+ } |
+ |
+ // 3.2.3 Stop using the CDM instance represented by the mediaKeys |
+ // attribute to decrypt media data and remove the association |
+ // with the media element. |
+ // 3.2.4 If the preceding step failed, reject promise with a new |
+ // DOMException whose name is the appropriate error name and |
+ // that has an appropriate message. |
+ if (m_element->webMediaPlayer()) { |
+ ContentDecryptionModuleResult* result = new SetContentDecryptionModuleResult(this); |
+ m_element->webMediaPlayer()->setContentDecryptionModule(nullptr, result->result()); |
+ |
+ // Don't do anything more until SetContentDecryptionModuleResult |
+ // finishes (and calls back to proceedToNextStage()). |
+ return; |
+ } |
+ } |
+ |
+ // MediaKeys not currently set or no player connected. Continue to next |
+ // stage. |
+ |
+ case Clearing: |
+ // Successfully cleared MediaKeys, so drop reference to previous value. |
+ thisElement.m_mediaKeys.clear(); |
+ |
+ // 3.3 If mediaKeys is not null, run the following steps: |
+ m_stage = Setting; |
+ if (m_mediaKeys) { |
+ // 3.3.1 Associate the CDM instance represented by mediaKeys with the |
+ // media element for decrypting media data. |
+ // 3.3.2 If the preceding step failed, run the following steps: |
+ // 3.3.2.1 Set the mediaKeys attribute to null. |
+ // 3.3.2.2 Reject promise with a new DOMException whose name is the |
+ // appropriate error name and that has an appropriate message. |
+ // 3.3.3 Run the Attempt to Resume Playback If Necessary algorithm on |
+ // the media element. The user agent may choose to skip this |
+ // step if it knows resuming will fail (i.e. mediaKeys has no |
+ // sessions). |
+ if (m_element->webMediaPlayer()) { |
+ ContentDecryptionModuleResult* result = new SetContentDecryptionModuleResult(this); |
+ m_element->webMediaPlayer()->setContentDecryptionModule(m_mediaKeys->contentDecryptionModule(), result->result()); |
+ |
+ // Don't do anything more until SetContentDecryptionModuleResult |
+ // finishes (and calls back to proceedToNextStage()). |
+ return; |
+ } |
+ } |
+ // MediaKeys doesn't need to be set on the player, so continue on to |
+ // next stage. |
+ |
+ case Setting: |
+ // 3.4 Set the mediaKeys attribute to mediaKeys. |
+ thisElement.m_mediaKeys = m_mediaKeys; |
+ |
+ // 3.5 Resolve promise with undefined. |
+ resolve(); |
+ return; |
+ } |
+} |
+ |
HTMLMediaElementEncryptedMedia::HTMLMediaElementEncryptedMedia() |
: m_emeMode(EmeModeNotSelected) |
{ |
@@ -60,12 +251,11 @@ HTMLMediaElementEncryptedMedia& HTMLMediaElementEncryptedMedia::from(HTMLMediaEl |
return *supplement; |
} |
-bool HTMLMediaElementEncryptedMedia::setEmeMode(EmeMode emeMode, ExceptionState& exceptionState) |
+bool HTMLMediaElementEncryptedMedia::setEmeMode(EmeMode emeMode) |
{ |
- if (m_emeMode != EmeModeNotSelected && m_emeMode != emeMode) { |
- exceptionState.throwDOMException(InvalidStateError, "Mixed use of EME prefixed and unprefixed API not allowed."); |
+ if (m_emeMode != EmeModeNotSelected && m_emeMode != emeMode) |
return false; |
- } |
+ |
m_emeMode = emeMode; |
return true; |
} |
@@ -81,28 +271,21 @@ MediaKeys* HTMLMediaElementEncryptedMedia::mediaKeys(HTMLMediaElement& element) |
return thisElement.m_mediaKeys.get(); |
} |
-void HTMLMediaElementEncryptedMedia::setMediaKeysInternal(HTMLMediaElement& element, MediaKeys* mediaKeys) |
-{ |
- if (m_mediaKeys == mediaKeys) |
- return; |
- |
- ASSERT(m_emeMode == EmeModeUnprefixed); |
- m_mediaKeys = mediaKeys; |
- |
- // If a player is connected, tell it that the CDM has changed. |
- if (element.webMediaPlayer()) |
- element.webMediaPlayer()->setContentDecryptionModule(contentDecryptionModule()); |
-} |
- |
-void HTMLMediaElementEncryptedMedia::setMediaKeys(HTMLMediaElement& element, MediaKeys* mediaKeys, ExceptionState& exceptionState) |
+ScriptPromise HTMLMediaElementEncryptedMedia::setMediaKeys(ScriptState* scriptState, HTMLMediaElement& element, MediaKeys* mediaKeys) |
{ |
- WTF_LOG(Media, "HTMLMediaElementEncryptedMedia::setMediaKeys"); |
HTMLMediaElementEncryptedMedia& thisElement = HTMLMediaElementEncryptedMedia::from(element); |
+ WTF_LOG(Media, "HTMLMediaElementEncryptedMedia::setMediaKeys current(%p), new(%p)", thisElement.m_mediaKeys.get(), mediaKeys); |
- if (!thisElement.setEmeMode(EmeModeUnprefixed, exceptionState)) |
- return; |
+ if (!thisElement.setEmeMode(EmeModeUnprefixed)) |
+ return ScriptPromise::rejectWithDOMException(scriptState, DOMException::create(InvalidStateError, "Mixed use of EME prefixed and unprefixed API not allowed.")); |
+ |
+ // 1. If mediaKeys and the mediaKeys attribute are the same object, return |
+ // a promise resolved with undefined. |
+ if (thisElement.m_mediaKeys == mediaKeys) |
+ return ScriptPromise::cast(scriptState, V8ValueTraits<V8UndefinedType>::toV8Value(V8UndefinedType(), scriptState->context()->Global(), scriptState->isolate())); |
- thisElement.setMediaKeysInternal(element, mediaKeys); |
+ // 2. Let promise be a new promise. Remaining steps done in worker. |
+ return SetMediaKeysWorker::create(scriptState, element, mediaKeys); |
} |
// Create a MediaKeyNeededEvent for WD EME. |
@@ -139,8 +322,10 @@ void HTMLMediaElementEncryptedMedia::generateKeyRequest(blink::WebMediaPlayer* w |
{ |
WTF_LOG(Media, "HTMLMediaElementEncryptedMedia::webkitGenerateKeyRequest"); |
- if (!setEmeMode(EmeModePrefixed, exceptionState)) |
+ if (!setEmeMode(EmeModePrefixed)) { |
+ exceptionState.throwDOMException(InvalidStateError, "Mixed use of EME prefixed and unprefixed API not allowed."); |
return; |
+ } |
if (keySystem.isEmpty()) { |
exceptionState.throwDOMException(SyntaxError, "The key system provided is empty."); |
@@ -177,8 +362,10 @@ void HTMLMediaElementEncryptedMedia::addKey(blink::WebMediaPlayer* webMediaPlaye |
{ |
WTF_LOG(Media, "HTMLMediaElementEncryptedMedia::webkitAddKey"); |
- if (!setEmeMode(EmeModePrefixed, exceptionState)) |
+ if (!setEmeMode(EmeModePrefixed)) { |
+ exceptionState.throwDOMException(InvalidStateError, "Mixed use of EME prefixed and unprefixed API not allowed."); |
return; |
+ } |
if (keySystem.isEmpty()) { |
exceptionState.throwDOMException(SyntaxError, "The key system provided is empty."); |
@@ -225,8 +412,10 @@ void HTMLMediaElementEncryptedMedia::cancelKeyRequest(blink::WebMediaPlayer* web |
{ |
WTF_LOG(Media, "HTMLMediaElementEncryptedMedia::webkitCancelKeyRequest"); |
- if (!setEmeMode(EmeModePrefixed, exceptionState)) |
+ if (!setEmeMode(EmeModePrefixed)) { |
+ exceptionState.throwDOMException(InvalidStateError, "Mixed use of EME prefixed and unprefixed API not allowed."); |
return; |
+ } |
if (keySystem.isEmpty()) { |
exceptionState.throwDOMException(SyntaxError, "The key system provided is empty."); |
@@ -341,7 +530,17 @@ void HTMLMediaElementEncryptedMedia::playerDestroyed(HTMLMediaElement& element) |
#endif |
HTMLMediaElementEncryptedMedia& thisElement = HTMLMediaElementEncryptedMedia::from(element); |
- thisElement.setMediaKeysInternal(element, 0); |
+ if (!thisElement.m_mediaKeys) |
+ return; |
+ |
+ ASSERT(thisElement.m_emeMode == EmeModeUnprefixed); |
+ thisElement.m_mediaKeys.clear(); |
+ |
+ // If a player is connected, tell it that the CDM has changed. |
+ if (element.webMediaPlayer()) { |
+ SetContentDecryptionModuleResult* result = new SetContentDecryptionModuleResult(0); |
+ element.webMediaPlayer()->setContentDecryptionModule(thisElement.contentDecryptionModule(), result->result()); |
+ } |
} |
blink::WebContentDecryptionModule* HTMLMediaElementEncryptedMedia::contentDecryptionModule(HTMLMediaElement& element) |