Bug 1327097 - Part III, Trap mouse/touch/pointer events in audio/video element, r=smaug draft
authorTimothy Guan-tin Chien <timdream@gmail.com>
Fri, 17 Nov 2017 11:07:12 +0800
changeset 699365 181664b6cef96df7d7a853b81ba27604d4487b4c
parent 698816 3606881b8f8d0a65ab8f82ec1b01f78843648af2
child 740608 1ce4036e44f3bf94dc2a0fee9bb542cfadbe1f1c
push id89546
push usertimdream@gmail.com
push dateFri, 17 Nov 2017 03:11:05 +0000
reviewerssmaug
bugs1327097
milestone59.0a1
Bug 1327097 - Part III, Trap mouse/touch/pointer events in audio/video element, r=smaug This patch implements HTMLMediaElement::GetEventTargetParent and set aVisitor.mCanHandle to false to mouse/touch/pointer events, when the media control is present. This tells the event dispatcher that these events are supposed to be handled exclusively by the videocontrol binding within the media element, and should not dispatch nor consumed by the content. MozReview-Commit-ID: BXWZX9SYsuC
dom/html/HTMLInputElement.h
dom/html/HTMLMediaElement.cpp
dom/html/HTMLMediaElement.h
gfx/layers/apz/test/mochitest/helper_bug1162771.html
toolkit/content/tests/widgets/test_videocontrols.html
--- a/dom/html/HTMLInputElement.h
+++ b/dom/html/HTMLInputElement.h
@@ -573,16 +573,22 @@ public:
   {
     SetUnsignedIntAttr(nsGkAtoms::height, aValue, 0, aRv);
   }
 
   bool Indeterminate() const
   {
     return mIndeterminate;
   }
+
+  bool IsDraggingRange() const
+  {
+    return mIsDraggingRange;
+  }
+
   // XPCOM SetIndeterminate() is OK
 
   void GetInputMode(nsAString& aValue);
   void SetInputMode(const nsAString& aValue, ErrorResult& aRv)
   {
     SetHTMLAttr(nsGkAtoms::inputmode, aValue, aRv);
   }
 
@@ -1530,17 +1536,17 @@ protected:
   bool IsPopupBlocked() const;
 
   GetFilesHelper* GetOrCreateGetFilesHelper(bool aRecursiveFlag,
                                             ErrorResult& aRv);
 
   void ClearGetFilesHelpers();
 
   /**
-   * nsINode::SetMayBeApzAware() will be invoked in this function if necessary 
+   * nsINode::SetMayBeApzAware() will be invoked in this function if necessary
    * to prevent default action of APZC so that we can increase/decrease the
    * value of this InputElement when mouse wheel event comes without scrolling
    * the page.
    *
    * SetMayBeApzAware() will set flag MayBeApzAware which is checked by apzc to
    * decide whether to add this element into its dispatch-to-content region.
    */
   void UpdateApzAwareFlag();
--- a/dom/html/HTMLMediaElement.cpp
+++ b/dom/html/HTMLMediaElement.cpp
@@ -9,16 +9,17 @@
 #include "mozilla/dom/HTMLSourceElement.h"
 #include "mozilla/dom/ElementInlines.h"
 #include "mozilla/dom/Promise.h"
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/MathAlgorithms.h"
 #include "mozilla/AsyncEventDispatcher.h"
 #include "mozilla/dom/MediaEncryptedEvent.h"
 #include "mozilla/EMEUtils.h"
+#include "mozilla/EventDispatcher.h"
 #include "mozilla/Sprintf.h"
 
 #include "base/basictypes.h"
 #include "nsIDOMHTMLMediaElement.h"
 #include "TimeRanges.h"
 #include "nsGenericHTMLElement.h"
 #include "nsAttrValueInlines.h"
 #include "nsDocShellLoadTypes.h"
