diff --git a/Source/WebCore/Modules/modern-media-controls/controls/icon-service.js b/Source/WebCore/Modules/modern-media-controls/controls/icon-service.js index b9769e5ea678a..96fad7495801f 100644 --- a/Source/WebCore/Modules/modern-media-controls/controls/icon-service.js +++ b/Source/WebCore/Modules/modern-media-controls/controls/icon-service.js @@ -74,6 +74,15 @@ const iconService = new class IconService { } // Public + get shadowRoot() + { + return this.shadowRootWeakRef ? this.shadowRootWeakRef.deref() : null; + } + + set shadowRoot(shadowRoot) + { + this.shadowRootWeakRef = new WeakRef(shadowRoot); + } imageForIconAndLayoutTraits(icon, layoutTraits) { diff --git a/Source/WebCore/Modules/modern-media-controls/media/media-controller.js b/Source/WebCore/Modules/modern-media-controls/media/media-controller.js index 77d6ccd5f0adb..af757460fcd4e 100644 --- a/Source/WebCore/Modules/modern-media-controls/media/media-controller.js +++ b/Source/WebCore/Modules/modern-media-controls/media/media-controller.js @@ -25,11 +25,10 @@ class MediaController { - constructor(shadowRoot, media, host) { - this.shadowRoot = shadowRoot; - this.media = media; + this.shadowRootWeakRef = new WeakRef(shadowRoot); + this.mediaWeakRef = new WeakRef(media); this.host = host; this.fullscreenChangeEventType = media.webkitSupportsPresentationMode ? "webkitpresentationmodechanged" : "webkitfullscreenchange"; @@ -65,6 +64,16 @@ class MediaController } // Public + get media() + { + return this.mediaWeakRef ? this.mediaWeakRef.deref() : null; + } + + get shadowRoot() + { + + return this.shadowRootWeakRef ? this.shadowRootWeakRef.deref() : null; + } get isAudio() { @@ -91,6 +100,9 @@ class MediaController get isFullscreen() { + if (!this.media) + return false; + return this.media.webkitSupportsPresentationMode ? this.media.webkitPresentationMode === "fullscreen" : this.media.webkitDisplayingFullscreen; } @@ -265,6 +277,22 @@ class MediaController return true; } + deinitialize() + { + this.shadowRoot.removeChild(this.container); + return true; + } + + reinitialize(shadowRoot, media, host) + { + iconService.shadowRoot = shadowRoot; + this.shadowRootWeakRef = new WeakRef(shadowRoot); + this.mediaWeakRef = new WeakRef(media); + this.host = host; + shadowRoot.appendChild(this.container); + return true; + } + // Private _supportingObjectClasses() diff --git a/Source/WebCore/html/HTMLMediaElement.cpp b/Source/WebCore/html/HTMLMediaElement.cpp index e1ec818dcea5d..6c3f2f3c82e4e 100644 --- a/Source/WebCore/html/HTMLMediaElement.cpp +++ b/Source/WebCore/html/HTMLMediaElement.cpp @@ -311,6 +311,25 @@ String convertEnumerationToString(HTMLMediaElement::SpeechSynthesisState enumera return values[static_cast(enumerationValue)]; } +String convertEnumerationToString(HTMLMediaElement::ControlsState enumerationValue) +{ + // None, Initializing, Ready, PartiallyDeinitialized + static const NeverDestroyed values[] = { + MAKE_STATIC_STRING_IMPL("None"), + MAKE_STATIC_STRING_IMPL("Initializing"), + MAKE_STATIC_STRING_IMPL("Ready"), + MAKE_STATIC_STRING_IMPL("PartiallyDeinitialized"), + }; + static_assert(!static_cast(HTMLMediaElement::ControlsState::None), "HTMLMediaElement::None is not 0 as expected"); + static_assert(static_cast(HTMLMediaElement::ControlsState::Initializing) == 1, "HTMLMediaElement::Initializing is not 1 as expected"); + static_assert(static_cast(HTMLMediaElement::ControlsState::Ready) == 2, "HTMLMediaElement::Ready is not 2 as expected"); + static_assert(static_cast(HTMLMediaElement::ControlsState::PartiallyDeinitialized) == 3, "HTMLMediaElement::PartiallyDeinitialized is not 3 as expected"); + ASSERT(static_cast(enumerationValue) < std::size(values)); + return values[static_cast(enumerationValue)]; +} + +static JSC::JSValue controllerJSValue(JSC::JSGlobalObject& lexicalGlobalObject, JSDOMGlobalObject&, HTMLMediaElement&); + class TrackDisplayUpdateScope { public: TrackDisplayUpdateScope(HTMLMediaElement& element) @@ -920,6 +939,38 @@ void HTMLMediaElement::pauseAfterDetachedTask() if (m_videoFullscreenMode == VideoFullscreenModeStandard) exitFullscreen(); +#if ENABLE(MODERN_MEDIA_CONTROLS) + if (m_controlsState == ControlsState::Initializing || m_controlsState == ControlsState::Ready) { + // Call MediaController.deinitialize() to get rid of circular references. + bool isDeinitialized = setupAndCallJS([this](JSDOMGlobalObject& globalObject, JSC::JSGlobalObject& lexicalGlobalObject, ScriptController&, DOMWrapperWorld&) { + auto& vm = globalObject.vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto controllerValue = controllerJSValue(lexicalGlobalObject, globalObject, *this); + RETURN_IF_EXCEPTION(scope, false); + auto* controllerObject = controllerValue.toObject(&lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, false); + + auto functionValue = controllerObject->get(&lexicalGlobalObject, JSC::Identifier::fromString(vm, "deinitialize"_s)); + if (UNLIKELY(scope.exception()) || functionValue.isUndefinedOrNull()) + return false; + + auto* function = functionValue.toObject(&lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, false); + + auto callData = JSC::getCallData(function); + if (callData.type == JSC::CallData::Type::None) + return false; + + auto resultValue = JSC::call(&lexicalGlobalObject, function, callData, controllerObject, JSC::MarkedArgumentBuffer()); + RETURN_IF_EXCEPTION(scope, false); + + return resultValue.toBoolean(&lexicalGlobalObject); + }); + m_controlsState = isDeinitialized ? ControlsState::PartiallyDeinitialized : m_controlsState; + } +#endif // ENABLE(MODERN_MEDIA_CONTROLS) + if (!m_player) return; @@ -8153,94 +8204,138 @@ bool HTMLMediaElement::ensureMediaControls() INFO_LOG(LOGIDENTIFIER); + ControlsState oldControlsState = m_controlsState; m_controlsState = ControlsState::Initializing; - auto controlsReady = setupAndCallJS([this, mediaControlsScripts = WTFMove(mediaControlsScripts)](JSDOMGlobalObject& globalObject, JSC::JSGlobalObject& lexicalGlobalObject, ScriptController& scriptController, DOMWrapperWorld& world) { - auto& vm = globalObject.vm(); - auto scope = DECLARE_CATCH_SCOPE(vm); + auto controlsReady = false; + if (oldControlsState == ControlsState::None) { + controlsReady = setupAndCallJS([this, mediaControlsScripts = WTFMove(mediaControlsScripts)](JSDOMGlobalObject& globalObject, JSC::JSGlobalObject& lexicalGlobalObject, ScriptController& scriptController, DOMWrapperWorld& world) { + auto& vm = globalObject.vm(); + auto scope = DECLARE_CATCH_SCOPE(vm); + + auto reportExceptionAndReturnFalse = [&] { + auto* exception = scope.exception(); + scope.clearException(); + reportException(&globalObject, exception); + return false; + }; + + for (auto& mediaControlsScript : mediaControlsScripts) { + if (mediaControlsScript.isEmpty()) + continue; + scriptController.evaluateInWorldIgnoringException(ScriptSourceCode(mediaControlsScript), world); + RETURN_IF_EXCEPTION(scope, reportExceptionAndReturnFalse()); + } - auto reportExceptionAndReturnFalse = [&] { - auto* exception = scope.exception(); - scope.clearException(); - reportException(&globalObject, exception); - return false; - }; + // The media controls script must provide a method with the following details. + // Name: createControls + // Parameters: + // 1. The ShadowRoot element that will hold the controls. + // 2. This object (and HTMLMediaElement). + // 3. The MediaControlsHost object. + // Return value: + // A reference to the created media controller instance. - for (auto& mediaControlsScript : mediaControlsScripts) { - if (mediaControlsScript.isEmpty()) - continue; - scriptController.evaluateInWorldIgnoringException(ScriptSourceCode(mediaControlsScript), world); + auto functionValue = globalObject.get(&lexicalGlobalObject, JSC::Identifier::fromString(vm, "createControls"_s)); + if (functionValue.isUndefinedOrNull()) + return false; + + if (!m_mediaControlsHost) + m_mediaControlsHost = MediaControlsHost::create(*this); + + auto mediaJSWrapper = toJS(&lexicalGlobalObject, &globalObject, *this); + auto mediaControlsHostJSWrapper = toJS(&lexicalGlobalObject, &globalObject, *m_mediaControlsHost); + + JSC::MarkedArgumentBuffer argList; + argList.append(toJS(&lexicalGlobalObject, &globalObject, ensureUserAgentShadowRoot())); + argList.append(mediaJSWrapper); + argList.append(mediaControlsHostJSWrapper); + ASSERT(!argList.hasOverflowed()); + + auto* function = functionValue.toObject(&lexicalGlobalObject); RETURN_IF_EXCEPTION(scope, reportExceptionAndReturnFalse()); - } + auto callData = JSC::getCallData(function); + if (callData.type == JSC::CallData::Type::None) + return false; - // The media controls script must provide a method with the following details. - // Name: createControls - // Parameters: - // 1. The ShadowRoot element that will hold the controls. - // 2. This object (and HTMLMediaElement). - // 3. The MediaControlsHost object. - // Return value: - // A reference to the created media controller instance. + auto controllerValue = JSC::call(&lexicalGlobalObject, function, callData, &globalObject, argList); + RETURN_IF_EXCEPTION(scope, reportExceptionAndReturnFalse()); - auto functionValue = globalObject.get(&lexicalGlobalObject, JSC::Identifier::fromString(vm, "createControls"_s)); - if (functionValue.isUndefinedOrNull()) - return false; + auto* controllerObject = JSC::jsDynamicCast(controllerValue); + if (!controllerObject) + return false; - if (!m_mediaControlsHost) - m_mediaControlsHost = MediaControlsHost::create(*this); + // Connect the Media, MediaControllerHost, and Controller so the GC knows about their relationship + auto* mediaJSWrapperObject = mediaJSWrapper.toObject(&lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, reportExceptionAndReturnFalse()); + auto controlsHost = JSC::Identifier::fromString(vm, "controlsHost"_s); - auto mediaJSWrapper = toJS(&lexicalGlobalObject, &globalObject, *this); - auto mediaControlsHostJSWrapper = toJS(&lexicalGlobalObject, &globalObject, *m_mediaControlsHost); + ASSERT(!mediaJSWrapperObject->hasProperty(&lexicalGlobalObject, controlsHost)); - JSC::MarkedArgumentBuffer argList; - argList.append(toJS(&lexicalGlobalObject, &globalObject, ensureUserAgentShadowRoot())); - argList.append(mediaJSWrapper); - argList.append(mediaControlsHostJSWrapper); - ASSERT(!argList.hasOverflowed()); + mediaJSWrapperObject->putDirect(vm, controlsHost, mediaControlsHostJSWrapper, JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::ReadOnly); - auto* function = functionValue.toObject(&lexicalGlobalObject); - RETURN_IF_EXCEPTION(scope, reportExceptionAndReturnFalse()); - auto callData = JSC::getCallData(function); - if (callData.type == JSC::CallData::Type::None) - return false; + auto* mediaControlsHostJSWrapperObject = JSC::jsDynamicCast(mediaControlsHostJSWrapper); + if (!mediaControlsHostJSWrapperObject) + return false; - auto controllerValue = JSC::call(&lexicalGlobalObject, function, callData, &globalObject, argList); - RETURN_IF_EXCEPTION(scope, reportExceptionAndReturnFalse()); + auto controller = builtinNames(vm).controllerPublicName(); - auto* controllerObject = JSC::jsDynamicCast(controllerValue); - if (!controllerObject) - return false; + ASSERT(!controllerObject->hasProperty(&lexicalGlobalObject, controller)); - // Connect the Media, MediaControllerHost, and Controller so the GC knows about their relationship - auto* mediaJSWrapperObject = mediaJSWrapper.toObject(&lexicalGlobalObject); - RETURN_IF_EXCEPTION(scope, reportExceptionAndReturnFalse()); - auto controlsHost = JSC::Identifier::fromString(vm, "controlsHost"_s); + mediaControlsHostJSWrapperObject->putDirect(vm, controller, controllerValue, JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::ReadOnly); - ASSERT(!mediaJSWrapperObject->hasProperty(&lexicalGlobalObject, controlsHost)); + if (m_mediaControlsDependOnPageScaleFactor) + updatePageScaleFactorJSProperty(); - mediaJSWrapperObject->putDirect(vm, controlsHost, mediaControlsHostJSWrapper, JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::ReadOnly); + RETURN_IF_EXCEPTION(scope, reportExceptionAndReturnFalse()); - auto* mediaControlsHostJSWrapperObject = JSC::jsDynamicCast(mediaControlsHostJSWrapper); - if (!mediaControlsHostJSWrapperObject) - return false; + updateUsesLTRUserInterfaceLayoutDirectionJSProperty(); + RETURN_IF_EXCEPTION(scope, reportExceptionAndReturnFalse()); - auto controller = builtinNames(vm).controllerPublicName(); + return true; + }); +} else if (oldControlsState == ControlsState::PartiallyDeinitialized) { + controlsReady = setupAndCallJS([this](JSDOMGlobalObject& globalObject, JSC::JSGlobalObject& lexicalGlobalObject, ScriptController&, DOMWrapperWorld&) { + auto& vm = globalObject.vm(); + auto scope = DECLARE_THROW_SCOPE(vm); - ASSERT(!controllerObject->hasProperty(&lexicalGlobalObject, controller)); + auto controllerValue = controllerJSValue(lexicalGlobalObject, globalObject, *this); + RETURN_IF_EXCEPTION(scope, false); + auto* controllerObject = controllerValue.toObject(&lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, false); - mediaControlsHostJSWrapperObject->putDirect(vm, controller, controllerValue, JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::ReadOnly); + auto functionValue = controllerObject->get(&lexicalGlobalObject, JSC::Identifier::fromString(vm, "reinitialize"_s)); + if (UNLIKELY(scope.exception()) || functionValue.isUndefinedOrNull()) + return false; - if (m_mediaControlsDependOnPageScaleFactor) - updatePageScaleFactorJSProperty(); + if (!m_mediaControlsHost) + m_mediaControlsHost = MediaControlsHost::create(*this); - RETURN_IF_EXCEPTION(scope, reportExceptionAndReturnFalse()); + auto mediaJSWrapper = toJS(&lexicalGlobalObject, &globalObject, *this); + auto mediaControlsHostJSWrapper = toJS(&lexicalGlobalObject, &globalObject, *m_mediaControlsHost.copyRef()); - updateUsesLTRUserInterfaceLayoutDirectionJSProperty(); - RETURN_IF_EXCEPTION(scope, reportExceptionAndReturnFalse()); + JSC::MarkedArgumentBuffer argList; + argList.append(toJS(&lexicalGlobalObject, &globalObject, Ref { ensureUserAgentShadowRoot() })); + argList.append(mediaJSWrapper); + argList.append(mediaControlsHostJSWrapper); + ASSERT(!argList.hasOverflowed()); - return true; - }); - m_controlsState = controlsReady ? ControlsState::Ready : ControlsState::None; + auto* function = functionValue.toObject(&lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, false); + + auto callData = JSC::getCallData(function); + if (callData.type == JSC::CallData::Type::None) + return false; + + auto resultValue = JSC::call(&lexicalGlobalObject, function, callData, controllerObject, argList); + RETURN_IF_EXCEPTION(scope, false); + + return resultValue.toBoolean(&lexicalGlobalObject); + }); + } else + ASSERT_NOT_REACHED(); + + m_controlsState = controlsReady ? ControlsState::Ready : oldControlsState; return controlsReady; } diff --git a/Source/WebCore/html/HTMLMediaElement.h b/Source/WebCore/html/HTMLMediaElement.h index 605a0c156f5e6..fb2c1fb0c7f20 100644 --- a/Source/WebCore/html/HTMLMediaElement.h +++ b/Source/WebCore/html/HTMLMediaElement.h @@ -1215,7 +1215,8 @@ class HTMLMediaElement bool m_shouldVideoPlaybackRequireUserGesture : 1; bool m_volumeLocked : 1; - enum class ControlsState : uint8_t { None, Initializing, Ready }; + enum class ControlsState : uint8_t { None, Initializing, Ready, PartiallyDeinitialized }; + friend String convertEnumerationToString(HTMLMediaElement::ControlsState enumerationValue); ControlsState m_controlsState { ControlsState::None }; AutoplayEventPlaybackState m_autoplayEventPlaybackState { AutoplayEventPlaybackState::None };