Commit f2c18626 authored by Becca Hughes's avatar Becca Hughes Committed by Commit Bot

Reland: Media Controls: Double tap on either side to jump

Add double tap gesture recognition to MediaControlOverlayPlayButton so
it can detect double tap gestures on the side and jump accordingly. Adds
LayoutTests as a virtual test suite so they are tested with the modern
media controls flag on.

BUG=779989

TBR=jbroman@chromium.org

Change-Id: I2d6e3c79d5ca3b447eda70f35da3dd22283a14b3
Reviewed-on: https://chromium-review.googlesource.com/800150
Commit-Queue: Becca Hughes <beccahughes@chromium.org>
Reviewed-by: default avatarMounir Lamouri <mlamouri@chromium.org>
Cr-Commit-Position: refs/heads/master@{#522139}
parent 101dbbef
......@@ -1835,3 +1835,7 @@ http/tests/ManualTests/ [ WontFix ]
# in testing, whereas otherwise we do not do so if the composited layer has a
# direct compositing reason (for performance). Only applies to SPv1.
paint/invalidation/compositing/subpixel-offset-scaled-transform-composited.html [ WontFix ]
# These tests should always fail because the modern media controls are not yet enabled
crbug.com/761305 media/controls/modern/ [ WontFix ]
crbug.com/761306 virtual/new-remote-playback-pipeline/media/controls/modern/ [ WontFix ]
......@@ -3626,3 +3626,14 @@ crbug.com/788390 [ Linux ] http/tests/media/autoplay/document-user-activation-fe
crbug.com/788390 [ Linux ] http/tests/media/autoplay/document-user-activation-feature-policy-iframe-no-gesture.html [ Failure Pass Timeout ]
crbug.com/788390 [ Linux ] http/tests/media/autoplay/document-user-activation-feature-policy-same-origin.html [ Failure Pass Timeout ]
crbug.com/788390 [ Linux ] http/tests/media/autoplay/document-user-activation-iframe-delegation.html [ Failure Pass Timeout ]
# Double tap on modern media controls is a bit more complicated on Mac but
# since we are not targeting Mac yet we can come back and fix this later.
crbug.com/783154 [ Mac ] virtual/modern-media-controls/media/controls/modern/doubletap-to-jump-backwards.html [ Skip ]
crbug.com/783154 [ Mac ] virtual/modern-media-controls/media/controls/modern/doubletap-to-jump-forwards.html [ Skip ]
crbug.com/783154 virtual/modern-media-controls/media/controls/modern/doubletap-to-jump-forwards-too-short.html [ Skip ]
crbug.com/783154 [ Mac ] virtual/modern-media-controls/media/controls/modern/doubletap-on-play-button.html [ Skip ]
crbug.com/783154 [ Mac ] virtual/modern-media-controls/media/controls/modern/doubletap-to-toggle-fullscreen.html [ Skip ]
crbug.com/783154 [ Mac ] virtual/modern-media-controls/media/controls/modern/singletap-on-outside.html [ Skip ]
crbug.com/783154 [ Mac ] virtual/modern-media-controls/media/controls/modern/singletap-on-play-button.html [ Skip ]
crbug.com/783154 [ Mac ] virtual/modern-media-controls/media/controls/modern/slow-doubletap.html [ Skip ]
......@@ -596,5 +596,10 @@
"prefix": "incremental-shadow-dom",
"base": "shadow-dom",
"args": ["--enable-blink-features=IncrementalShadowDOM"]
},
{
"prefix": "modern-media-controls",
"base": "media/controls/modern",
"args": ["--enable-features=UseModernMediaControls"]
}
]
<!DOCTYPE html>
<html>
<title>Test that player will play then pause if double tapped on the play button.</title>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="../../media-controls.js"></script>
<video controls width=400 src="../../content/60_sec_video.webm"></video>
<script>
async_test(t => {
const video = document.querySelector('video');
let didPause = false;
video.onplaying = t.step_func(() => {
if (didPause) {
t.done();
} else {
const coordinates =
elementCoordinates(mediaControlsOverlayPlayButtonInternal(video));
doubleTapAtCoordinates(coordinates[0], coordinates[1]);
}
});
video.addEventListener('pause', t.step_func(() => {
didPause = true;
}), { once: true });
video.play();
});
</script>
</html>
<!DOCTYPE html>
<html>
<title>Test that player will jump to the beginning if it's in the first 10 seconds.</title>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="../../media-controls.js"></script>
<video controls width=400 src="../../content/60_sec_video.webm"></video>
<script>
async_test(t => {
enableDoubleTapToJumpForTest(t);
const video = document.querySelector('video');
let count = 0;
video.addEventListener('playing', t.step_func(() => {
// Double tap in the top left hand corner
const coordinates =
coordinatesOutsideElement(mediaControlsOverlayPlayButton(video));
doubleTapAtCoordinates(coordinates[0] + 1, coordinates[1] + 1);
}), { once: true });
video.ontimeupdate = t.step_func(() => {
// The time should reach 0 seconds twice, the first time when playing and
// the second because of tapping.
if (Math.round(video.currentTime) == 0) {
count++;
if (count == 2)
t.done();
}
});
video.play();
});
</script>
</html></script>
</html>
<!DOCTYPE html>
<html>
<title>Test that player will jump backwards 10 seconds if double tapped on the left hand side.</title>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="../../media-controls.js"></script>
<video controls width=400 src="../../content/60_sec_video.webm"></video>
<script>
async_test(t => {
enableDoubleTapToJumpForTest(t);
const video = document.querySelector('video');
let time = 0;
video.addEventListener('playing', t.step_func(() => {
// Seek the video to the middle
video.currentTime = 30;
}), { once: true });
video.ontimeupdate = t.step_func(() => {
// The time should never reach 25 seconds as we skipped over it
assert_not_equals(Math.round(video.currentTime), 25);
});
video.onseeked = t.step_func(() => {
const currentTime = Math.round(video.currentTime);
if (currentTime == 30) {
// Double tap in the top left hand corner
time = currentTime;
const coordinates =
coordinatesOutsideElement(mediaControlsOverlayPlayButton(video));
doubleTapAtCoordinates(coordinates[0] + 1, coordinates[1] + 1);
} else if (time > 0) {
// Check the video went back 10 seconds
assert_greater_than(time, 0);
assert_equals(currentTime, time - 10);
t.done();
}
});
video.play();
});
</script>
</html>
<!DOCTYPE html>
<html>
<title>Test that player will jump to the end if less than 10 seconds remaining.</title>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="../../media-controls.js"></script>
<video controls width=400 src="../../content/60_sec_video.webm"></video>
<script>
async_test(t => {
enableDoubleTapToJumpForTest(t);
const video = document.querySelector('video');
video.addEventListener('playing', () => {
// Seek the video to the end
if (video.currentTime < 55) {
video.currentTime = 55;
// Double tap in the top right hand corner
const coordinates =
coordinatesOutsideElement(mediaControlsOverlayPlayButton(video));
doubleTapAtCoordinates(coordinates[0] + video.width, coordinates[1] + 1);
}
}, { once: true });
video.ontimeupdate = t.step_func(() => {
// The time should never reach 57 seconds as we skipped over it
assert_not_equals(57, Math.round(video.currentTime));
});
video.addEventListener('ended', t.step_func_done(), { once: true });
video.play();
});
</script>
</html></script>
</html>
<!DOCTYPE html>
<html>
<title>Test that player will jump forwards 10 seconds if double tapped.</title>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="../../media-controls.js"></script>
<video controls width=400 src="../../content/60_sec_video.webm"></video>
<script>
async_test(t => {
enableDoubleTapToJumpForTest(t);
const video = document.querySelector('video');
let time = 0;
video.addEventListener('playing', t.step_func(() => {
// Double tap in the top right hand corner
time = Math.round(video.currentTime);
const coordinates =
coordinatesOutsideElement(mediaControlsOverlayPlayButton(video));
doubleTapAtCoordinates(coordinates[0] + video.width, coordinates[1] + 1);
}), { once: true });
video.ontimeupdate = t.step_func(() => {
// The time should never be 5 seconds as we skipped over it
assert_not_equals(Math.round(video.currentTime), 5);
});
video.addEventListener('seeked', t.step_func_done(() => {
// Check the video advanced 10 seconds
assert_equals(Math.round(video.currentTime), time + 10);
}), { once: true });
video.play();
});
</script>
</html>
<!DOCTYPE html>
<html>
<title>Test that player will enter fullscreen if double tapped.</title>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="../../media-controls.js"></script>
<video controls width=400 src="../../content/60_sec_video.webm"></video>
<script>
async_test(t => {
const video = document.querySelector('video');
video.addEventListener('playing', () => {
// Double tap in the top left hand corner
const coordinates =
coordinatesOutsideElement(mediaControlsOverlayPlayButton(video));
doubleTapAtCoordinates(coordinates[0] + 1, coordinates[1] + 1);
}, { once: true });
video.addEventListener('webkitfullscreenchange', t.step_func(() => {
assert_equals(video, document.fullscreenElement);
// We are now fullscreen, update the event handler and doubletap to exit
video.addEventListener('webkitfullscreenchange',
t.step_func_done(), { once: true });
const coordinates =
coordinatesOutsideElement(mediaControlsOverlayPlayButton(video));
doubleTapAtCoordinates(coordinates[0] + 1, coordinates[1] + 1);
}), { once: true });
video.play();
});
</script>
</html>
<!DOCTYPE html>
<html>
<title>Test that the player pauses if single taped in the outer region.</title>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="../../media-controls.js"></script>
<video controls width=400 src="../../content/60_sec_video.webm"></video>
<script>
async_test(t => {
const video = document.querySelector('video');
video.addEventListener('playing', t.step_func(() => {
// Single tap in the top right hand corner
const coordinates =
coordinatesOutsideElement(mediaControlsOverlayPlayButtonInternal(video));
singleTapAtCoordinates(coordinates[0] + 1, coordinates[1] + 1);
}), { once: true });
video.addEventListener('pause', t.step_func_done(), { once: true });
video.play();
});
</script>
</html>
<!DOCTYPE html>
<html>
<title>Test that the player pauses if single tapped on the play button.</title>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="../../media-controls.js"></script>
<video controls width=400 src="../../content/60_sec_video.webm"></video>
<script>
async_test(t => {
const video = document.querySelector('video');
video.addEventListener('playing', t.step_func(() => {
// Single tap in the middle of the button.
const coordinates =
elementCoordinates(mediaControlsOverlayPlayButtonInternal(video));
singleTapAtCoordinates(coordinates[0], coordinates[1]);
}), { once: true });
video.addEventListener('pause', t.step_func_done(), { once: true });
video.play();
});
</script>
</html>
<!DOCTYPE html>
<html>
<title>Test that player will not jump if the tap is too slow.</title>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="../../media-controls.js"></script>
<video controls width=400 src="../../content/60_sec_video.webm"></video>
<script>
async_test(t => {
const video = document.querySelector('video');
let didPause = false;
video.onplaying = t.step_func(() => {
if (didPause) {
t.done();
} else {
// Double tap in the top right hand corner
const coordinates =
coordinatesOutsideElement(mediaControlsOverlayPlayButton(video));
doubleTapAtCoordinates(coordinates[0] + 1, coordinates[1] + 1, 400);
}
});
video.addEventListener('pause', t.step_func(() => {
didPause = true;
}), { once: true });
video.play();
});
</script>
</html>
......@@ -286,3 +286,51 @@ function checkButtonNotHasClass(button, className) {
function checkControlsClassName(videoElement, className) {
assert_equals(window.internals.shadowRoot(videoElement).firstChild.className, className);
}
function mediaControlsOverlayPlayButton(videoElement) {
return mediaControlsButton(videoElement, 'overlay-play-button');
}
function mediaControlsOverlayPlayButtonInternal(videoElement) {
var controlID = '-internal-media-controls-overlay-play-button-internal';
var element = mediaControlsElement(
window.internals.shadowRoot(
mediaControlsOverlayPlayButton(videoElement)).firstChild, controlID);
if (!element)
throw 'Failed to find the internal overlay play button';
return element;
}
function doubleTapAtCoordinates(x, y, timeout) {
if (timeout == undefined)
timeout = 100;
singleTapAtCoordinates(x, y);
setTimeout(() => {
singleTapAtCoordinates(x, y);
}, timeout);
}
function singleTapAtCoordinates(xPos, yPos) {
chrome.gpuBenchmarking.pointerActionSequence([
{
source: 'touch',
actions: [
{ name: 'pointerDown', x: xPos, y: yPos },
{ name: 'pointerUp' }
]
}
]);
}
function enableDoubleTapToJumpForTest(t) {
var doubleTapToJumpOnVideoEnabledValue =
internals.runtimeFlags.doubleTapToJumpOnVideoEnabled;
internals.runtimeFlags.doubleTapToJumpOnVideoEnabled = true;
t.add_cleanup(() => {
internals.runtimeFlags.doubleTapToJumpOnVideoEnabled =
doubleTapToJumpOnVideoEnabledValue;
});
}
These are tests specific to the modern media controls. As such they should be
run with the modern media controls enabled.
......@@ -6,13 +6,17 @@
#include "core/dom/ElementShadow.h"
#include "core/dom/events/Event.h"
#include "core/events/MouseEvent.h"
#include "core/geometry/DOMRect.h"
#include "core/html/media/HTMLMediaElement.h"
#include "core/html/media/HTMLMediaSource.h"
#include "core/input_type_names.h"
#include "modules/media_controls/MediaControlsImpl.h"
#include "modules/media_controls/elements/MediaControlElementsHelper.h"
#include "platform/runtime_enabled_features.h"
#include "platform/wtf/Time.h"
#include "public/platform/Platform.h"
#include "public/platform/TaskType.h"
#include "public/platform/WebSize.h"
namespace {
......@@ -20,6 +24,24 @@ namespace {
// The size of the inner circle button in pixels.
constexpr int kInnerButtonSize = 56;
// The touch padding of the inner circle button in pixels.
constexpr int kInnerButtonTouchPaddingSize = 20;
// Check if a point is based within the boundary of a DOMRect with a margin.
bool IsPointInRect(blink::DOMRect& rect, int margin, int x, int y) {
return ((x >= (rect.left() - margin)) && (x <= (rect.right() + margin)) &&
(y >= (rect.top() - margin)) && (y <= (rect.bottom() + margin)));
}
// The delay if a touch is outside the internal button.
constexpr WTF::TimeDelta kOutsideTouchDelay = TimeDelta::FromMilliseconds(300);
// The delay if a touch is inside the internal button.
constexpr WTF::TimeDelta kInsideTouchDelay = TimeDelta::FromMilliseconds(0);
// The number of seconds to jump when double tapping.
constexpr int kNumberOfSecondsToJump = 10;
} // namespace.
namespace blink {
......@@ -34,6 +56,9 @@ namespace blink {
MediaControlOverlayPlayButtonElement::MediaControlOverlayPlayButtonElement(
MediaControlsImpl& media_controls)
: MediaControlInputElement(media_controls, kMediaOverlayPlayButton),
tap_timer_(GetDocument().GetTaskRunner(TaskType::kMediaElementEvent),
this,
&MediaControlOverlayPlayButtonElement::TapTimerFired),
internal_button_(nullptr) {
setType(InputTypeNames::button);
SetShadowPseudoId(AtomicString("-webkit-media-controls-overlay-play-button"));
......@@ -55,29 +80,99 @@ const char* MediaControlOverlayPlayButtonElement::GetNameForHistograms() const {
return "PlayOverlayButton";
}
void MediaControlOverlayPlayButtonElement::MaybePlayPause() {
if (MediaElement().paused()) {
Platform::Current()->RecordAction(
UserMetricsAction("Media.Controls.PlayOverlay"));
} else {
Platform::Current()->RecordAction(
UserMetricsAction("Media.Controls.PauseOverlay"));
}
// Allow play attempts for plain src= media to force a reload in the error
// state. This allows potential recovery for transient network and decoder
// resource issues.
const String& url = MediaElement().currentSrc().GetString();
if (MediaElement().error() && !HTMLMediaElement::IsMediaStreamURL(url) &&
!HTMLMediaSource::Lookup(url)) {
MediaElement().load();
}
MediaElement().TogglePlayState();
MaybeRecordInteracted();
UpdateDisplayType();
}
void MediaControlOverlayPlayButtonElement::MaybeJump(int seconds) {
double new_time = std::max(0.0, MediaElement().currentTime() + seconds);
new_time = std::min(new_time, MediaElement().duration());
MediaElement().setCurrentTime(new_time);
}
void MediaControlOverlayPlayButtonElement::HandlePlayPauseEvent(
Event* event,
WTF::TimeDelta delay) {
event->SetDefaultHandled();
if (tap_timer_.IsActive())
return;
tap_timer_.StartOneShot(delay, BLINK_FROM_HERE);
}
void MediaControlOverlayPlayButtonElement::DefaultEventHandler(Event* event) {
if (event->type() == EventTypeNames::click) {
if (MediaElement().paused()) {
Platform::Current()->RecordAction(
UserMetricsAction("Media.Controls.PlayOverlay"));
} else {
Platform::Current()->RecordAction(
UserMetricsAction("Media.Controls.PauseOverlay"));
// Double tap to navigate should only be available on modern controls.
if (!MediaControlsImpl::IsModern() || !event->IsMouseEvent()) {
HandlePlayPauseEvent(event, kInsideTouchDelay);
return;
}
// Allow play attempts for plain src= media to force a reload in the error
// state. This allows potential recovery for transient network and decoder
// resource issues.
const String& url = MediaElement().currentSrc().GetString();
if (MediaElement().error() && !HTMLMediaElement::IsMediaStreamURL(url) &&
!HTMLMediaSource::Lookup(url)) {
MediaElement().load();
// If the event doesn't have position data we should just default to
// play/pause.
// TODO(beccahughes): Move to PointerEvent.
MouseEvent* mouse_event = ToMouseEvent(event);
if (!mouse_event->HasPosition()) {
HandlePlayPauseEvent(event, kInsideTouchDelay);
return;
}
MediaElement().TogglePlayState();
UpdateDisplayType();
MaybeRecordInteracted();
event->SetDefaultHandled();
// If the click happened on the internal button or a margin around it then
// we should play/pause.
if (IsPointInRect(*internal_button_->getBoundingClientRect(),
kInnerButtonTouchPaddingSize, mouse_event->clientX(),
mouse_event->clientY())) {
HandlePlayPauseEvent(event, kInsideTouchDelay);
} else if (!tap_timer_.IsActive()) {
// If there was not a previous touch and this was outside of the button
// then we should play/pause but with a small unnoticeable delay to allow
// for a secondary tap.
HandlePlayPauseEvent(event, kOutsideTouchDelay);
} else {
// Cancel the play pause event.
tap_timer_.Stop();
if (RuntimeEnabledFeatures::DoubleTapToJumpOnVideoEnabled()) {
// Jump forwards or backwards based on the position of the tap.
WebSize element_size =
MediaControlElementsHelper::GetSizeOrDefault(*this, WebSize(0, 0));
if (mouse_event->clientX() >= element_size.width / 2) {
MaybeJump(kNumberOfSecondsToJump);
} else {
MaybeJump(kNumberOfSecondsToJump * -1);
}
} else {
// Enter or exit fullscreen.
if (MediaElement().IsFullscreen())
GetMediaControls().ExitFullscreen();
else
GetMediaControls().EnterFullscreen();
}
event->SetDefaultHandled();
}
}
// TODO(mlamouri): should call MediaControlInputElement::DefaultEventHandler.
......@@ -94,6 +189,14 @@ WebSize MediaControlOverlayPlayButtonElement::GetSizeOrDefault() const {
*internal_button_, WebSize(kInnerButtonSize, kInnerButtonSize));
}
void MediaControlOverlayPlayButtonElement::TapTimerFired(TimerBase*) {
std::unique_ptr<UserGestureIndicator> user_gesture_scope =
Frame::NotifyUserActivation(GetDocument().GetFrame(),
UserGestureToken::kNewGesture);
MaybePlayPause();
}
void MediaControlOverlayPlayButtonElement::Trace(blink::Visitor* visitor) {
MediaControlInputElement::Trace(visitor);
visitor->Trace(internal_button_);
......
......@@ -6,6 +6,7 @@
#define MediaControlOverlayPlayButtonElement_h
#include "modules/media_controls/elements/MediaControlInputElement.h"
#include "platform/Timer.h"
namespace blink {
......@@ -29,9 +30,18 @@ class MediaControlOverlayPlayButtonElement final
const char* GetNameForHistograms() const override;
private:
void TapTimerFired(TimerBase*);
void DefaultEventHandler(Event*) override;
bool KeepEventInNode(Event*) override;
void MaybePlayPause();
void MaybeJump(int);
void HandlePlayPauseEvent(Event*, WTF::TimeDelta);
TaskRunnerTimer<MediaControlOverlayPlayButtonElement> tap_timer_;
Member<HTMLDivElement> internal_button_;
};
......
......@@ -350,6 +350,10 @@
{
name: "DocumentWrite",
},
{
name: "DoubleTapToJumpOnVideo",
settable_from_internals: true
},
{
name: "EmbedderCSPEnforcement",
status: "stable",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment