Skip to content
Permalink
Browse files
Media element never populates its UA shadow if it was initially creat…
…ed in a document without browsing context

https://bugs.webkit.org/show_bug.cgi?id=222657
<rdar://problem/75266631>

Reviewed by Eric Carlson.

Previously, `HTMLMediaElement` relied on `didAddUserAgentShadowRoot` to call the JS `createControls`
that actually populates the UA shadow root. This did not work with `adoptNode` because in that case
the UA shadow root already existed and was already attached to the `<video>`, so it was not called.

Rather than rely on this single path to set up everything, instead have a `ControlsState` enum that
`HTMLMediaElement` can internally reason about to decide whether or not it needs to inject the JS
that creates `createControls` and then call it. This also has the added benefit of having those two
steps now be in the same place, whereas before they were split across two unrelated functions.

* Source/WebCore/html/HTMLMediaElement.h:
* Source/WebCore/html/HTMLMediaElement.cpp:
(WebCore::HTMLMediaElement::HTMLMediaElement):
(WebCore::HTMLMediaElement::updateCaptionContainer):
(WebCore::HTMLMediaElement::virtualHasPendingActivity const):
(WebCore::HTMLMediaElement::configureTextTrackDisplay):
(WebCore::HTMLMediaElement::updateTextTrackDisplay):
(WebCore::HTMLMediaElement::updateTextTrackRepresentationImageIfNeeded):
(WebCore::HTMLMediaElement::configureMediaControls):
(WebCore::HTMLMediaElement::ensureMediaControls): Added.
(WebCore::HTMLMediaElement::getCurrentMediaControlsStatus):
(WebCore::HTMLMediaElement::ensureMediaControlsShadowRoot): Deleted.
(WebCore::HTMLMediaElement::ensureMediaControlsInjectedScript): Deleted.
(WebCore::HTMLMediaElement::didAddUserAgentShadowRoot): Deleted.

* LayoutTests/media/audio-controls-adoptNode.html: Added.
* LayoutTests/media/audio-controls-adoptNode-expected.txt: Added.
* LayoutTests/media/video-controls-adoptNode.html: Added.
* LayoutTests/media/video-controls-adoptNode-expected.txt: Added.

Canonical link: https://commits.webkit.org/253225@main
  • Loading branch information
dcrousso committed Aug 8, 2022
1 parent c602754 commit f331cc98c1de65a0a2523c674859e01c1011fc8d
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 84 deletions.
@@ -0,0 +1,6 @@
Test that UA shadow from 'controls' attribute is still populated when using adoptNode.

EXPECTED (mediaElement.getAttribute('controls') == '') OK
EXPECTED (shadow?.childNodes.length > '0') OK
END OF TEST

@@ -0,0 +1,16 @@
<p>Test that UA shadow from 'controls' attribute is still populated when using adoptNode.<p>
<script src=video-test.js></script>
<script>
var parser = new DOMParser;
mediaElement = parser.parseFromString("<audio controls></audio>", "text/html").body.children[0];

document.body.appendChild(document.adoptNode(mediaElement));

testExpected("mediaElement.getAttribute('controls')", "");

var shadow = window.internals?.shadowRoot(mediaElement);
testExpected("shadow?.childNodes.length", 0, ">");

endTest();

</script>
@@ -0,0 +1,6 @@
Test that UA shadow from 'controls' attribute is still populated when using adoptNode.

EXPECTED (mediaElement.getAttribute('controls') == '') OK
EXPECTED (shadow?.childNodes.length > '0') OK
END OF TEST

@@ -0,0 +1,16 @@
<p>Test that UA shadow from 'controls' attribute is still populated when using adoptNode.<p>
<script src=video-test.js></script>
<script>
var parser = new DOMParser;
mediaElement = parser.parseFromString("<video controls></video>", "text/html").body.children[0];

document.body.appendChild(document.adoptNode(mediaElement));

testExpected("mediaElement.getAttribute('controls')", "");

var shadow = window.internals?.shadowRoot(mediaElement);
testExpected("shadow?.childNodes.length", 0, ">");

endTest();

