blob: c033f9c131879cf475ab27ec5a14f3a9fc23401e [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/common/widget/screen_info.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/public/strings/grit/blink_strings.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/gesture_event.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/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/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/elements/media_control_remaining_time_display_element.h"
#include "third_party/blink/renderer/modules/media_controls/media_controls_impl.h"
#include "third_party/blink/renderer/modules/media_controls/media_controls_shared_helper.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/text/platform_locale.h"
namespace {
const int kThumbRadius = 6;
// Only respond to main button of primary pointer(s).
bool IsValidPointerEvent(const blink::Event& event) {
DCHECK(blink::IsA<blink::PointerEvent>(event));
const auto& pointer_event = blink::To<blink::PointerEvent>(event);
return pointer_event.isPrimary() &&
pointer_event.button() ==
static_cast<int16_t>(blink::WebPointerProperties::Button::kLeft);
}
} // namespace.
namespace blink {
// The DOM structure looks like:
//
// MediaControlTimelineElement
// (-webkit-media-controls-timeline)
// +-div#thumb (created by the HTMLSliderElement)
MediaControlTimelineElement::MediaControlTimelineElement(
MediaControlsImpl& media_controls)
: MediaControlSliderElement(media_controls) {
SetShadowPseudoId(AtomicString("-webkit-media-controls-timeline"));
}
bool MediaControlTimelineElement::WillRespondToMouseClickEvents() {
return isConnected() && GetDocument().IsActive();
}
void MediaControlTimelineElement::UpdateAria() {
String aria_label =
GetLocale().QueryString(IsA<HTMLVideoElement>(MediaElement())
? IDS_AX_MEDIA_VIDEO_SLIDER_HELP
: IDS_AX_MEDIA_AUDIO_SLIDER_HELP) +
" " + GetMediaControls().CurrentTimeDisplay().textContent(true) + " " +
GetMediaControls().RemainingTimeDisplay().textContent(true);
setAttribute(html_names::kAriaLabelAttr, AtomicString(aria_label));
setAttribute(html_names::kAriaValuetextAttr,
AtomicString(GetLocale().QueryString(
IDS_AX_MEDIA_CURRENT_TIME_DISPLAY,
GetMediaControls().CurrentTimeDisplay().textContent(true))));
}
void MediaControlTimelineElement::SetPosition(double current_time,
bool suppress_aria) {
setValue(String::Number(current_time));
if (!suppress_aria)
UpdateAria();
RenderBarSegments();
}
void MediaControlTimelineElement::SetDuration(double duration) {
double duration_value = std::isfinite(duration) ? duration : 0;
SetFloatingPointAttribute(html_names::kMaxAttr, duration_value);
RenderBarSegments();
}
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));
} else if (EndScrubbingEvent(event)) {
Platform::Current()->RecordAction(
UserMetricsAction("Media.Controls.ScrubbingEnd"));
GetMediaControls().EndScrubbing();
}
if (event.type() == event_type_names::kFocus)
UpdateAria();
MediaControlInputElement::DefaultEventHandler(event);
if (IsA<MouseEvent>(event) || IsA<KeyboardEvent>(event) ||
IsA<GestureEvent>(event) || IsA<PointerEvent>(event)) {
MaybeRecordInteracted();
}
// Update the value based on the touchmove event.
if (is_touching_ && event.type() == event_type_names::kTouchmove) {
auto& touch_event = To<TouchEvent>(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() != event_type_names::kInput) {
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;
// 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;
// Transform the current_position to always align with the center of thumb
// At time 0, the thumb's center is 6px away from beginning of progress bar
// At the end of video, thumb's center is -6px away from end of progress bar
// Convert 6px into ratio respect to progress bar width since
// current_position is range from 0 to 1
double width = TrackWidth() / ZoomFactor();
if (width != 0 && current_position != 0 && !MediaElement().ended()) {
double offset = kThumbRadius / width;
current_position += offset - (2 * offset * current_position);
}
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.
before_segment.width = current_position;
base::Optional<unsigned> current_buffered_time_range =
MediaControlsSharedHelpers::GetCurrentBufferedTimeRange(MediaElement());
if (current_buffered_time_range) {
float end = buffered_time_ranges->end(current_buffered_time_range.value(),
ASSERT_NO_EXCEPTION);
double end_position = end / duration;
// Draw dark grey highlight to show what we have loaded. This just uses a
// width since it just starts at zero just like the before segment.
// We use |std::max()| here because |current_position| has an offset added
// to it and can therefore be greater than |end_position| in some cases.
after_segment.width = std::max(current_position, end_position);
}
// Update the positions of the segments.
SetBeforeSegmentPosition(before_segment);
SetAfterSegmentPosition(after_segment);
}
void MediaControlTimelineElement::Trace(Visitor* visitor) const {
MediaControlSliderElement::Trace(visitor);
}
bool MediaControlTimelineElement::BeginScrubbingEvent(Event& event) {
if (event.type() == event_type_names::kTouchstart) {
is_touching_ = true;
return true;
}
if (event.type() == event_type_names::kPointerdown)
return IsValidPointerEvent(event);
return false;
}
void MediaControlTimelineElement::OnControlsHidden() {
controls_hidden_ = true;
// End scrubbing state.
is_touching_ = false;
MediaControlSliderElement::OnControlsHidden();
}
void MediaControlTimelineElement::OnControlsShown() {
controls_hidden_ = false;
MediaControlSliderElement::OnControlsShown();
}
bool MediaControlTimelineElement::EndScrubbingEvent(Event& event) {
if (is_touching_) {
if (event.type() == event_type_names::kTouchend ||
event.type() == event_type_names::kTouchcancel ||
event.type() == event_type_names::kChange) {
is_touching_ = false;
return true;
}
} else if (event.type() == event_type_names::kPointerup ||
event.type() == event_type_names::kPointercancel) {
return IsValidPointerEvent(event);
}
return false;
}
} // namespace blink