blob: 8aa736794ace3e18df415bdff75d329021ea68fe [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "modules/media_controls/elements/MediaControlOverlayPlayButtonElement.h"
#include "core/dom/ShadowRoot.h"
#include "core/dom/events/Event.h"
#include "core/events/MouseEvent.h"
#include "core/geometry/DOMRect.h"
#include "core/html/HTMLStyleElement.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/MediaControlsResourceLoader.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 {
// 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 between two taps to be recognized as a double tap gesture.
constexpr WTF::TimeDelta kDoubleTapDelay = TimeDelta::FromMilliseconds(300);
// The number of seconds to jump when double tapping.
constexpr int kNumberOfSecondsToJump = 10;
} // namespace.
namespace blink {
MediaControlOverlayPlayButtonElement::AnimatedArrow::AnimatedArrow(
const AtomicString& id,
ContainerNode& parent)
: HTMLDivElement(parent.GetDocument()) {
setAttribute("id", id);
parent.AppendChild(this);
SetInnerHTMLFromString(MediaControlsResourceLoader::GetJumpSVGImage());
last_arrow_ = getElementById("arrow-3");
svg_container_ = getElementById("jump");
event_listener_ = new MediaControlAnimationEventListener(this);
svg_container_->SetInlineStyleProperty(CSSPropertyDisplay, CSSValueNone);
}
void MediaControlOverlayPlayButtonElement::AnimatedArrow::
OnAnimationIteration() {
counter_--;
if (counter_ == 0) {
svg_container_->SetInlineStyleProperty(CSSPropertyDisplay, CSSValueNone);
hidden_ = true;
}
}
void MediaControlOverlayPlayButtonElement::AnimatedArrow::Show() {
if (hidden_) {
svg_container_->RemoveInlineStyleProperty(CSSPropertyDisplay);
hidden_ = false;
}
counter_++;
}
Element&
MediaControlOverlayPlayButtonElement::AnimatedArrow::WatchedAnimationElement()
const {
return *last_arrow_;
}
void MediaControlOverlayPlayButtonElement::AnimatedArrow::Trace(
Visitor* visitor) {
MediaControlAnimationEventListener::Observer::Trace(visitor);
HTMLDivElement::Trace(visitor);
visitor->Trace(last_arrow_);
visitor->Trace(svg_container_);
visitor->Trace(event_listener_);
}
// The DOM structure looks like:
//
// MediaControlOverlayPlayButtonElement
// (-webkit-media-controls-overlay-play-button)
// +-div (-internal-media-controls-overlay-play-button-internal)
// {if MediaControlsImpl::IsModern}
// This contains the inner circle with the actual play/pause icon.
MediaControlOverlayPlayButtonElement::MediaControlOverlayPlayButtonElement(
MediaControlsImpl& media_controls)
: MediaControlInputElement(media_controls, kMediaOverlayPlayButton),
tap_timer_(GetDocument().GetTaskRunner(TaskType::kMediaElementEvent),
this,
&MediaControlOverlayPlayButtonElement::TapTimerFired),
internal_button_(nullptr),
left_jump_arrow_(nullptr),
right_jump_arrow_(nullptr) {
EnsureUserAgentShadowRoot();
setType(InputTypeNames::button);
SetShadowPseudoId(AtomicString("-webkit-media-controls-overlay-play-button"));
if (MediaControlsImpl::IsModern()) {
ShadowRoot* shadow_root = GetShadowRoot();
// This stylesheet element and will contain rules that are specific to the
// loading panel. The shadow DOM protects these rules from the parent DOM
// from bleeding across the shadow DOM boundary.
auto* style = HTMLStyleElement::Create(GetDocument(), CreateElementFlags());
style->setTextContent(
MediaControlsResourceLoader::GetOverlayPlayStyleSheet());
shadow_root->AppendChild(style);
left_jump_arrow_ = new MediaControlOverlayPlayButtonElement::AnimatedArrow(
"left-arrow", *shadow_root);
internal_button_ = MediaControlElementsHelper::CreateDiv(
"-internal-media-controls-overlay-play-button-internal", shadow_root);
right_jump_arrow_ = new MediaControlOverlayPlayButtonElement::AnimatedArrow(
"right-arrow", *shadow_root);
}
}
void MediaControlOverlayPlayButtonElement::UpdateDisplayType() {
SetIsWanted(MediaElement().ShouldShowControls() &&
(MediaControlsImpl::IsModern() || MediaElement().paused()));
MediaControlInputElement::UpdateDisplayType();
}
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);
if (seconds > 0)
right_jump_arrow_->Show();
else
left_jump_arrow_->Show();
}
void MediaControlOverlayPlayButtonElement::DefaultEventHandler(Event* event) {
if (event->type() == EventTypeNames::click) {
event->SetDefaultHandled();
// Double tap to navigate should only be available on modern controls.
if (!MediaControlsImpl::IsModern() || !event->IsMouseEvent()) {
MaybePlayPause();
return;
}
// 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()) {
MaybePlayPause();
return;
}
// 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())) {
MaybePlayPause();
} else if (!tap_timer_.IsActive()) {
// If there was not a previous touch and this was outside of the button
// then we should toggle visibility with a small unnoticeable delay in
// case their is a second tap.
if (tap_timer_.IsActive())
return;
tap_was_touch_event_ = MediaControlsImpl::IsTouchEvent(event);
tap_timer_.StartOneShot(kDoubleTapDelay, FROM_HERE);
} else {
// Cancel the play pause event.
tap_timer_.Stop();
tap_was_touch_event_.reset();
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 {
if (GetMediaControls().IsFullscreenEnabled()) {
// Enter or exit fullscreen.
if (MediaElement().IsFullscreen())
GetMediaControls().ExitFullscreen();
else
GetMediaControls().EnterFullscreen();
}
}
event->SetDefaultHandled();
}
}
MediaControlInputElement::DefaultEventHandler(event);
}
bool MediaControlOverlayPlayButtonElement::KeepEventInNode(Event* event) {
return MediaControlElementsHelper::IsUserInteractionEvent(event);
}
WebSize MediaControlOverlayPlayButtonElement::GetSizeOrDefault() const {
// The size should come from the internal button which actually displays the
// button.
return MediaControlElementsHelper::GetSizeOrDefault(
*internal_button_, WebSize(kInnerButtonSize, kInnerButtonSize));
}
void MediaControlOverlayPlayButtonElement::TapTimerFired(TimerBase*) {
if (tap_was_touch_event_.value())
GetMediaControls().MaybeToggleControlsFromTap();
tap_was_touch_event_.reset();
}
void MediaControlOverlayPlayButtonElement::Trace(blink::Visitor* visitor) {
MediaControlInputElement::Trace(visitor);
visitor->Trace(internal_button_);
visitor->Trace(left_jump_arrow_);
visitor->Trace(right_jump_arrow_);
}
} // namespace blink