</script>
@@ -442,7 +442,6 @@ HTMLMediaElement::HTMLMediaElement(const QualifiedName& tagName, Document& docum
, m_havePreparedToPlay(false)
, m_parsingInProgress(createdByParser)
, m_elementIsHidden(document.hidden())
, m_creatingControls(false)
, m_receivedLayoutSizeChanged(false)
, m_hasEverNotifiedAboutPlaying(false)
, m_hasEverHadAudio(false)
@@ -4641,16 +4640,6 @@ static JSC::JSValue controllerJSValue(JSC::JSGlobalObject& lexicalGlobalObject,
return controllerJSWrapper;
}

void HTMLMediaElement::ensureMediaControlsShadowRoot()
{
if (m_creatingControls)
return;

m_creatingControls = true;
ensureUserAgentShadowRoot();
m_creatingControls = false;
}

bool HTMLMediaElement::setupAndCallJS(const JSSetupFunction& task)
{
Page* page = document().page();
@@ -4673,14 +4662,9 @@ void HTMLMediaElement::updateCaptionContainer()
if (m_haveSetUpCaptionContainer)
return;

if (!ensureMediaControlsInjectedScript())
if (!ensureMediaControls())
return;

ensureMediaControlsShadowRoot();

if (!m_mediaControlsHost)
m_mediaControlsHost = MediaControlsHost::create(*this);

setupAndCallJS([this](JSDOMGlobalObject& globalObject, JSC::JSGlobalObject& lexicalGlobalObject, ScriptController&, DOMWrapperWorld&) {
auto& vm = globalObject.vm();
auto scope = DECLARE_CATCH_SCOPE(vm);
@@ -6077,7 +6061,7 @@ bool HTMLMediaElement::hasLiveSource() const

bool HTMLMediaElement::virtualHasPendingActivity() const
{
return m_creatingControls
return m_controlsState == ControlsState::Initializing
|| (hasAudio() && isPlaying())
|| (hasLiveSource() && hasEventListeners());
}
@@ -6753,24 +6737,19 @@ void HTMLMediaElement::configureTextTrackDisplay(TextTrackVisibilityCheckType ch
if (!m_haveVisibleTextTrack)
return;

ensureMediaControlsShadowRoot();
updateTextTrackDisplay();
}

void HTMLMediaElement::updateTextTrackDisplay()
{
ensureMediaControlsShadowRoot();
if (!m_mediaControlsHost)
m_mediaControlsHost = MediaControlsHost::create(*this);
m_mediaControlsHost->updateTextTrackContainer();
if (ensureMediaControls())
m_mediaControlsHost->updateTextTrackContainer();
}

void HTMLMediaElement::updateTextTrackRepresentationImageIfNeeded()
{
ensureMediaControlsShadowRoot();
if (!m_mediaControlsHost)
m_mediaControlsHost = MediaControlsHost::create(*this);
m_mediaControlsHost->updateTextTrackRepresentationImageIfNeeded();
if (ensureMediaControls())
m_mediaControlsHost->updateTextTrackRepresentationImageIfNeeded();
}

void HTMLMediaElement::setClosedCaptionsVisible(bool closedCaptionVisible)
@@ -6933,7 +6912,7 @@ void HTMLMediaElement::configureMediaControls()
if (!requireControls || !isConnected() || !inActiveDocument())
return;

ensureMediaControlsShadowRoot();
ensureMediaControls();
}

void HTMLMediaElement::captionPreferencesChanged()
@@ -7640,45 +7619,6 @@ DOMWrapperWorld& HTMLMediaElement::ensureIsolatedWorld()
return *m_isolatedWorld;
}

bool HTMLMediaElement::ensureMediaControlsInjectedScript()
{
INFO_LOG(LOGIDENTIFIER);

Page* page = document().page();
if (!page)
return false;

auto mediaControlsScripts = RenderTheme::singleton().mediaControlsScripts();
if (mediaControlsScripts.isEmpty())
return false;

return setupAndCallJS([mediaControlsScripts = WTFMove(mediaControlsScripts)](JSDOMGlobalObject& globalObject, JSC::JSGlobalObject& lexicalGlobalObject, ScriptController& scriptController, DOMWrapperWorld& world) {
auto& vm = globalObject.vm();
auto scope = DECLARE_CATCH_SCOPE(vm);

auto reportExceptionAndReturnFalse = [&] () -> bool {
auto* exception = scope.exception();
scope.clearException();
reportException(&globalObject, exception);
return false;
};

auto functionValue = globalObject.get(&lexicalGlobalObject, JSC::Identifier::fromString(vm, "createControls"_s));
RETURN_IF_EXCEPTION(scope, reportExceptionAndReturnFalse());
if (functionValue.isCallable())
return true;

for (auto& mediaControlsScript : mediaControlsScripts) {
if (mediaControlsScript.isEmpty())
continue;
scriptController.evaluateInWorldIgnoringException(ScriptSourceCode(mediaControlsScript), world);
RETURN_IF_EXCEPTION(scope, reportExceptionAndReturnFalse());
}

return true;
});
}

void HTMLMediaElement::updatePageScaleFactorJSProperty()
{
Page* page = document().page();
@@ -7721,17 +7661,37 @@ void HTMLMediaElement::setControllerJSProperty(ASCIILiteral propertyName, JSC::J
});
}

void HTMLMediaElement::didAddUserAgentShadowRoot(ShadowRoot& root)
bool HTMLMediaElement::ensureMediaControls()
{
if (m_controlsState == ControlsState::Ready)
return true;

auto mediaControlsScripts = RenderTheme::singleton().mediaControlsScripts();
if (mediaControlsScripts.isEmpty())
return false;

INFO_LOG(LOGIDENTIFIER);

if (!ensureMediaControlsInjectedScript())
return;
m_controlsState = ControlsState::Initializing;

setupAndCallJS([this, &root](JSDOMGlobalObject& globalObject, JSC::JSGlobalObject& lexicalGlobalObject, ScriptController&, DOMWrapperWorld&) {
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 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());
}

// The media controls script must provide a method with the following details.
// Name: createControls
// Parameters:
@@ -7752,18 +7712,11 @@ void HTMLMediaElement::didAddUserAgentShadowRoot(ShadowRoot& root)
auto mediaControlsHostJSWrapper = toJS(&lexicalGlobalObject, &globalObject, *m_mediaControlsHost);

JSC::MarkedArgumentBuffer argList;
argList.append(toJS(&lexicalGlobalObject, &globalObject, root));
argList.append(toJS(&lexicalGlobalObject, &globalObject, ensureUserAgentShadowRoot()));
argList.append(mediaJSWrapper);
argList.append(mediaControlsHostJSWrapper);
ASSERT(!argList.hasOverflowed());

auto reportExceptionAndReturnFalse = [&] () -> bool {
auto* exception = scope.exception();
scope.clearException();
reportException(&globalObject, exception);
return false;
};

auto* function = functionValue.toObject(&lexicalGlobalObject);
RETURN_IF_EXCEPTION(scope, reportExceptionAndReturnFalse());
auto callData = JSC::getCallData(function);
@@ -7805,6 +7758,8 @@ void HTMLMediaElement::didAddUserAgentShadowRoot(ShadowRoot& root)

return true;
});
m_controlsState = controlsReady ? ControlsState::Ready : ControlsState::None;
return controlsReady;
}