@@ -130,16 +131,17 @@ static mozilla::LazyLogModule gMediaElem
 #include "mozilla/Preferences.h"
 #include "mozilla/FloatingPoint.h"
 
 #include "nsIPermissionManager.h"
 #include "nsDocShell.h"
 
 #include "mozilla/EventStateManager.h"
 
+#include "mozilla/dom/HTMLInputElement.h"
 #include "mozilla/dom/HTMLVideoElement.h"
 #include "mozilla/dom/VideoPlaybackQuality.h"
 #include "HTMLMediaElement.h"
 
 #include "GMPCrashHelper.h"
 
 using namespace mozilla::layers;
 using mozilla::net::nsMediaFragmentURIParser;
@@ -4378,16 +4380,70 @@ HTMLMediaElement::OutputMediaStream::Out
 
 HTMLMediaElement::OutputMediaStream::~OutputMediaStream()
 {
   for (auto pair : mTrackPorts) {
     pair.second()->Destroy();
   }
 }
 
+nsresult
+HTMLMediaElement::GetEventTargetParent(EventChainPreVisitor& aVisitor)
+{
+  if (!this->Controls() || !aVisitor.mEvent->mFlags.mIsTrusted) {
+    return nsGenericHTMLElement::GetEventTargetParent(aVisitor);
+  }
+
+  HTMLInputElement* el = nullptr;
+  nsCOMPtr<nsINode> node;
+
+  // We will need to trap pointer, touch, and mouse events within the media
+  // element, allowing media control exclusive consumption on these events,
+  // and preventing the content from handling them.
+  switch (aVisitor.mEvent->mMessage) {
+    case ePointerDown:
+    case ePointerUp:
+    case eTouchEnd:
+    // Always prevent touchmove captured in video element from being handled by content,
+    // since we always do that for touchstart.
+    case eTouchMove:
+    case eTouchStart:
+    case eMouseClick:
+    case eMouseDoubleClick:
+    case eMouseDown:
+    case eMouseUp:
+      aVisitor.mCanHandle = false;
+      return NS_OK;
+
+    // The *move events however are only comsumed when the range input is being
+    // dragged.
+    case ePointerMove:
+    case eMouseMove:
+      node = do_QueryInterface(aVisitor.mEvent->mOriginalTarget);
+      if (node->IsInNativeAnonymousSubtree()) {
+        if (node->IsHTMLElement(nsGkAtoms::input)) {
+          // The node is a <input type="range">
+          el = static_cast<HTMLInputElement*>(node.get());
+        } else if (node->GetParentNode() &&
+                   node->GetParentNode()->IsHTMLElement(nsGkAtoms::input)) {
+          // The node is a child of <input type="range">
+          el = static_cast<HTMLInputElement*>(node->GetParentNode());
+        }
+      }
+      if (el && el->IsDraggingRange()) {
+        aVisitor.mCanHandle = false;
+        return NS_OK;
+      }
+      return nsGenericHTMLElement::GetEventTargetParent(aVisitor);
+
+    default:
+      return nsGenericHTMLElement::GetEventTargetParent(aVisitor);
+  }
+}
+
 bool HTMLMediaElement::ParseAttribute(int32_t aNamespaceID,
                                       nsAtom* aAttribute,
                                       const nsAString& aValue,
                                       nsAttrValue& aResult)
 {
   // Mappings from 'preload' attribute strings to an enumeration.
   static const nsAttrValue::EnumTable kPreloadTable[] = {
     { "",         HTMLMediaElement::PRELOAD_ATTR_EMPTY },
--- a/dom/html/HTMLMediaElement.h
+++ b/dom/html/HTMLMediaElement.h
@@ -140,16 +140,20 @@ public:
   // nsIDOMHTMLMediaElement
   NS_DECL_NSIDOMHTMLMEDIAELEMENT
 
   // nsISupports
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLMediaElement,
                                            nsGenericHTMLElement)
 
+  // nsIDOMEventTarget
+  virtual nsresult
+  GetEventTargetParent(EventChainPreVisitor& aVisitor) override;
+
   virtual bool ParseAttribute(int32_t aNamespaceID,
                               nsAtom* aAttribute,
                               const nsAString& aValue,
                               nsAttrValue& aResult) override;
 
   virtual nsresult BindToTree(nsIDocument* aDocument, nsIContent* aParent,
                               nsIContent* aBindingParent,
                               bool aCompileEventHandlers) override;
--- a/gfx/layers/apz/test/mochitest/helper_bug1162771.html
+++ b/gfx/layers/apz/test/mochitest/helper_bug1162771.html
@@ -93,12 +93,12 @@ waitUntilApzStable()
       border:solid black 1px;
       background-color: #88a;
     }
   </style>
 </head>
 <body>
  <p>Tap on the colored boxes to hide them.</p>
  <video id="video"></video>
