Commit f7653674 authored by philipj@opera.com's avatar philipj@opera.com

Implement the activation behavior of media elements (click to play/pause)

http://www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html#user-interface

Added to the spec in http://html5.org/r/8315 after a long discussion:
http://lists.whatwg.org/htdig.cgi/whatwg-whatwg.org/2013-November/041662.html

Firefox already implements click to play/pause when controls are
visible, but that implementation predates the spec so accesskey does not
work.

HTMLMediaElement::togglePlayState() is made public so that it can be
used to implement play/pause in MediaControls also.

The video-click-dblckick-standalone.html test has been expected to fail
since April 2013, r148246, so remove it now since the the results would
change. The new tests provide sufficient coverage.

BUG=354746
TEST=LayoutTests/media/activation-behavior.html
     LayoutTests/media/activation-behavior-accesskey.html
     LayoutTests/media/activation-behavior-shadow.html

Review URL: https://codereview.chromium.org/208483002

git-svn-id: svn://svn.chromium.org/blink/trunk@169812 bbb929c8-8fbe-4397-9dbb-9b2b20218538
parent a9722792
......@@ -15,8 +15,10 @@ onload = function() {
video.src = mediaFile;
video.addEventListener('canplaythrough', canplaythrough);
video.addEventListener('webkitfullscreenchange', fullscreenChanged);
video.onclick = function() {
video.onclick = function(event) {
video.webkitRequestFullscreen();
// cancel the activation behavior (click to play/pause)
event.preventDefault();
};
}
</script>
......
This is a testharness.js-based test.
PASS activation behavior using accesskey
Harness: the test ran to completion.
<!doctype html>
<title>activation behavior using accesskey</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<video controls accesskey="x"></video>
<script>
function pressAccessKey(key)
{
if (navigator.userAgent.search(/\bMac OS X\b/) != -1)
modifiers = ["ctrlKey", "altKey"];
else
modifiers = ["altKey"];
eventSender.keyDown(key, modifiers);
}
test(function()
{
var v = document.querySelector("video");
assert_true(v.paused, "paused state before access key press");
pressAccessKey("x");
assert_false(v.paused, "paused state before access key press");
});
</script>
This is a testharness.js-based test.
PASS audio activation behavior for restrained media controller
PASS audio activation behavior for restrained media controller (with non-autoplaying paused slave)
PASS audio activation behavior for restrained media controller (with non-blocked playing and autoplaying-and-paused slaves)
PASS audio activation behavior for paused media controller
PASS audio activation behavior for paused media controller (with non-blocked paused slave)
PASS audio activation behavior for playing media controller
PASS audio activation behavior for playing media controller (with non-blocked playing slave)
PASS audio activation behavior for playing media controller (with non-blocked autoplaying-and-paused and blocked paused slaves)
PASS audio activation behavior for paused media element
PASS audio activation behavior for playing media element
PASS audio activation behavior for canceled event
PASS audio activation behavior without controls
PASS video activation behavior for restrained media controller
PASS video activation behavior for restrained media controller (with non-autoplaying paused slave)
PASS video activation behavior for restrained media controller (with non-blocked playing and autoplaying-and-paused slaves)
PASS video activation behavior for paused media controller
PASS video activation behavior for paused media controller (with non-blocked paused slave)
PASS video activation behavior for playing media controller
PASS video activation behavior for playing media controller (with non-blocked playing slave)
PASS video activation behavior for playing media controller (with non-blocked autoplaying-and-paused and blocked paused slaves)
PASS video activation behavior for paused media element
PASS video activation behavior for playing media element
PASS video activation behavior for canceled event
PASS video activation behavior without controls
Harness: the test ran to completion.
This is a testharness.js-based test.
PASS activation behavior with shadow children
Harness: the test ran to completion.
<!doctype html>
<title>activation behavior with shadow children</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<video controls></video>
<script>
function shouldTogglePlayState(shadowChild)
{
var id = internals.shadowPseudoId(shadowChild);
if (id == "-webkit-media-controls")
return true;
if (id == "-webkit-media-controls-play-button")
return true;
if (id == "-webkit-media-controls-panel")
return false;
return shouldTogglePlayState(shadowChild.parentNode);
}
test(function()
{
var v = document.querySelector("video");
var shadowChildren = internals.shadowRoot(v).querySelectorAll("*");
Array.prototype.forEach.call(shadowChildren, function(shadowChild)
{
v.pause();
shadowChild.click();
assert_equals(v.paused, !shouldTogglePlayState(shadowChild),
"paused state after click element with pseudo id '"
+ internals.shadowPseudoId(shadowChild) + "'");
});
});
</script>
<!doctype html>
<title>activation behavior</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script src="media-file.js"></script>
<div id="log"></div>
<script>
function activation_behavior_test(tagName, src)
{
async_test(function(t)
{
var e = document.createElement(tagName);
e.controls = true;
e.src = src;
e.preload = 'auto';
e.oncanplay = t.step_func(function()
{
assert_greater_than(e.readyState, e.HAVE_CURRENT_DATA, 'element readyState');
e.controller = new MediaController();
assert_false(e.controller.paused, 'controller paused state before click');
assert_true(e.paused, 'element paused state before click');
e.click();
assert_false(e.controller.paused, 'controller paused state after click');
assert_false(e.paused, 'element paused state after click');
t.done();
});
}, tagName + ' activation behavior for restrained media controller');
async_test(function(t)
{
var e = document.createElement(tagName);
e.controls = true;
e.src = src;
e.preload = 'auto';
e.oncanplay = t.step_func(function()
{
e.pause(); // clears autoplaying flag
assert_greater_than(e.readyState, e.HAVE_CURRENT_DATA, 'element readyState');
e.controller = new MediaController();
assert_false(e.controller.paused, 'controller paused state before click');
assert_true(e.paused, 'element paused state before click');
e.click();
assert_false(e.controller.paused, 'controller paused state after click');
assert_false(e.paused, 'element paused state after click');
t.done();
});
}, tagName + ' activation behavior for restrained media controller (with non-autoplaying paused slave)');
async_test(function(t)
{
var e1 = document.createElement(tagName);
var e2 = document.createElement(tagName);
e1.controls = true;
e1.src = e2.src = src;
e1.preload = e2.preload = 'auto';
var canplaycount = 0;
e1.oncanplay = e2.oncanplay = t.step_func(function()
{
if (++canplaycount != 2) {
return;
}
e1.play();
assert_greater_than(e1.readyState, e1.HAVE_CURRENT_DATA, 'element 1 readyState');
assert_greater_than(e2.readyState, e2.HAVE_CURRENT_DATA, 'element 2 readyState');
e1.controller = new MediaController();
e2.controller = e1.controller;
assert_false(e1.controller.paused, 'controller paused state before click');
assert_false(e1.paused, 'element 1 paused state before click');
assert_true(e2.paused, 'element 2 paused state before click');
e1.click();
assert_false(e1.controller.paused, 'controller paused state after click');
assert_false(e1.paused, 'element 1 paused state after click');
assert_false(e2.paused, 'element 2 paused state after click');
t.done();
});
}, tagName + ' activation behavior for restrained media controller (with non-blocked playing and autoplaying-and-paused slaves)');
test(function()
{
var e = document.createElement(tagName);
e.controls = true;
e.controller = new MediaController();
e.controller.pause();
assert_true(e.controller.paused, 'controller paused state before click');
assert_true(e.paused, 'element paused state before click');
e.click();
assert_false(e.controller.paused, 'controller paused state after click');
assert_true(e.paused, 'element paused state after click');
}, tagName + ' activation behavior for paused media controller');
async_test(function(t)
{
var e = document.createElement(tagName);
e.controls = true;
e.src = src;
e.preload = 'auto';
e.oncanplay = t.step_func(function()
{
assert_greater_than(e.readyState, e.HAVE_CURRENT_DATA, 'element readyState');
e.controller = new MediaController();
e.controller.pause();
assert_true(e.controller.paused, 'controller paused state before click');
assert_true(e.paused, 'element paused state before click');
e.click();
assert_false(e.controller.paused, 'controller paused state after click');
assert_true(e.paused, 'element paused state after click');
t.done();
});
}, tagName + ' activation behavior for paused media controller (with non-blocked paused slave)');
test(function()
{
var e = document.createElement(tagName);
e.controls = true;
e.controller = new MediaController();
e.controller.play();
assert_false(e.controller.paused, 'controller paused state before click');
assert_false(e.paused, 'element paused state before click');
e.click();
assert_true(e.controller.paused, 'controller paused state after click');
assert_false(e.paused, 'element paused state after click');
}, tagName + ' activation behavior for playing media controller');
async_test(function(t)
{
var e = document.createElement(tagName);
e.controls = true;
e.src = src;
e.preload = 'auto';
e.oncanplay = t.step_func(function()
{
e.play();
assert_greater_than(e.readyState, e.HAVE_CURRENT_DATA, 'element readyState');
e.controller = new MediaController();
assert_false(e.controller.paused, 'controller paused state before click');
assert_false(e.paused, 'element paused state before click');
e.click();
assert_true(e.controller.paused, 'controller paused state after click');
assert_false(e.paused, 'element paused state after click');
t.done();
});
}, tagName + ' activation behavior for playing media controller (with non-blocked playing slave)');
async_test(function(t)
{
var e1 = document.createElement(tagName);
var e2 = document.createElement(tagName);
e1.controls = true;
e1.src = src;
e1.preload = 'auto';
e1.oncanplay = t.step_func(function()
{
assert_greater_than(e1.readyState, e1.HAVE_CURRENT_DATA, 'element 1 readyState');
assert_equals(e2.readyState, e2.HAVE_NOTHING, 'element 2 readyState');
e1.controller = new MediaController();
e2.controller = e1.controller;
assert_false(e1.controller.paused, 'controller paused state before click');
assert_true(e1.paused, 'element 1 paused state before click');
assert_true(e2.paused, 'element 2 paused state before click');
e1.click();
assert_true(e1.controller.paused, 'controller paused state after click');
assert_true(e1.paused, 'element 1 paused state after click');
assert_true(e2.paused, 'element 2 paused state after click');
t.done();
});
}, tagName + ' activation behavior for playing media controller (with non-blocked autoplaying-and-paused and blocked paused slaves)');
test(function()
{
var e = document.createElement(tagName);
e.controls = true;
assert_true(e.paused, 'paused state before click()');
e.click();
assert_false(e.paused, 'paused state after click()');
}, tagName + ' activation behavior for paused media element');
test(function()
{
var e = document.createElement(tagName);
e.controls = true;
e.play();
assert_false(e.paused, 'paused state before click()');
e.click();
assert_true(e.paused, 'paused state after click()');
}, tagName + ' activation behavior for playing media element');
test(function()
{
var e = document.createElement(tagName);
e.controls = true;
e.onclick = function(ev) { ev.preventDefault(); };
assert_true(e.paused, 'paused state before click()');
e.click();
assert_true(e.paused, 'paused state after click()');
}, tagName + ' activation behavior for canceled event');
test(function()
{
var e = document.createElement(tagName);
assert_true(e.paused, 'paused state before click()');
e.click();
assert_true(e.paused, 'paused state after click()');
}, tagName + ' activation behavior without controls');
}
activation_behavior_test('audio', findMediaFile('audio', 'content/test'));
activation_behavior_test('video', findMediaFile('video', 'content/test'));
</script>
This tests that clicking on a standalone video will pause and double-clicking will play.
FAIL: video should be paused
FAIL: video should be playing
<html>
<head>
<script src=media-file.js></script>
<script>
if (window.testRunner) {
testRunner.dumpAsText();
testRunner.waitUntilDone();
}
var video;
function log(msg)
{
document.getElementById('console').appendChild(document.createTextNode(msg + "\n"));
}
function test()
{
video = document.getElementById('fr').contentDocument.getElementsByTagName('video')[0];
if (video.readyState >= 4)
test2();
else
video.addEventListener('canplaythrough', test2);
}
function test2()
{
var click = document.createEvent("MouseEvents");
click.initMouseEvent("click", true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, document);
video.dispatchEvent(click);
if (video.paused) {
log("PASS: video is paused.");
test3();
} else {
log("FAIL: video should be paused");
video.addEventListener('pause', test3);
video.pause();
}
}
function test3()
{
var doubleClick = document.createEvent("MouseEvents");
doubleClick.initMouseEvent("dblclick", true, true, window, 2, 0, 0, 0, 0, false, false, false, false, 0, document);
video.dispatchEvent(doubleClick);
if (!video.paused)
log("PASS: video is playing");
else
log("FAIL: video should be playing");
if (window.testRunner)
testRunner.notifyDone();
}
</script>
</head>
<body>
<iframe id="fr"></iframe>
<script>
var frame = document.getElementById("fr");
frame.src = findMediaFile("video", "content/test");
frame.addEventListener("load", test);
</script>
<p>This tests that clicking on a standalone video will pause and double-clicking will play.</p>
<pre id="console"></pre>
</body>
</html>
......@@ -2222,6 +2222,25 @@ bool HTMLMediaElement::canPlay() const
return paused() || ended() || m_readyState < HAVE_METADATA;
}
void HTMLMediaElement::togglePlayState()
{
ASSERT(controls());
// The activation behavior of a media element that is exposing a user interface to the user
if (m_mediaController) {
if (m_mediaController->isRestrained())
m_mediaController->play();
else if (m_mediaController->paused())
m_mediaController->unpause();
else
m_mediaController->pause();
} else {
if (paused())
play();
else
pause();
}
}
void HTMLMediaElement::mediaPlayerDidAddTextTrack(WebInbandTextTrack* webTrack)
{
if (!RuntimeEnabledFeatures::videoTrackEnabled())
......@@ -3423,6 +3442,10 @@ void HTMLMediaElement::markCaptionAndSubtitleTracksAsUnconfigured()
configureTextTracks();
}
bool HTMLMediaElement::willRespondToMouseClickEvents()
{
return controls();
}
void* HTMLMediaElement::preDispatchEventHandler(Event* event)
{
......@@ -3432,6 +3455,16 @@ void* HTMLMediaElement::preDispatchEventHandler(Event* event)
return 0;
}
void HTMLMediaElement::defaultEventHandler(Event* event)
{
if (event->type() == EventTypeNames::click && willRespondToMouseClickEvents()) {
togglePlayState();
event->setDefaultHandled();
return;
}
HTMLElement::defaultEventHandler(event);
}
void HTMLMediaElement::createMediaPlayer()
{
#if ENABLE(WEB_AUDIO)
......
......@@ -162,6 +162,8 @@ public:
virtual bool canPlay() const OVERRIDE FINAL;
void togglePlayState();
PassRefPtr<TextTrack> addTextTrack(const AtomicString& kind, const AtomicString& label, const AtomicString& language, ExceptionState&);
PassRefPtr<TextTrack> addTextTrack(const AtomicString& kind, const AtomicString& label, ExceptionState& exceptionState) { return addTextTrack(kind, label, emptyAtom, exceptionState); }
PassRefPtr<TextTrack> addTextTrack(const AtomicString& kind, ExceptionState& exceptionState) { return addTextTrack(kind, emptyAtom, emptyAtom, exceptionState); }
......@@ -403,7 +405,9 @@ private:
void prepareMediaFragmentURI();
void applyMediaFragmentURI();
virtual bool willRespondToMouseClickEvents() OVERRIDE FINAL;
virtual void* preDispatchEventHandler(Event*) OVERRIDE FINAL;
virtual void defaultEventHandler(Event*) OVERRIDE FINAL;
void changeNetworkStateFromLoadingToIdle();
......
......@@ -485,20 +485,51 @@ void MediaController::bringElementUpToSpeed(HTMLMediaElement* element)
element->seek(currentTime(), IGNORE_EXCEPTION);
}
bool MediaController::isRestrained() const
{
ASSERT(!m_mediaElements.isEmpty());
// A MediaController is a restrained media controller if the MediaController is a playing media
// controller,
if (m_paused)
return false;
bool anyAutoplayingAndPaused = false;
bool allPaused = true;
for (size_t index = 0; index < m_mediaElements.size(); ++index) {
HTMLMediaElement* element = m_mediaElements[index];
// and none of its slaved media elements are blocked media elements,
if (element->isBlocked())
return false;
if (element->isAutoplaying() && element->paused())
anyAutoplayingAndPaused = true;
if (!element->paused())
allPaused = false;
}
// but either at least one of its slaved media elements whose autoplaying flag is true still has
// its paused attribute set to true, or, all of its slaved media elements have their paused
// attribute set to true.
return anyAutoplayingAndPaused || allPaused;
}
bool MediaController::isBlocked() const
{
ASSERT(!m_mediaElements.isEmpty());
// A MediaController is a blocked media controller if the MediaController is a paused media
// controller,
if (m_paused)
return true;
if (m_mediaElements.isEmpty())
return false;
bool allPaused = true;
for (size_t index = 0; index < m_mediaElements.size(); ++index) {
HTMLMediaElement* element = m_mediaElements[index];
// or if any of its slaved media elements are blocked media elements,
// or if any of its slaved media elements are blocked media elements,
if (element->isBlocked())
return true;
......
......@@ -91,6 +91,7 @@ public:
virtual bool canPlay() const OVERRIDE;
bool isRestrained() const;
bool isBlocked() const;
void clearExecutionContext() { m_executionContext = 0; }
......
......@@ -77,6 +77,17 @@ const AtomicString& MediaControlPanelElement::shadowPseudoId() const
return id;
}
void MediaControlPanelElement::defaultEventHandler(Event* event)
{
// Suppress the media element activation behavior (toggle play/pause) when
// any part of the control panel is clicked.
if (event->type() == EventTypeNames::click) {
event->setDefaultHandled();
return;
}
HTMLDivElement::defaultEventHandler(event);
}
void MediaControlPanelElement::startTimer()
{
stopTimer();
......
......@@ -49,6 +49,7 @@ private:
explicit MediaControlPanelElement(MediaControls&);
virtual const AtomicString& shadowPseudoId() const OVERRIDE;
virtual void defaultEventHandler(Event*) OVERRIDE;
void startTimer();
void stopTimer();
......
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