Bug 1431255 - Part II, Create a Shadow Root in HTMLMediaElement when enabled, skipping <xul:videocontrols>
This prevents XBL binding from being attached, and create the Shadow Root to
host controls to be created by the script.
Shadow Root and the JS controls are lazily constructed when the controls
attribute is set.
Set nsVideoFrame as dynamic-leaf so it will ignore content child frames when
the controls are XBL anonymous content, and handles child frames from controls
in the Shadow DOM. The content nodes are still ignored since there is no
<slot>s in our Shadow DOM.
MozReview-Commit-ID: 3hk41iMa07n
--- a/dom/html/HTMLMediaElement.cpp
+++ b/dom/html/HTMLMediaElement.cpp
@@ -4504,16 +4504,27 @@ HTMLMediaElement::AfterSetAttr(int32_t a
UpdatePreloadAction();
}
} else if (aName == nsGkAtoms::preload) {
UpdatePreloadAction();
} else if (aName == nsGkAtoms::loop) {
if (mDecoder) {
mDecoder->SetLooping(!!aValue);
}
+ } else if (nsContentUtils::IsUAWidgetEnabled() &&
+ aName == nsGkAtoms::controls &&
+ IsInComposedDoc()) {
+ AsyncEventDispatcher* dispatcher =
+ new AsyncEventDispatcher(this,
+ NS_LITERAL_STRING("UAWidgetAttributeChanged"),
+ CanBubble::eYes,
+ ChromeOnlyDispatch::eYes);
+ // This has to happen at this tick so that UA Widget could respond
+ // before returning to content script.
+ dispatcher->RunDOMEventWhenSafe();
}
}
// Since AfterMaybeChangeAttr may call DoLoad, make sure that it is called
// *after* any possible changes to mSrcMediaSource.
if (aValue) {
AfterMaybeChangeAttr(aNameSpaceID, aName, aNotify);
}
@@ -4549,16 +4560,44 @@ HTMLMediaElement::AfterMaybeChangeAttr(i
nsresult
HTMLMediaElement::BindToTree(nsIDocument* aDocument,
nsIContent* aParent,
nsIContent* aBindingParent)
{
nsresult rv = nsGenericHTMLElement::BindToTree(
aDocument, aParent, aBindingParent);
+ if (nsContentUtils::IsUAWidgetEnabled() && IsInComposedDoc()) {
+ // Construct Shadow Root so web content can be hidden in the DOM.
+ AttachAndSetUAShadowRoot();
+#ifdef ANDROID
+ AsyncEventDispatcher* dispatcher =
+ new AsyncEventDispatcher(this,
+ NS_LITERAL_STRING("UAWidgetBindToTree"),
+ CanBubble::eYes,
+ ChromeOnlyDispatch::eYes);
+ dispatcher->RunDOMEventWhenSafe();
+#else
+ // We don't want to call into JS if the website never asks for native
+ // video controls.
+ // If controls attribute is set later, controls is constructed lazily
+ // with the UAWidgetAttributeChanged event.
+ // This only applies to Desktop because on Fennec we would need to show
+ // an UI if the video is blocked.
+ if (Controls()) {
+ AsyncEventDispatcher* dispatcher =
+ new AsyncEventDispatcher(this,
+ NS_LITERAL_STRING("UAWidgetBindToTree"),
+ CanBubble::eYes,
+ ChromeOnlyDispatch::eYes);
+ dispatcher->RunDOMEventWhenSafe();
+ }
+#endif
+ }
+
mUnboundFromTree = false;
if (aDocument) {
// The preload action depends on the value of the autoplay attribute.
// It's value may have changed, so update it.
UpdatePreloadAction();
}
@@ -4798,16 +4837,23 @@ HTMLMediaElement::UnbindFromTree(bool aD
mUnboundFromTree = true;
mVisibilityState = Visibility::UNTRACKED;
nsGenericHTMLElement::UnbindFromTree(aDeep, aNullParent);
MOZ_ASSERT(IsHidden());
NotifyDecoderActivityChanges();
+ AsyncEventDispatcher* dispatcher =
+ new AsyncEventDispatcher(this,
+ NS_LITERAL_STRING("UAWidgetUnbindFromTree"),
+ CanBubble::eYes,
+ ChromeOnlyDispatch::eYes);
+ dispatcher->RunDOMEventWhenSafe();
+
RefPtr<HTMLMediaElement> self(this);
nsCOMPtr<nsIRunnable> task =
NS_NewRunnableFunction("dom::HTMLMediaElement::UnbindFromTree", [self]() {
if (self->mUnboundFromTree) {
self->Pause();
}
});
RunInStableState(task);
@@ -4886,16 +4932,27 @@ HTMLMediaElement::AssertReadyStateIsNoth
int(mPreloadAction),
mSuspendedForPreloadNone,
GetError() ? GetError()->Code() : 0);
MOZ_CRASH_UNSAFE_PRINTF("ReadyState should be HAVE_NOTHING! %s", buf);
}
#endif
}
+void
+HTMLMediaElement::AttachAndSetUAShadowRoot()
+{
+ if (GetShadowRoot()) {
+ return;
+ }
+
+ // Add a closed shadow root to host video controls
+ AttachShadowWithoutNameChecks(ShadowRootMode::Closed);
+}
+
nsresult
HTMLMediaElement::InitializeDecoderAsClone(ChannelMediaDecoder* aOriginal)
{
NS_ASSERTION(mLoadingSrc, "mLoadingSrc must already be set");
NS_ASSERTION(mDecoder == nullptr, "Shouldn't have a decoder");
AssertReadyStateIsNothing();
MediaDecoderInit decoderInit(this,
--- a/dom/html/HTMLMediaElement.h
+++ b/dom/html/HTMLMediaElement.h
@@ -1853,16 +1853,19 @@ private:
// A pending seek promise which is created at Seek() method call and is
// resolved/rejected at AsyncResolveSeekDOMPromiseIfExists()/
// AsyncRejectSeekDOMPromiseIfExists() methods.
RefPtr<dom::Promise> mSeekDOMPromise;
// For debugging bug 1407148.
void AssertReadyStateIsNothing();
+
+ // Attach UA Shadow Root if it is not attached.
+ void AttachAndSetUAShadowRoot();
};
// Check if the context is chrome or has the debugger or tabs permission
bool
HasDebuggerOrTabsPrivilege(JSContext* aCx, JSObject* aObj);
} // namespace dom
} // namespace mozilla
--- a/dom/html/TextTrackManager.cpp
+++ b/dom/html/TextTrackManager.cpp
@@ -271,17 +271,17 @@ TextTrackManager::UpdateCueDisplay()
nsIFrame* frame = mMediaElement->GetPrimaryFrame();
nsVideoFrame* videoFrame = do_QueryFrame(frame);
if (!videoFrame) {
return;
}
nsCOMPtr<nsIContent> overlay = videoFrame->GetCaptionOverlay();
nsCOMPtr<nsIContent> controls = videoFrame->GetVideoControls();
- if (!overlay) {
+ if (!overlay || !controls) {
return;
}
nsTArray<RefPtr<TextTrackCue> > showingCues;
mTextTracks->GetShowingCues(showingCues);
if (showingCues.Length() > 0) {
WEBVTT_LOG("UpdateCueDisplay ProcessCues");
--- a/dom/media/webvtt/vtt.jsm
+++ b/dom/media/webvtt/vtt.jsm
@@ -1007,18 +1007,24 @@ ChromeUtils.import("resource://gre/modul
WebVTT.processCues = function(window, cues, overlay, controls) {
if (!window || !cues || !overlay) {
return null;
}
var controlBar;
var controlBarShown;
if (controls) {
- controlBar = controls.ownerDocument.getAnonymousElementByAttribute(
- controls, "anonid", "controlBar");
+ if (controls.localName == "videocontrols") {
+ // controls is a NAC; The control bar is in a XBL binding.
+ controlBar = controls.ownerDocument.getAnonymousElementByAttribute(
+ controls, "anonid", "controlBar");
+ } else {
+ // controls is a <div> that is the children of the UA Widget Shadow Root.
+ controlBar = controls.parentNode.getElementById("controlBar");
+ }
controlBarShown = controlBar ? !!controlBar.clientHeight : false;
}
// Determine if we need to compute the display states of the cues. This could
// be the case if a cue's state has been changed since the last computation or
// if it has not been computed yet.
function shouldCompute(cues) {
if (overlay.lastControlBarShownStatus != controlBarShown) {
@@ -1376,17 +1382,17 @@ ChromeUtils.import("resource://gre/modul
// parseHeader returns false if the same line doesn't need to be
// parsed again.
if (!parseHeader(line)) {
return;
}
}
if (self.state === "ID") {
- // If there is no cue identifier, read the next line.
+ // If there is no cue identifier, read the next line.
if (line == "") {
return;
}
// If there is no cue identifier, parse the line again.
if (!parseCueIdentifier(line)) {
return self.parseLine(line);
}
--- a/layout/generic/nsFrameIdList.h
+++ b/layout/generic/nsFrameIdList.h
@@ -138,17 +138,17 @@ FRAME_ID(nsTableWrapperFrame, TableWrapp
FRAME_ID(nsTableRowFrame, TableRow, NotLeaf)
FRAME_ID(nsTableRowGroupFrame, TableRowGroup, NotLeaf)
FRAME_ID(nsTextBoxFrame, LeafBox, Leaf)
FRAME_ID(nsTextControlFrame, TextInput, Leaf)
FRAME_ID(nsTextFrame, Text, Leaf)
FRAME_ID(nsTitleBarFrame, Box, NotLeaf)
FRAME_ID(nsTreeBodyFrame, LeafBox, Leaf)
FRAME_ID(nsTreeColFrame, Box, NotLeaf)
-FRAME_ID(nsVideoFrame, HTMLVideo, Leaf)
+FRAME_ID(nsVideoFrame, HTMLVideo, DynamicLeaf)
FRAME_ID(nsXULLabelFrame, XULLabel, NotLeaf)
FRAME_ID(nsXULScrollFrame, Scroll, NotLeaf)
FRAME_ID(ViewportFrame, Viewport, NotLeaf)
// The following ABSTRACT_FRAME_IDs needs to come after the above
// FRAME_IDs, because we have two separate enums, one that includes
// only FRAME_IDs and another which includes both and we depend on
// FRAME_IDs to have the same number in both.
--- a/layout/generic/nsVideoFrame.cpp
+++ b/layout/generic/nsVideoFrame.cpp
@@ -141,19 +141,21 @@ nsVideoFrame::CreateAnonymousContent(nsT
// Set up "videocontrols" XUL element which will be XBL-bound to the
// actual controls.
nodeInfo = nodeInfoManager->GetNodeInfo(nsGkAtoms::videocontrols,
nullptr,
kNameSpaceID_XUL,
nsINode::ELEMENT_NODE);
NS_ENSURE_TRUE(nodeInfo, NS_ERROR_OUT_OF_MEMORY);
- NS_TrustedNewXULElement(getter_AddRefs(mVideoControls), nodeInfo.forget());
- if (!aElements.AppendElement(mVideoControls))
- return NS_ERROR_OUT_OF_MEMORY;
+ if (!nsContentUtils::IsUAWidgetEnabled()) {
+ NS_TrustedNewXULElement(getter_AddRefs(mVideoControls), nodeInfo.forget());
+ if (!aElements.AppendElement(mVideoControls))
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
return NS_OK;
}
void
nsVideoFrame::AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements,
uint32_t aFliter)
{
@@ -165,16 +167,29 @@ nsVideoFrame::AppendAnonymousContentTo(n
aElements.AppendElement(mVideoControls);
}
if (mCaptionDiv) {
aElements.AppendElement(mCaptionDiv);
}
}
+nsIContent*
+nsVideoFrame::GetVideoControls()
+{
+ if (mVideoControls) {
+ return mVideoControls;
+ }
+ if (mContent->GetShadowRoot()) {
+ // The video controls <div> is the only child of the UA Widget Shadow Root.
+ return mContent->GetShadowRoot()->GetFirstChild();
+ }
+ return nullptr;
+}
+
void
nsVideoFrame::DestroyFrom(nsIFrame* aDestructRoot, PostDestroyData& aPostDestroyData)
{
aPostDestroyData.AddAnonymousContent(mCaptionDiv.forget());
aPostDestroyData.AddAnonymousContent(mVideoControls.forget());
aPostDestroyData.AddAnonymousContent(mPosterImage.forget());
nsContainerFrame::DestroyFrom(aDestructRoot, aPostDestroyData);
}
@@ -301,16 +316,18 @@ nsVideoFrame::Reflow(nsPresContext* aPre
nscoord borderBoxBSize;
if (!isBSizeShrinkWrapping) {
borderBoxBSize = contentBoxBSize +
aReflowInput.ComputedLogicalBorderPadding().BStartEnd(myWM);
}
nsMargin borderPadding = aReflowInput.ComputedPhysicalBorderPadding();
+ nsIContent* videoControlsDiv = GetVideoControls();
+
// Reflow the child frames. We may have up to three: an image
// frame (for the poster image), a container frame for the controls,
// and a container frame for the caption.
for (nsIFrame* child : mFrames) {
nsSize oldChildSize = child->GetSize();
nsReflowStatus childStatus;
if (child->GetContent() == mPosterImage) {
@@ -344,47 +361,50 @@ nsVideoFrame::Reflow(nsPresContext* aPre
"We gave our child unconstrained available block-size, "
"so it should be complete!");
FinishReflowChild(imageFrame, aPresContext,
kidDesiredSize, &kidReflowInput,
posterRenderRect.x, posterRenderRect.y, 0);
} else if (child->GetContent() == mCaptionDiv ||
- child->GetContent() == mVideoControls) {
+ child->GetContent() == videoControlsDiv) {
// Reflow the caption and control bar frames.
WritingMode wm = child->GetWritingMode();
LogicalSize availableSize = aReflowInput.ComputedSize(wm);
availableSize.BSize(wm) = NS_UNCONSTRAINEDSIZE;
ReflowInput kidReflowInput(aPresContext,
aReflowInput,
child,
availableSize);
ReflowOutput kidDesiredSize(kidReflowInput);
ReflowChild(child, aPresContext, kidDesiredSize, kidReflowInput,
borderPadding.left, borderPadding.top, 0, childStatus);
MOZ_ASSERT(childStatus.IsFullyComplete(),
"We gave our child unconstrained available block-size, "
"so it should be complete!");
- if (child->GetContent() == mVideoControls && isBSizeShrinkWrapping) {
+ if (child->GetContent() == videoControlsDiv && isBSizeShrinkWrapping) {
// Resolve our own BSize based on the controls' size in the same axis.
contentBoxBSize = myWM.IsOrthogonalTo(wm) ?
kidDesiredSize.ISize(wm) : kidDesiredSize.BSize(wm);
}
FinishReflowChild(child, aPresContext,
kidDesiredSize, &kidReflowInput,
borderPadding.left, borderPadding.top, 0);
- }
- if (child->GetContent() == mVideoControls && child->GetSize() != oldChildSize) {
- RefPtr<Runnable> event = new DispatchResizeToControls(child->GetContent());
- nsContentUtils::AddScriptRunner(event);
+ if (child->GetContent() == videoControlsDiv && child->GetSize() != oldChildSize) {
+ RefPtr<Runnable> event = new DispatchResizeToControls(child->GetContent());
+ nsContentUtils::AddScriptRunner(event);
+ }
+ } else {
+ MOZ_ASSERT_UNREACHABLE("Extra child frame found in nsVideoFrame. "
+ "Possibly from stray whitespace around the videocontrols container element.");
}
}
if (isBSizeShrinkWrapping) {
if (contentBoxBSize == NS_INTRINSICSIZE) {
// We didn't get a BSize from our intrinsic size/ratio, nor did we
// get one from our controls. Just use BSize of 0.
contentBoxBSize = 0;
@@ -406,16 +426,33 @@ nsVideoFrame::Reflow(nsPresContext* aPre
NS_FRAME_TRACE(NS_FRAME_TRACE_CALLS,
("exit nsVideoFrame::Reflow: size=%d,%d",
aMetrics.Width(), aMetrics.Height()));
MOZ_ASSERT(aStatus.IsEmpty(), "This type of frame can't be split.");
NS_FRAME_SET_TRUNCATION(aStatus, aReflowInput, aMetrics);
}
+/**
+ * nsVideoFrame should be a non-leaf frame when UA Widget is enabled,
+ * so the videocontrols container element inserted under the Shadow Root can be
+ * picked up. No frames will be generated from elements from the web content,
+ * given that they have been replaced by the Shadow Root without and <slots>
+ * element in the DOM tree.
+ *
+ * When the UA Widget is disabled, i.e. the videocontrols is bound as anonymous
+ * content with XBL, nsVideoFrame has to be a leaf so no frames from web content
+ * element will be generated.
+ */
+bool
+nsVideoFrame::IsLeafDynamic() const
+{
+ return !nsContentUtils::IsUAWidgetEnabled();
+}
+
class nsDisplayVideo : public nsDisplayItem {
public:
nsDisplayVideo(nsDisplayListBuilder* aBuilder, nsVideoFrame* aFrame)
: nsDisplayItem(aBuilder, aFrame)
{
MOZ_COUNT_CTOR(nsDisplayVideo);
}
#ifdef NS_BUILD_REFCNT_LOGGING
--- a/layout/generic/nsVideoFrame.h
+++ b/layout/generic/nsVideoFrame.h
@@ -69,16 +69,18 @@ public:
nscoord GetPrefISize(gfxContext *aRenderingContext) override;
void DestroyFrom(nsIFrame* aDestructRoot, PostDestroyData& aPostDestroyData) override;
void Reflow(nsPresContext* aPresContext,
ReflowOutput& aDesiredSize,
const ReflowInput& aReflowInput,
nsReflowStatus& aStatus) override;
+ bool IsLeafDynamic() const override;
+
#ifdef ACCESSIBILITY
mozilla::a11y::AccType AccessibleType() override;
#endif
bool IsFrameOfType(uint32_t aFlags) const override
{
return nsSplittableFrame::IsFrameOfType(aFlags &
~(nsIFrame::eReplaced | nsIFrame::eReplacedSizing));
@@ -90,18 +92,17 @@ public:
mozilla::dom::Element* GetPosterImage() { return mPosterImage; }
// Returns true if we should display the poster. Note that once we show
// a video frame, the poster will never be displayed again.
bool ShouldDisplayPoster();
nsIContent *GetCaptionOverlay() { return mCaptionDiv; }
-
- nsIContent *GetVideoControls() { return mVideoControls; }
+ nsIContent *GetVideoControls();
#ifdef DEBUG_FRAME_DUMP
nsresult GetFrameName(nsAString& aResult) const override;
#endif
already_AddRefed<Layer> BuildLayer(nsDisplayListBuilder* aBuilder,
LayerManager* aManager,
nsDisplayItem* aItem,