void HTMLMediaElement::setMediaControlsDependOnPageScaleFactor(bool dependsOnPageScale)
@@ -7869,7 +7824,7 @@ void HTMLMediaElement::userInterfaceLayoutDirectionChanged()

String HTMLMediaElement::getCurrentMediaControlsStatus()
{
ensureMediaControlsShadowRoot();
ensureMediaControls();

String status;
setupAndCallJS([this, &status](JSDOMGlobalObject& globalObject, JSC::JSGlobalObject& lexicalGlobalObject, ScriptController&, DOMWrapperWorld&) {
@@ -903,9 +903,7 @@ class HTMLMediaElement
enum class SleepType { None, Display, System };
SleepType shouldDisableSleep() const;

void didAddUserAgentShadowRoot(ShadowRoot&) override;
DOMWrapperWorld& ensureIsolatedWorld();
bool ensureMediaControlsInjectedScript();

PlatformMediaSession::MediaType mediaType() const override;
PlatformMediaSession::MediaType presentationType() const override;
@@ -939,7 +937,7 @@ class HTMLMediaElement
void initializeMediaSession();

void updateCaptionContainer();
void ensureMediaControlsShadowRoot();
bool ensureMediaControls();

using JSSetupFunction = Function<bool(JSDOMGlobalObject&, JSC::JSGlobalObject&, ScriptController&, DOMWrapperWorld&)>;
bool setupAndCallJS(const JSSetupFunction&);
@@ -1131,7 +1129,6 @@ class HTMLMediaElement
bool m_parsingInProgress : 1;
bool m_elementIsHidden : 1;
bool m_elementWasRemovedFromDOM : 1;
bool m_creatingControls : 1;
bool m_receivedLayoutSizeChanged : 1;
bool m_hasEverNotifiedAboutPlaying : 1;

@@ -1153,6 +1150,9 @@ class HTMLMediaElement
bool m_shouldVideoPlaybackRequireUserGesture : 1;
bool m_volumeLocked : 1;

enum class ControlsState : uint8_t { None, Initializing, Ready };
ControlsState m_controlsState { ControlsState::None };

AutoplayEventPlaybackState m_autoplayEventPlaybackState { AutoplayEventPlaybackState::None };

String m_subtitleTrackLanguage;

0 comments on commit f331cc9

Please sign in to comment.