2 * Copyright (C) 2013 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
27 #include "modules/encryptedmedia/MediaKeySession.h"
29 #include "bindings/core/v8/DOMWrapperWorld.h"
30 #include "bindings/core/v8/ScriptPromise.h"
31 #include "bindings/core/v8/ScriptPromiseResolver.h"
32 #include "bindings/core/v8/ScriptState.h"
33 #include "core/dom/ExceptionCode.h"
34 #include "core/events/Event.h"
35 #include "core/events/GenericEventQueue.h"
36 #include "core/html/MediaKeyError.h"
37 #include "modules/encryptedmedia/MediaKeyMessageEvent.h"
38 #include "modules/encryptedmedia/MediaKeys.h"
39 #include "modules/encryptedmedia/SimpleContentDecryptionModuleResult.h"
40 #include "platform/ContentDecryptionModuleResult.h"
41 #include "platform/ContentType.h"
42 #include "platform/Logging.h"
43 #include "platform/MIMETypeRegistry.h"
44 #include "platform/Timer.h"
45 #include "public/platform/WebContentDecryptionModule.h"
46 #include "public/platform/WebContentDecryptionModuleException.h"
47 #include "public/platform/WebContentDecryptionModuleSession.h"
48 #include "public/platform/WebString.h"
49 #include "public/platform/WebURL.h"
50 #include "wtf/ArrayBuffer.h"
51 #include "wtf/ArrayBufferView.h"
55 static bool isKeySystemSupportedWithInitDataType(const String& keySystem, const String& initDataType)
57 ASSERT(!keySystem.isEmpty());
59 // FIXME: initDataType != contentType. Implement this properly.
60 // http://crbug.com/385874.
61 String contentType = initDataType;
62 if (initDataType == "webm") {
63 contentType = "video/webm";
64 } else if (initDataType == "cenc") {
65 contentType = "video/mp4";
68 ContentType type(contentType);
69 return MIMETypeRegistry::isSupportedEncryptedMediaMIMEType(keySystem, type.type(), type.parameter("codecs"));
72 // A class holding a pending action.
73 class MediaKeySession::PendingAction : public GarbageCollectedFinalized<MediaKeySession::PendingAction> {
81 Type type() const { return m_type; }
83 const Persistent<ContentDecryptionModuleResult> result() const
88 const RefPtr<ArrayBuffer> data() const
90 ASSERT(m_type == GenerateRequest || m_type == Update);
94 const String& initDataType() const
96 ASSERT(m_type == GenerateRequest);
97 return m_initDataType;
100 static PendingAction* CreatePendingGenerateRequest(ContentDecryptionModuleResult* result, const String& initDataType, PassRefPtr<ArrayBuffer> initData)
104 return new PendingAction(GenerateRequest, result, initDataType, initData);
107 static PendingAction* CreatePendingUpdate(ContentDecryptionModuleResult* result, PassRefPtr<ArrayBuffer> data)
111 return new PendingAction(Update, result, String(), data);
114 static PendingAction* CreatePendingRelease(ContentDecryptionModuleResult* result)
117 return new PendingAction(Release, result, String(), PassRefPtr<ArrayBuffer>());
124 void trace(Visitor* visitor)
126 visitor->trace(m_result);
130 PendingAction(Type type, ContentDecryptionModuleResult* result, const String& initDataType, PassRefPtr<ArrayBuffer> data)
133 , m_initDataType(initDataType)
139 const Member<ContentDecryptionModuleResult> m_result;
140 const String m_initDataType;
141 const RefPtr<ArrayBuffer> m_data;
144 // This class wraps the promise resolver used when initializing a new session
145 // and is passed to Chromium to fullfill the promise. This implementation of
146 // completeWithSession() will resolve the promise with void, while
147 // completeWithError() will reject the promise with an exception. complete()
148 // is not expected to be called, and will reject the promise.
149 class NewSessionResult : public ContentDecryptionModuleResult {
151 NewSessionResult(ScriptState* scriptState, MediaKeySession* session)
152 : m_resolver(ScriptPromiseResolver::create(scriptState))
155 WTF_LOG(Media, "NewSessionResult(%p)", this);
158 virtual ~NewSessionResult()
160 WTF_LOG(Media, "~NewSessionResult(%p)", this);
163 // ContentDecryptionModuleResult implementation.
164 virtual void complete() OVERRIDE
166 ASSERT_NOT_REACHED();
167 completeWithDOMException(InvalidStateError, "Unexpected completion.");
170 virtual void completeWithSession(WebContentDecryptionModuleResult::SessionStatus status) OVERRIDE
172 if (status != WebContentDecryptionModuleResult::NewSession) {
173 ASSERT_NOT_REACHED();
174 completeWithDOMException(InvalidStateError, "Unexpected completion.");
177 m_session->finishGenerateRequest();
178 m_resolver->resolve();
182 virtual void completeWithError(WebContentDecryptionModuleException exceptionCode, unsigned long systemCode, const WebString& errorMessage) OVERRIDE
184 completeWithDOMException(WebCdmExceptionToExceptionCode(exceptionCode), errorMessage);
187 // It is only valid to call this before completion.
188 ScriptPromise promise() { return m_resolver->promise(); }
190 void trace(Visitor* visitor)
192 visitor->trace(m_session);
193 ContentDecryptionModuleResult::trace(visitor);
197 // Reject the promise with a DOMException.
198 void completeWithDOMException(ExceptionCode code, const String& errorMessage)
200 m_resolver->reject(DOMException::create(code, errorMessage));
204 RefPtr<ScriptPromiseResolver> m_resolver;
205 Member<MediaKeySession> m_session;
208 MediaKeySession* MediaKeySession::create(ScriptState* scriptState, MediaKeys* mediaKeys, const String& sessionType)
210 RefPtrWillBeRawPtr<MediaKeySession> session = adoptRefCountedGarbageCollectedWillBeNoop(new MediaKeySession(scriptState, mediaKeys, sessionType));
211 session->suspendIfNeeded();
212 return session.get();
215 MediaKeySession::MediaKeySession(ScriptState* scriptState, MediaKeys* mediaKeys, const String& sessionType)
216 : ActiveDOMObject(scriptState->executionContext())
217 , m_keySystem(mediaKeys->keySystem())
218 , m_asyncEventQueue(GenericEventQueue::create(this))
219 , m_mediaKeys(mediaKeys)
220 , m_sessionType(sessionType)
221 , m_isUninitialized(true)
222 , m_isCallable(false)
224 , m_closedPromise(new ClosedPromise(scriptState->executionContext(), this, ClosedPromise::Closed))
225 , m_actionTimer(this, &MediaKeySession::actionTimerFired)
227 WTF_LOG(Media, "MediaKeySession(%p)::MediaKeySession", this);
229 // Create the matching Chromium object. It will not be usable until
230 // initializeNewSession() is called in response to the user calling
231 // generateRequest().
232 WebContentDecryptionModule* cdm = mediaKeys->contentDecryptionModule();
233 m_session = adoptPtr(cdm->createSession());
234 m_session->setClientInterface(this);
236 // MediaKeys::createSession(), step 2.
237 // 2.1 Let the sessionId attribute be the empty string.
238 ASSERT(sessionId().isEmpty());
240 // 2.2 Let the expiration attribute be NaN.
241 // FIXME: Add expiration property.
243 // 2.3 Let the closed attribute be a new promise.
244 ASSERT(!closed(scriptState).isUndefinedOrNull());
246 // 2.4 Let the session type be sessionType.
247 ASSERT(sessionType == m_sessionType);
249 // 2.5 Let uninitialized be true.
250 ASSERT(m_isUninitialized);
252 // 2.6 Let callable be false.
253 ASSERT(!m_isCallable);
256 MediaKeySession::~MediaKeySession()
258 WTF_LOG(Media, "MediaKeySession(%p)::~MediaKeySession", this);
261 // MediaKeySession and m_asyncEventQueue always become unreachable
262 // together. So MediaKeySession and m_asyncEventQueue are destructed in the
263 // same GC. We don't need to call cancelAllEvents explicitly in Oilpan.
264 m_asyncEventQueue->cancelAllEvents();
268 void MediaKeySession::setError(MediaKeyError* error)
273 String MediaKeySession::sessionId() const
275 return m_session->sessionId();
278 ScriptPromise MediaKeySession::closed(ScriptState* scriptState)
280 return m_closedPromise->promise(scriptState->world());
283 ScriptPromise MediaKeySession::generateRequest(ScriptState* scriptState, const String& initDataType, ArrayBuffer* initData)
285 RefPtr<ArrayBuffer> initDataCopy = ArrayBuffer::create(initData->data(), initData->byteLength());
286 return generateRequestInternal(scriptState, initDataType, initDataCopy.release());
289 ScriptPromise MediaKeySession::generateRequest(ScriptState* scriptState, const String& initDataType, ArrayBufferView* initData)
291 RefPtr<ArrayBuffer> initDataCopy = ArrayBuffer::create(initData->baseAddress(), initData->byteLength());
292 return generateRequestInternal(scriptState, initDataType, initDataCopy.release());
295 ScriptPromise MediaKeySession::generateRequestInternal(ScriptState* scriptState, const String& initDataType, PassRefPtr<ArrayBuffer> initData)
297 WTF_LOG(Media, "MediaKeySession(%p)::generateRequest %s", this, initDataType.ascii().data());
299 // From https://dvcs.w3.org/hg/html-media/raw-file/default/encrypted-media/encrypted-media.html#dom-generaterequest:
300 // The generateRequest(initDataType, initData) method creates a new session
301 // for the specified initData. It must run the following steps:
303 // 1. If this object's uninitialized value is false, return a promise
304 // rejected with a new DOMException whose name is "InvalidStateError".
305 if (!m_isUninitialized) {
306 return ScriptPromise::rejectWithDOMException(
307 scriptState, DOMException::create(InvalidStateError, "The session is already initialized."));
310 // 2. Let this object's uninitialized be false.
311 m_isUninitialized = false;
313 // 3. If initDataType is an empty string, return a promise rejected with a
314 // new DOMException whose name is "InvalidAccessError".
315 if (initDataType.isEmpty()) {
316 return ScriptPromise::rejectWithDOMException(
317 scriptState, DOMException::create(InvalidAccessError, "The initDataType parameter is empty."));
320 // 4. If initData is an empty array, return a promise rejected with a new
321 // DOMException whose name is"InvalidAccessError".
322 if (!initData->byteLength()) {
323 return ScriptPromise::rejectWithDOMException(
324 scriptState, DOMException::create(InvalidAccessError, "The initData parameter is empty."));
327 // 5. Let media keys be the MediaKeys object that created this object.
328 // (Use m_mediaKey, which was set in the constructor.)
330 // 6. If the content decryption module corresponding to media keys's
331 // keySystem attribute does not support initDataType as an initialization
332 // data type, return a promise rejected with a new DOMException whose
333 // name is "NotSupportedError". String comparison is case-sensitive.
334 if (!isKeySystemSupportedWithInitDataType(m_keySystem, initDataType)) {
335 return ScriptPromise::rejectWithDOMException(
336 scriptState, DOMException::create(NotSupportedError, "The initialization data type '" + initDataType + "' is not supported by the key system."));
339 // 7. Let init data be a copy of the contents of the initData parameter.
340 // (Done before calling this method.)
342 // 8. Let session type be this object's session type.
343 // (Done in constructor.)
345 // 9. Let promise be a new promise.
346 NewSessionResult* result = new NewSessionResult(scriptState, this);
347 ScriptPromise promise = result->promise();
349 // 10. Run the following steps asynchronously (documented in
350 // actionTimerFired())
351 m_pendingActions.append(PendingAction::CreatePendingGenerateRequest(result, initDataType, initData));
352 ASSERT(!m_actionTimer.isActive());
353 m_actionTimer.startOneShot(0, FROM_HERE);
355 // 11. Return promise.
359 ScriptPromise MediaKeySession::update(ScriptState* scriptState, ArrayBuffer* response)
361 RefPtr<ArrayBuffer> responseCopy = ArrayBuffer::create(response->data(), response->byteLength());
362 return updateInternal(scriptState, responseCopy.release());
365 ScriptPromise MediaKeySession::update(ScriptState* scriptState, ArrayBufferView* response)
367 RefPtr<ArrayBuffer> responseCopy = ArrayBuffer::create(response->baseAddress(), response->byteLength());
368 return updateInternal(scriptState, responseCopy.release());
371 ScriptPromise MediaKeySession::updateInternal(ScriptState* scriptState, PassRefPtr<ArrayBuffer> response)
373 WTF_LOG(Media, "MediaKeySession(%p)::update", this);
376 // From <https://dvcs.w3.org/hg/html-media/raw-file/default/encrypted-media/encrypted-media.html#dom-update>:
377 // The update(response) method provides messages, including licenses, to the
378 // CDM. It must run the following steps:
380 // 1. If response is an empty array, return a promise rejected with a new
381 // DOMException whose name is "InvalidAccessError" and that has the
382 // message "The response parameter is empty."
383 if (!response->byteLength()) {
384 return ScriptPromise::rejectWithDOMException(
385 scriptState, DOMException::create(InvalidAccessError, "The response parameter is empty."));
388 // 2. Let message be a copy of the contents of the response parameter.
389 // (Copied in the caller.)
391 // 3. Let promise be a new promise.
392 SimpleContentDecryptionModuleResult* result = new SimpleContentDecryptionModuleResult(scriptState);
393 ScriptPromise promise = result->promise();
395 // 4. Run the following steps asynchronously (documented in
396 // actionTimerFired())
397 m_pendingActions.append(PendingAction::CreatePendingUpdate(result, response));
398 if (!m_actionTimer.isActive())
399 m_actionTimer.startOneShot(0, FROM_HERE);
401 // 5. Return promise.
405 ScriptPromise MediaKeySession::release(ScriptState* scriptState)
407 WTF_LOG(Media, "MediaKeySession(%p)::release", this);
408 SimpleContentDecryptionModuleResult* result = new SimpleContentDecryptionModuleResult(scriptState);
409 ScriptPromise promise = result->promise();
411 // From <https://dvcs.w3.org/hg/html-media/raw-file/default/encrypted-media/encrypted-media.html#dom-close>:
412 // The close() method allows an application to indicate that it no longer
413 // needs the session and the CDM should release any resources associated
414 // with this object and close it. The returned promise is resolved when the
415 // request has been processed, and the closed attribute promise is resolved
416 // when the session is closed. It must run the following steps:
418 // 1. If the Session Close algorithm has been run on this object, return a
419 // promise fulfilled with undefined.
425 // 2. Let promise be a new promise.
426 // (Created earlier so it was available in step 1.)
428 // 3. Run the following steps asynchronously (documented in
429 // actionTimerFired()).
430 m_pendingActions.append(PendingAction::CreatePendingRelease(result));
431 if (!m_actionTimer.isActive())
432 m_actionTimer.startOneShot(0, FROM_HERE);
434 // 4. Return promise.
438 void MediaKeySession::actionTimerFired(Timer<MediaKeySession>*)
440 ASSERT(m_pendingActions.size());
442 // Resolving promises now run synchronously and may result in additional
443 // actions getting added to the queue. As a result, swap the queue to
444 // a local copy to avoid problems if this happens.
445 HeapDeque<Member<PendingAction> > pendingActions;
446 pendingActions.swap(m_pendingActions);
448 while (!pendingActions.isEmpty()) {
449 PendingAction* action = pendingActions.takeFirst();
451 switch (action->type()) {
452 case PendingAction::GenerateRequest:
453 WTF_LOG(Media, "MediaKeySession(%p)::actionTimerFired: GenerateRequest", this);
455 // 10.1 Let request be null.
456 // 10.2 Let cdm be the CDM loaded during the initialization of
458 // 10.3 Use the cdm to execute the following steps:
459 // 10.3.1 If the init data is not valid for initDataType, reject
460 // promise with a new DOMException whose name is
461 // "InvalidAccessError".
462 // 10.3.2 If the init data is not supported by the cdm, reject
463 // promise with a new DOMException whose name is
464 // "NotSupportedError".
465 // 10.3.3 Let request be a request (e.g. a license request)
466 // generated based on the init data, which is interpreted
467 // per initDataType, and session type.
468 m_session->initializeNewSession(action->initDataType(), static_cast<unsigned char*>(action->data()->data()), action->data()->byteLength(), m_sessionType, action->result()->result());
470 // Remainder of steps executed in finishGenerateRequest(), called
471 // when |result| is resolved.
474 case PendingAction::Update:
475 WTF_LOG(Media, "MediaKeySession(%p)::actionTimerFired: Update", this);
476 // NOTE: Continued from step 4 of MediaKeySession::update().
477 // Continue the update call by passing message to the cdm. Once
478 // completed, it will resolve/reject the promise.
479 m_session->update(static_cast<unsigned char*>(action->data()->data()), action->data()->byteLength(), action->result()->result());
482 case PendingAction::Release:
483 WTF_LOG(Media, "MediaKeySession(%p)::actionTimerFired: Release", this);
484 // NOTE: Continued from step 3 of MediaKeySession::release().
485 // 3.1 Let cdm be the cdm loaded in create().
486 // 3.2 Use the cdm to execute the following steps:
487 // 3.2.1 Process the close request. Do not remove stored session data.
488 // 3.2.2 If the previous step caused the session to be closed, run the
489 // Session Close algorithm on this object.
490 // 3.3 Resolve promise with undefined.
491 m_session->release(action->result()->result());
497 void MediaKeySession::finishGenerateRequest()
499 // 10.4 Set the sessionId attribute to a unique Session ID string.
500 // It may be obtained from cdm.
501 ASSERT(!sessionId().isEmpty());
503 // 10.5 If any of the preceding steps failed, reject promise with a new
504 // DOMException whose name is the appropriate error name.
505 // (Done by call to completeWithError()).
507 // 10.6 Add an entry for the value of the sessionId attribute to
508 // media keys's list of active session IDs.
509 // FIXME: Is this required?
510 // https://www.w3.org/Bugs/Public/show_bug.cgi?id=26758
512 // 10.7 Run the Queue a "message" Event algorithm on the session,
513 // providing request and null.
514 // (Done by the CDM).
516 // 10.8 Let this object's callable be true.
520 // Queue a task to fire a simple event named keymessage at the new object.
521 void MediaKeySession::message(const unsigned char* message, size_t messageLength, const WebURL& destinationURL)
523 WTF_LOG(Media, "MediaKeySession(%p)::message", this);
525 // Verify that 'message' not fired before session initialization is complete.
526 ASSERT(m_isCallable);
528 MediaKeyMessageEventInit init;
529 init.bubbles = false;
530 init.cancelable = false;
531 init.message = ArrayBuffer::create(static_cast<const void*>(message), messageLength);
532 init.destinationURL = destinationURL.string();
534 RefPtrWillBeRawPtr<MediaKeyMessageEvent> event = MediaKeyMessageEvent::create(EventTypeNames::message, init);
535 event->setTarget(this);
536 m_asyncEventQueue->enqueueEvent(event.release());
539 void MediaKeySession::ready()
541 WTF_LOG(Media, "MediaKeySession(%p)::ready", this);
543 RefPtrWillBeRawPtr<Event> event = Event::create(EventTypeNames::ready);
544 event->setTarget(this);
545 m_asyncEventQueue->enqueueEvent(event.release());
548 void MediaKeySession::close()
550 WTF_LOG(Media, "MediaKeySession(%p)::close", this);
552 // Once closed, the session can no longer be the target of events from
553 // the CDM so this object can be garbage collected.
556 // Resolve the closed promise.
557 m_closedPromise->resolve(V8UndefinedType());
560 // Queue a task to fire a simple event named keyadded at the MediaKeySession object.
561 void MediaKeySession::error(MediaKeyErrorCode errorCode, unsigned long systemCode)
563 WTF_LOG(Media, "MediaKeySession(%p)::error: errorCode=%d, systemCode=%lu", this, errorCode, systemCode);
565 MediaKeyError::Code mediaKeyErrorCode = MediaKeyError::MEDIA_KEYERR_UNKNOWN;
567 case MediaKeyErrorCodeUnknown:
568 mediaKeyErrorCode = MediaKeyError::MEDIA_KEYERR_UNKNOWN;
570 case MediaKeyErrorCodeClient:
571 mediaKeyErrorCode = MediaKeyError::MEDIA_KEYERR_CLIENT;
575 // 1. Create a new MediaKeyError object with the following attributes:
576 // code = the appropriate MediaKeyError code
577 // systemCode = a Key System-specific value, if provided, and 0 otherwise
578 // 2. Set the MediaKeySession object's error attribute to the error object created in the previous step.
579 m_error = MediaKeyError::create(mediaKeyErrorCode, systemCode);
581 // 3. queue a task to fire a simple event named keyerror at the MediaKeySession object.
582 RefPtrWillBeRawPtr<Event> event = Event::create(EventTypeNames::error);
583 event->setTarget(this);
584 m_asyncEventQueue->enqueueEvent(event.release());
587 void MediaKeySession::error(WebContentDecryptionModuleException exception, unsigned long systemCode, const WebString& errorMessage)
589 WTF_LOG(Media, "MediaKeySession::error: exception=%d, systemCode=%lu", exception, systemCode);
591 // FIXME: EME-WD MediaKeyError now derives from DOMException. Figure out how
592 // to implement this without breaking prefixed EME, which has a totally
593 // different definition. The spec may also change to be just a DOMException.
594 // For now, simply generate an existing MediaKeyError.
595 MediaKeyErrorCode errorCode;
597 case WebContentDecryptionModuleExceptionClientError:
598 errorCode = MediaKeyErrorCodeClient;
601 // All other exceptions get converted into Unknown.
602 errorCode = MediaKeyErrorCodeUnknown;
605 error(errorCode, systemCode);
608 const AtomicString& MediaKeySession::interfaceName() const
610 return EventTargetNames::MediaKeySession;
613 ExecutionContext* MediaKeySession::executionContext() const
615 return ActiveDOMObject::executionContext();
618 bool MediaKeySession::hasPendingActivity() const
620 // Remain around if there are pending events or MediaKeys is still around
621 // and we're not closed.
622 WTF_LOG(Media, "MediaKeySession(%p)::hasPendingActivity %s%s%s%s", this,
623 ActiveDOMObject::hasPendingActivity() ? " ActiveDOMObject::hasPendingActivity()" : "",
624 !m_pendingActions.isEmpty() ? " !m_pendingActions.isEmpty()" : "",
625 m_asyncEventQueue->hasPendingEvents() ? " m_asyncEventQueue->hasPendingEvents()" : "",
626 (m_mediaKeys && !m_isClosed) ? " m_mediaKeys && !m_isClosed" : "");
628 return ActiveDOMObject::hasPendingActivity()
629 || !m_pendingActions.isEmpty()
630 || m_asyncEventQueue->hasPendingEvents()
631 || (m_mediaKeys && !m_isClosed);
634 void MediaKeySession::stop()
636 // Stop the CDM from firing any more events for this session.
640 if (m_actionTimer.isActive())
641 m_actionTimer.stop();
642 m_pendingActions.clear();
643 m_asyncEventQueue->close();
646 void MediaKeySession::trace(Visitor* visitor)
648 visitor->trace(m_error);
649 visitor->trace(m_asyncEventQueue);
650 visitor->trace(m_pendingActions);
651 visitor->trace(m_mediaKeys);
652 visitor->trace(m_closedPromise);
653 EventTargetWithInlineData::trace(visitor);