Bug 1431255 - Part II, Create a Shadow Root in HTMLMediaElement when enabled, skipping <xul:videocontrols> draft
authorTimothy Guan-tin Chien <timdream@gmail.com>
Wed, 27 Jun 2018 11:12:38 -0700
changeset 829407 e5c11bffeab4cafaa02743a669e9db5da69ce3ab
parent 829406 284ddded69d504bf070ce1b41891043550ede501
child 829408 30f77c9ed6ec3a88cd95602f8b7f31210a42b685
push id118779
push usertimdream@gmail.com
push dateWed, 15 Aug 2018 23:50:47 +0000
bugs1431255
milestone63.0a1
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
dom/html/HTMLMediaElement.cpp
dom/html/HTMLMediaElement.h
dom/html/TextTrackManager.cpp
dom/media/webvtt/vtt.jsm
layout/generic/nsFrameIdList.h
layout/generic/nsVideoFrame.cpp
layout/generic/nsVideoFrame.h
--- 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,