Bug 1327097 - Part III, Trap mouse/touch/pointer events in audio/video element, r=smaug
authorTimothy Guan-tin Chien <timdream@gmail.com>
Fri, 17 Nov 2017 11:07:12 +0800
changeset 436999 194be07f3d4b8a5988e730eb984eaf5c87685782
parent 436998 45520c6ace4e91772d80e431f3f564133b40fb14
child 437000 168bbdf67be523875bdc90070208eb605f1e7df2
push id117
push userfmarier@mozilla.com
push dateTue, 28 Nov 2017 20:17:16 +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;
@@ -4387,16 +4389,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");