blob: 456171e86fd9f94e163e64af2d302b75cb7d6688 [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 "third_party/blink/renderer/modules/media_controls/elements/media_control_timeline_element.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/public/platform/web_screen_info.h"
#include "third_party/blink/renderer/core/css/css_style_declaration.h"
#include "third_party/blink/renderer/core/dom/events/event.h"
#include "third_party/blink/renderer/core/dom/shadow_root.h"
#include "third_party/blink/renderer/core/events/keyboard_event.h"
#include "third_party/blink/renderer/core/events/pointer_event.h"
#include "third_party/blink/renderer/core/events/touch_event.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/html/html_div_element.h"
#include "third_party/blink/renderer/core/html/html_style_element.h"
#include "third_party/blink/renderer/core/html/media/html_media_element.h"
#include "third_party/blink/renderer/core/html/shadow/shadow_element_names.h"
#include "third_party/blink/renderer/core/html/time_ranges.h"
#include "third_party/blink/renderer/core/html_names.h"
#include "third_party/blink/renderer/core/input/touch.h"
#include "third_party/blink/renderer/core/input/touch_list.h"
#include "third_party/blink/renderer/core/input_type_names.h"
#include "third_party/blink/renderer/core/layout/layout_box.h"
#include "third_party/blink/renderer/core/page/chrome_client.h"
#include "third_party/blink/renderer/core/style/computed_style.h"
#include "third_party/blink/renderer/modules/media_controls/elements/media_control_current_time_display_element.h"
#include "third_party/blink/renderer/modules/media_controls/elements/media_control_elements_helper.h"
#include "third_party/blink/renderer/modules/media_controls/media_controls_impl.h"
#include "third_party/blink/renderer/modules/media_controls/media_controls_resource_loader.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
namespace {
const double kCurrentTimeBufferedDelta = 1.0;
// Only respond to main button of primary pointer(s).
bool IsValidPointerEvent(const blink::Event& event) {
DCHECK(event.IsPointerEvent());
const blink::PointerEvent& pointer_event = ToPointerEvent(event);
return pointer_event.isPrimary() &&
pointer_event.button() ==
static_cast<short>(blink::WebPointerProperties::Button::kLeft);
}
} // namespace.
namespace blink {
// The DOM structure looks like:
//
// MediaControlTimelineElement
// (-webkit-media-controls-timeline)
// +-div#thumb (created by the HTMLSliderElement)
//
// +-HTMLStyleElement
MediaControlTimelineElement::MediaControlTimelineElement(
MediaControlsImpl& media_controls)
: MediaControlSliderElement(media_controls, kMediaSlider) {
SetShadowPseudoId(AtomicString("-webkit-media-controls-timeline"));
if (MediaControlsImpl::IsModern()) {
Element& track = GetTrackElement();
// TODO(851144): This stylesheet no longer contains animations, so should
// be re-combined with the UA sheet.
// This stylesheet element contains rules that cannot be present in the UA
// stylesheet (e.g. animations).
auto* style = HTMLStyleElement::Create(GetDocument(), CreateElementFlags());
style->setTextContent(
MediaControlsResourceLoader::GetShadowTimelineStyleSheet());
track.ParserAppendChild(style);
}
}
bool MediaControlTimelineElement::WillRespondToMouseClickEvents() {
return isConnected() && GetDocument().IsActive();
}
void MediaControlTimelineElement::SetPosition(double current_time) {
setValue(String::Number(current_time));
setAttribute(
HTMLNames::aria_valuetextAttr,
AtomicString(GetMediaControls().CurrentTimeDisplay().textContent(true)));
RenderBarSegments();
}
void MediaControlTimelineElement::SetDuration(double duration) {
SetFloatingPointAttribute(HTMLNames::maxAttr,
std::isfinite(duration) ? duration : 0);
RenderBarSegments();
}
void MediaControlTimelineElement::OnPlaying() {
Frame* frame = GetDocument().GetFrame();
if (!frame)
return;
metrics_.RecordPlaying(
frame->GetChromeClient().GetScreenInfo().orientation_type,
MediaElement().IsFullscreen(), TrackWidth());
}
const char* MediaControlTimelineElement::GetNameForHistograms() const {
return "TimelineSlider";
}
void MediaControlTimelineElement::DefaultEventHandler(Event& event) {
if (!isConnected() || !GetDocument().IsActive() || controls_hidden_)
return;
RenderBarSegments();
if (BeginScrubbingEvent(event)) {
Platform::Current()->RecordAction(
UserMetricsAction("Media.Controls.ScrubbingBegin"));
GetMediaControls().BeginScrubbing(MediaControlsImpl::IsTouchEvent(&event));
Element* thumb = UserAgentShadowRoot()->getElementById(
ShadowElementNames::SliderThumb());
bool started_from_thumb = thumb && thumb == event.target()->ToNode();
metrics_.StartGesture(started_from_thumb);
} else if (EndScrubbingEvent(event)) {
Platform::Current()->RecordAction(
UserMetricsAction("Media.Controls.ScrubbingEnd"));
GetMediaControls().EndScrubbing();
metrics_.RecordEndGesture(TrackWidth(), MediaElement().duration());
}
if (event.type() == EventTypeNames::keydown) {
metrics_.StartKey();
}
if (event.type() == EventTypeNames::keyup && event.IsKeyboardEvent()) {
metrics_.RecordEndKey(TrackWidth(), ToKeyboardEvent(event).keyCode());
}
MediaControlInputElement::DefaultEventHandler(event);
if (event.IsMouseEvent() || event.IsKeyboardEvent() ||
event.IsGestureEvent() || event.IsPointerEvent()) {
MaybeRecordInteracted();
}
// Update the value based on the touchmove event.
if (is_touching_ && event.type() == EventTypeNames::touchmove) {
auto& touch_event = ToTouchEvent(event);
if (touch_event.touches()->length() != 1)
return;
const Touch* touch = touch_event.touches()->item(0);
double position =
max(0.0, fmin(1.0, touch->clientX() / TrackWidth() * ZoomFactor()));
SetPosition(position * MediaElement().duration());
} else if (event.type() != EventTypeNames::input) {
return;
}
double time = value().ToDouble();
double duration = MediaElement().duration();
// Workaround for floating point error - it's possible for this element's max
// attribute to be rounded to a value slightly higher than the duration. If
// this happens and scrubber is dragged near the max, seek to duration.
if (time > duration)
time = duration;
metrics_.OnInput(MediaElement().currentTime(), time);
// FIXME: This will need to take the timeline offset into consideration
// once that concept is supported, see https://crbug.com/312699
if (MediaElement().seekable()->Contain(time))
MediaElement().setCurrentTime(time);
// Provide immediate feedback (without waiting for media to seek) to make it
// easier for user to seek to a precise time.
GetMediaControls().UpdateCurrentTimeDisplay();
}
bool MediaControlTimelineElement::KeepEventInNode(const Event& event) const {
return MediaControlElementsHelper::IsUserInteractionEventForSlider(
event, GetLayoutObject());
}
void MediaControlTimelineElement::RenderBarSegments() {
SetupBarSegments();
double current_time = MediaElement().currentTime();
double duration = MediaElement().duration();
// Draw the buffered range. Since the element may have multiple buffered
// ranges and it'd be distracting/'busy' to show all of them, show only the
// buffered range containing the current play head.
TimeRanges* buffered_time_ranges = MediaElement().buffered();
DCHECK(buffered_time_ranges);
if (std::isnan(duration) || std::isinf(duration) || !duration ||
std::isnan(current_time)) {
SetBeforeSegmentPosition(MediaControlSliderElement::Position(0, 0));
SetAfterSegmentPosition(MediaControlSliderElement::Position(0, 0));
return;
}
double current_position = current_time / duration;
MediaControlSliderElement::Position before_segment(0, 0);
MediaControlSliderElement::Position after_segment(0, 0);
// The before segment (i.e. what has been played) should be purely be based on
// the current time in the modern controls.
if (MediaControlsImpl::IsModern())
before_segment.width = current_position;
// Calculate the size of the after segment (i.e. what has been buffered).
for (unsigned i = 0; i < buffered_time_ranges->length(); ++i) {
float start = buffered_time_ranges->start(i, ASSERT_NO_EXCEPTION);
float end = buffered_time_ranges->end(i, ASSERT_NO_EXCEPTION);
// The delta is there to avoid corner cases when buffered
// ranges is out of sync with current time because of
// asynchronous media pipeline and current time caching in
// HTMLMediaElement.
// This is related to https://www.w3.org/Bugs/Public/show_bug.cgi?id=28125
// FIXME: Remove this workaround when WebMediaPlayer
// has an asynchronous pause interface.
if (std::isnan(start) || std::isnan(end) ||
start > current_time + kCurrentTimeBufferedDelta ||
end < current_time) {
continue;
}
double start_position = start / duration;
double end_position = end / duration;
if (MediaControlsImpl::IsModern()) {
// Draw dark grey highlight to show what we have loaded.
after_segment.left = current_position;
after_segment.width = end_position - current_position;
} else {
// Draw highlight to show what we have played.
if (current_position > start_position) {
after_segment.left = start_position;
after_segment.width = current_position - start_position;
}
// Draw dark grey highlight to show what we have loaded.
if (end_position > current_position) {
before_segment.left = current_position;
before_segment.width = end_position - current_position;
}
}
// Break out of the loop since we've drawn the only buffered range
// we're going to draw.
break;
}
// Update the positions of the segments.
SetBeforeSegmentPosition(before_segment);
SetAfterSegmentPosition(after_segment);
}
void MediaControlTimelineElement::Trace(blink::Visitor* visitor) {
MediaControlSliderElement::Trace(visitor);
}
bool MediaControlTimelineElement::BeginScrubbingEvent(Event& event) {
if (event.type() == EventTypeNames::touchstart) {
is_touching_ = true;
return true;
}
if (event.type() == EventTypeNames::pointerdown)
return IsValidPointerEvent(event);
return false;
}
void MediaControlTimelineElement::OnControlsHidden() {
controls_hidden_ = true;
// End scrubbing state.
is_touching_ = false;
}
void MediaControlTimelineElement::OnControlsShown() {
controls_hidden_ = false;
}
bool MediaControlTimelineElement::EndScrubbingEvent(Event& event) {
if (is_touching_) {
if (event.type() == EventTypeNames::touchend ||
event.type() == EventTypeNames::touchcancel ||
event.type() == EventTypeNames::change) {
is_touching_ = false;
return true;
}
} else if (event.type() == EventTypeNames::pointerup ||
event.type() == EventTypeNames::pointercancel) {
return IsValidPointerEvent(event);
}
return false;
}
} // namespace blink