- <audio id="audio" controls></audio>
+ <audio id="audio"></audio>
  <div id="div"></div>
 </body>
 </html>
--- a/toolkit/content/tests/widgets/test_videocontrols.html
+++ b/toolkit/content/tests/widgets/test_videocontrols.html
@@ -114,16 +114,26 @@ add_task(async function setup() {
 
   video.addEventListener("play", verifyExpectedEvent);
   video.addEventListener("pause", verifyExpectedEvent);
   video.addEventListener("volumechange", verifyExpectedEvent);
   video.addEventListener("seeking", verifyExpectedEvent);
   video.addEventListener("seeked", verifyExpectedEvent);
   document.addEventListener("mozfullscreenchange", verifyExpectedEvent);
 
+  ["mousedown", "mouseup", "dblclick", "click"]
+    .forEach((eventType) => {
+      window.addEventListener(eventType, (evt) => {
+        // Prevent default action of leaked events and make the tests fail.
+        evt.preventDefault();
+        ok(false, "Event " + eventType + " in videocontrol should not leak to content;" +
+          "the event was dispatched from the " + evt.target.tagName.toLowerCase() + " element.");
+      });
+    });
+
   // Check initial state upon load
   is(video.paused, true, "checking video play state");
   is(video.muted, false, "checking video mute state");
 });
 
 add_task(async function click_playbutton() {
   synthesizeMouse(video, playButtonCenterX, playButtonCenterY, {});
   await waitForEvent("play");
@@ -207,25 +217,34 @@ add_task(async function reset_currentTim
 /*
  * Drag the slider's thumb to the halfway point with the mouse.
  */
 add_task(async function drag_slider() {
   const beginDragX = scrubberOffsetX;
   const endDragX = scrubberOffsetX + (scrubberWidth / 2);
   const expectedTime = videoDuration / 2;
 
+  function mousemoved(evt) {
+    ok(false, "Mousemove event should not be handled by content while dragging; " +
+      "the event was dispatched from the " + evt.target.tagName.toLowerCase() + " element.");
+  }
+
+  window.addEventListener("mousemove", mousemoved);
+
   synthesizeMouse(video, beginDragX, scrubberCenterY, {type: "mousedown", button: 0});
   synthesizeMouse(video, endDragX, scrubberCenterY, {type: "mousemove", button: 0});
   synthesizeMouse(video, endDragX, scrubberCenterY, {type: "mouseup",   button: 0});
   await waitForEvent("seeking", "seeked");
   ok(true, "video position is at " + video.currentTime);
   // The width of srubber is not equal in every platform as we use system default font
   // in duration and position box. We can not precisely drag to expected position, so
   // we just make sure the difference is within 10% of video duration.
   ok(Math.abs(video.currentTime - expectedTime) < videoDuration / 10, "checking expected playback position");
+
+  window.removeEventListener("mousemove", mousemoved);
 });
 
 /*
  * Click the slider at the 1/4 point with the mouse (jump backwards)
  */
 add_task(async function click_slider() {
   synthesizeMouse(video, scrubberOffsetX + (scrubberWidth / 4), scrubberCenterY, {});
   await waitForEvent("seeking", "seeked");