blob: 171ad5b8c19250051f470b22989772110db35fe5 [file] [log] [blame]
// Copyright 2015 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/core/html/track/cue_timeline.h"
#include <algorithm>
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/events/event.h"
#include "third_party/blink/renderer/core/html/media/html_media_element.h"
#include "third_party/blink/renderer/core/html/track/html_track_element.h"
#include "third_party/blink/renderer/core/html/track/loadable_text_track.h"
#include "third_party/blink/renderer/core/html/track/text_track.h"
#include "third_party/blink/renderer/core/html/track/text_track_cue.h"
#include "third_party/blink/renderer/core/html/track/text_track_cue_list.h"
namespace blink {
namespace {
CueInterval CreateCueInterval(TextTrackCue* cue) {
// Negative duration cues need be treated in the interval tree as
// zero-length cues.
double const interval_end_time = std::max(cue->startTime(), cue->endTime());
return CueIntervalTree::CreateInterval(cue->startTime(), interval_end_time,
cue);
}
base::TimeDelta CalculateEventTimeout(double event_time,
HTMLMediaElement const& media_element) {
static_assert(HTMLMediaElement::kMinPlaybackRate >= 0,
"The following code assumes playback rates are never negative");
DCHECK_NE(media_element.playbackRate(), 0);
auto const timeout =
base::TimeDelta::FromSecondsD((event_time - media_element.currentTime()) /
media_element.playbackRate());
// Only allow timeouts of multiples of 1ms to prevent "polling-by-timer"
// and excessive calls to `TimeMarchesOn`.
constexpr base::TimeDelta kMinTimeoutInterval =
base::TimeDelta::FromMilliseconds(1);
return std::max(timeout.CeilToMultiple(kMinTimeoutInterval),
kMinTimeoutInterval);
}
} // namespace
CueTimeline::CueTimeline(HTMLMediaElement& media_element)
: media_element_(&media_element),
last_update_time_(-1),
cue_event_timer_(
media_element.GetDocument().GetTaskRunner(TaskType::kInternalMedia),
this,
&CueTimeline::CueEventTimerFired),
cue_timestamp_event_timer_(
media_element.GetDocument().GetTaskRunner(TaskType::kInternalMedia),
this,
&CueTimeline::CueTimestampEventTimerFired),
ignore_update_(0),
update_requested_while_ignoring_(false) {}
void CueTimeline::AddCues(TextTrack* track, const TextTrackCueList* cues) {
DCHECK_NE(track->mode(), TextTrack::DisabledKeyword());
for (wtf_size_t i = 0; i < cues->length(); ++i)
AddCueInternal(cues->AnonymousIndexedGetter(i));
if (!MediaElement().IsShowPosterFlagSet()) {
InvokeTimeMarchesOn();
}
}
void CueTimeline::AddCue(TextTrack* track, TextTrackCue* cue) {
DCHECK_NE(track->mode(), TextTrack::DisabledKeyword());
AddCueInternal(cue);
if (!MediaElement().IsShowPosterFlagSet()) {
InvokeTimeMarchesOn();
}
}
void CueTimeline::AddCueInternal(TextTrackCue* cue) {
CueInterval interval = CreateCueInterval(cue);
if (!cue_tree_.Contains(interval))
cue_tree_.Add(interval);
}
void CueTimeline::RemoveCues(TextTrack*, const TextTrackCueList* cues) {
for (wtf_size_t i = 0; i < cues->length(); ++i)
RemoveCueInternal(cues->AnonymousIndexedGetter(i));
if (!MediaElement().IsShowPosterFlagSet()) {
InvokeTimeMarchesOn();
}
}
void CueTimeline::RemoveCue(TextTrack*, TextTrackCue* cue) {
RemoveCueInternal(cue);
if (!MediaElement().IsShowPosterFlagSet()) {
InvokeTimeMarchesOn();
}
}
void CueTimeline::RemoveCueInternal(TextTrackCue* cue) {
CueInterval interval = CreateCueInterval(cue);
cue_tree_.Remove(interval);
wtf_size_t index = currently_active_cues_.Find(interval);
if (index != kNotFound) {
DCHECK(cue->IsActive());
currently_active_cues_.EraseAt(index);
cue->SetIsActive(false);
// Since the cue will be removed from the media element and likely the
// TextTrack might also be destructed, notifying the region of the cue
// removal shouldn't be done.
cue->RemoveDisplayTree(TextTrackCue::kDontNotifyRegion);
}
}
void CueTimeline::HideCues(TextTrack*, const TextTrackCueList* cues) {
for (wtf_size_t i = 0; i < cues->length(); ++i)
cues->AnonymousIndexedGetter(i)->RemoveDisplayTree();
}
static bool TrackIndexCompare(TextTrack* a, TextTrack* b) {
return a->TrackIndex() - b->TrackIndex() < 0;
}
static bool EventTimeCueCompare(const std::pair<double, TextTrackCue*>& a,
const std::pair<double, TextTrackCue*>& b) {
// 12 - Sort the tasks in events in ascending time order (tasks with earlier
// times first).
if (a.first != b.first)
return a.first - b.first < 0;
// If the cues belong to different text tracks, it doesn't make sense to
// compare the two tracks by the relative cue order, so return the relative
// track order.
if (a.second->track() != b.second->track())
return TrackIndexCompare(a.second->track(), b.second->track());
// 12 - Further sort tasks in events that have the same time by the
// relative text track cue order of the text track cues associated
// with these tasks.
return a.second->CueIndex() < b.second->CueIndex();
}
static Event* CreateEventWithTarget(const AtomicString& event_name,
EventTarget* event_target) {
Event* event = Event::Create(event_name);
event->SetTarget(event_target);
return event;
}
void CueTimeline::TimeMarchesOn() {
DCHECK(!MediaElement().IsShowPosterFlagSet());
// 4.8.10.8 Playing the media resource
// If the current playback position changes while the steps are running,
// then the user agent must wait for the steps to complete, and then must
// immediately rerun the steps.
if (InsideIgnoreUpdateScope()) {
update_requested_while_ignoring_ = true;
return;
}
// Prevent recursive updates
auto scope = BeginIgnoreUpdateScope();
HTMLMediaElement& media_element = MediaElement();
double const movie_time = media_element.currentTime();
// Don't run the "time marches on" algorithm if the document has been
// detached. This primarily guards against dispatch of events w/
// HTMLTrackElement targets.
if (media_element.GetDocument().IsDetached())
return;
// Get the next cue event after this update
next_cue_event_ = cue_tree_.NextIntervalPoint(movie_time);
// https://html.spec.whatwg.org/C/#time-marches-on
// 1 - Let current cues be a list of cues, initialized to contain all the
// cues of all the hidden, showing, or showing by default text tracks of the
// media element (not the disabled ones) whose start times are less than or
// equal to the current playback position and whose end times are greater
// than the current playback position.
CueList current_cues;
// The user agent must synchronously unset [the text track cue active] flag
// whenever ... the media element's readyState is changed back to
// kHaveNothing.
if (media_element.getReadyState() != HTMLMediaElement::kHaveNothing &&
media_element.GetWebMediaPlayer()) {
current_cues =
cue_tree_.AllOverlaps(cue_tree_.CreateInterval(movie_time, movie_time));
}
CueList previous_cues;
// 2 - Let other cues be a list of cues, initialized to contain all the cues
// of hidden, showing, and showing by default text tracks of the media
// element that are not present in current cues.
previous_cues = currently_active_cues_;
// 3 - Let last time be the current playback position at the time this
// algorithm was last run for this media element, if this is not the first
// time it has run.
double last_time = last_update_time_;
double last_seek_time = media_element.LastSeekTime();
// 4 - If the current playback position has, since the last time this
// algorithm was run, only changed through its usual monotonic increase
// during normal playback, then let missed cues be the list of cues in other
// cues whose start times are greater than or equal to last time and whose
// end times are less than or equal to the current playback position.
// Otherwise, let missed cues be an empty list.
CueList missed_cues;
if (last_time >= 0 && last_seek_time < movie_time) {
CueList potentially_skipped_cues =
cue_tree_.AllOverlaps(cue_tree_.CreateInterval(last_time, movie_time));
missed_cues.ReserveInitialCapacity(potentially_skipped_cues.size());
for (CueInterval cue : potentially_skipped_cues) {
// Consider cues that may have been missed since the last seek time.
if (cue.Low() > std::max(last_seek_time, last_time) &&
cue.High() < movie_time)
missed_cues.push_back(cue);
}
}
last_update_time_ = movie_time;
// 5 - If the time was reached through the usual monotonic increase of the
// current playback position during normal playback, and if the user agent
// has not fired a timeupdate event at the element in the past 15 to 250ms...
// NOTE: periodic 'timeupdate' scheduling is handled by HTMLMediaElement in
// PlaybackProgressTimerFired().
// Explicitly cache vector sizes, as their content is constant from here.
wtf_size_t missed_cues_size = missed_cues.size();
wtf_size_t previous_cues_size = previous_cues.size();
// 6 - If all of the cues in current cues have their text track cue active
// flag set, none of the cues in other cues have their text track cue active
// flag set, and missed cues is empty, then abort these steps.
bool active_set_changed = missed_cues_size;
for (wtf_size_t i = 0; !active_set_changed && i < previous_cues_size; ++i) {
if (!current_cues.Contains(previous_cues[i]) &&
previous_cues[i].Data()->IsActive())
active_set_changed = true;
}
for (CueInterval current_cue : current_cues) {
// Notify any cues that are already active of the current time to mark
// past and future nodes. Any inactive cues have an empty display state;
// they will be notified of the current time when the display state is
// updated.
if (current_cue.Data()->IsActive())
current_cue.Data()->UpdatePastAndFutureNodes(movie_time);
else
active_set_changed = true;
}
if (!active_set_changed)
return;
// 7 - If the time was reached through the usual monotonic increase of the
// current playback position during normal playback, and there are cues in
// other cues that have their text track cue pause-on-exi flag set and that
// either have their text track cue active flag set or are also in missed
// cues, then immediately pause the media element.
for (wtf_size_t i = 0; !media_element.paused() && i < previous_cues_size;
++i) {
if (previous_cues[i].Data()->pauseOnExit() &&
previous_cues[i].Data()->IsActive() &&
!current_cues.Contains(previous_cues[i]))
media_element.pause();
}
for (wtf_size_t i = 0; !media_element.paused() && i < missed_cues_size; ++i) {
if (missed_cues[i].Data()->pauseOnExit())
media_element.pause();
}
// 8 - Let events be a list of tasks, initially empty. Each task in this
// list will be associated with a text track, a text track cue, and a time,
// which are used to sort the list before the tasks are queued.
HeapVector<std::pair<double, Member<TextTrackCue>>> event_tasks;
// 8 - Let affected tracks be a list of text tracks, initially empty.
HeapVector<Member<TextTrack>> affected_tracks;
for (const auto& missed_cue : missed_cues) {
// 9 - For each text track cue in missed cues, prepare an event named enter
// for the TextTrackCue object with the text track cue start time.
event_tasks.push_back(
std::make_pair(missed_cue.Data()->startTime(), missed_cue.Data()));
// 10 - For each text track [...] in missed cues, prepare an event
// named exit for the TextTrackCue object with the with the later of
// the text track cue end time and the text track cue start time.
// Note: An explicit task is added only if the cue is NOT a zero or
// negative length cue. Otherwise, the need for an exit event is
// checked when these tasks are actually queued below. This doesn't
// affect sorting events before dispatch either, because the exit
// event has the same time as the enter event.
if (missed_cue.Data()->startTime() < missed_cue.Data()->endTime()) {
event_tasks.push_back(
std::make_pair(missed_cue.Data()->endTime(), missed_cue.Data()));
}
}
for (const auto& previous_cue : previous_cues) {
// 10 - For each text track cue in other cues that has its text
// track cue active flag set prepare an event named exit for the
// TextTrackCue object with the text track cue end time.
if (!current_cues.Contains(previous_cue)) {
event_tasks.push_back(
std::make_pair(previous_cue.Data()->endTime(), previous_cue.Data()));
}
}
for (const auto& current_cue : current_cues) {
// 11 - For each text track cue in current cues that does not have its
// text track cue active flag set, prepare an event named enter for the
// TextTrackCue object with the text track cue start time.
if (!previous_cues.Contains(current_cue)) {
event_tasks.push_back(
std::make_pair(current_cue.Data()->startTime(), current_cue.Data()));
}
}
// 12 - Sort the tasks in events in ascending time order (tasks with earlier
// times first).
std::sort(event_tasks.begin(), event_tasks.end(), EventTimeCueCompare);
for (const auto& task : event_tasks) {
if (!affected_tracks.Contains(task.second->track()))
affected_tracks.push_back(task.second->track());
// 13 - Queue each task in events, in list order.
// Each event in eventTasks may be either an enterEvent or an exitEvent,
// depending on the time that is associated with the event. This
// correctly identifies the type of the event, if the startTime is
// less than the endTime in the cue.
if (task.second->startTime() >= task.second->endTime()) {
media_element.ScheduleEvent(
CreateEventWithTarget(event_type_names::kEnter, task.second.Get()));
media_element.ScheduleEvent(
CreateEventWithTarget(event_type_names::kExit, task.second.Get()));
} else {
bool is_enter_event = task.first == task.second->startTime();
AtomicString event_name =
is_enter_event ? event_type_names::kEnter : event_type_names::kExit;
media_element.ScheduleEvent(
CreateEventWithTarget(event_name, task.second.Get()));
}
}
// 14 - Sort affected tracks in the same order as the text tracks appear in
// the media element's list of text tracks, and remove duplicates.
std::sort(affected_tracks.begin(), affected_tracks.end(), TrackIndexCompare);
// 15 - For each text track in affected tracks, in the list order, queue a
// task to fire a simple event named cuechange at the TextTrack object, and,
// ...
for (const auto& track : affected_tracks) {
media_element.ScheduleEvent(
CreateEventWithTarget(event_type_names::kCuechange, track.Get()));
// ... if the text track has a corresponding track element, to then fire a
// simple event named cuechange at the track element as well.
if (auto* loadable_text_track = DynamicTo<LoadableTextTrack>(track.Get())) {
HTMLTrackElement* track_element = loadable_text_track->TrackElement();
DCHECK(track_element);
media_element.ScheduleEvent(
CreateEventWithTarget(event_type_names::kCuechange, track_element));
}
}
// 16 - Set the text track cue active flag of all the cues in the current
// cues, and unset the text track cue active flag of all the cues in the
// other cues.
for (const auto& cue : current_cues)
cue.Data()->SetIsActive(true);
for (const auto& previous_cue : previous_cues) {
if (!current_cues.Contains(previous_cue)) {
TextTrackCue* cue = previous_cue.Data();
cue->SetIsActive(false);
cue->RemoveDisplayTree();
}
}
// Update the current active cues.
currently_active_cues_ = current_cues;
media_element.UpdateTextTrackDisplay();
}
void CueTimeline::UpdateActiveCuePastAndFutureNodes() {
double const movie_time = MediaElement().currentTime();
for (auto cue : currently_active_cues_) {
DCHECK(cue.Data()->IsActive());
if (!cue.Data()->track() || !cue.Data()->track()->IsRendered())
continue;
cue.Data()->UpdatePastAndFutureNodes(movie_time);
}
SetCueTimestampEventTimer();
}
CueTimeline::IgnoreUpdateScope CueTimeline::BeginIgnoreUpdateScope() {
DCHECK(!ignore_update_ || !update_requested_while_ignoring_);
++ignore_update_;
IgnoreUpdateScope scope(*this);
return scope;
}
void CueTimeline::EndIgnoreUpdateScope(base::PassKey<IgnoreUpdateScope>,
IgnoreUpdateScope const& scope) {
DCHECK(ignore_update_);
--ignore_update_;
// If this is the last scope and an update was requested, then perform it
if (!ignore_update_ && update_requested_while_ignoring_) {
update_requested_while_ignoring_ = false;
if (!MediaElement().IsShowPosterFlagSet()) {
InvokeTimeMarchesOn();
}
}
}
void CueTimeline::InvokeTimeMarchesOn() {
TimeMarchesOn();
SetCueEventTimer();
SetCueTimestampEventTimer();
}
void CueTimeline::OnPause() {
CancelCueEventTimer();
CancelCueTimestampEventTimer();
}
void CueTimeline::OnPlaybackRateUpdated() {
SetCueEventTimer();
SetCueTimestampEventTimer();
}
void CueTimeline::OnReadyStateReset() {
auto& media_element = MediaElement();
DCHECK(media_element.getReadyState() == HTMLMediaElement::kHaveNothing);
// Deactivate all active cues
// "The user agent must synchronously unset this flag ... whenever the media
// element's readyState is changed back to HAVE_NOTHING."
for (auto cue : currently_active_cues_) {
cue.Data()->SetIsActive(false);
}
currently_active_cues_.clear();
CancelCueEventTimer();
CancelCueTimestampEventTimer();
last_update_time_ = -1;
if (media_element.IsHTMLVideoElement() && media_element.TextTracksVisible()) {
media_element.UpdateTextTrackDisplay();
}
}
void CueTimeline::SetCueEventTimer() {
auto const& media_element = MediaElement();
if (!next_cue_event_.has_value() || media_element.paused() ||
media_element.playbackRate() == 0) {
CancelCueEventTimer();
return;
}
auto const timeout =
CalculateEventTimeout(next_cue_event_.value(), media_element);
cue_event_timer_.StartOneShot(timeout, FROM_HERE);
}
void CueTimeline::CancelCueEventTimer() {
if (cue_event_timer_.IsActive()) {
cue_event_timer_.Stop();
}
}
void CueTimeline::CueEventTimerFired(TimerBase*) {
InvokeTimeMarchesOn();
}
void CueTimeline::CueTimestampEventTimerFired(TimerBase*) {
UpdateActiveCuePastAndFutureNodes();
SetCueTimestampEventTimer();
}
void CueTimeline::SetCueTimestampEventTimer() {
double constexpr kInfinity = std::numeric_limits<double>::infinity();
auto const& media_element = MediaElement();
if (media_element.paused() || media_element.playbackRate() == 0) {
CancelCueTimestampEventTimer();
return;
}
double const movie_time = media_element.currentTime();
double next_cue_timestamp_event = kInfinity;
for (auto cue : currently_active_cues_) {
auto const timestamp = cue.Data()->GetNextIntraCueTime(movie_time);
next_cue_timestamp_event =
std::min(next_cue_timestamp_event, timestamp.value_or(kInfinity));
}
if (std::isinf(next_cue_timestamp_event)) {
CancelCueTimestampEventTimer();
return;
}
auto const timeout =
CalculateEventTimeout(next_cue_timestamp_event, media_element);
cue_timestamp_event_timer_.StartOneShot(timeout, FROM_HERE);
}
void CueTimeline::CancelCueTimestampEventTimer() {
if (cue_timestamp_event_timer_.IsActive()) {
cue_timestamp_event_timer_.Stop();
}
}
void CueTimeline::DidMoveToNewDocument(Document& /*old_document*/) {
cue_event_timer_.MoveToNewTaskRunner(
MediaElement().GetDocument().GetTaskRunner(TaskType::kInternalMedia));
cue_timestamp_event_timer_.MoveToNewTaskRunner(
MediaElement().GetDocument().GetTaskRunner(TaskType::kInternalMedia));
}
void CueTimeline::Trace(Visitor* visitor) const {
visitor->Trace(media_element_);
visitor->Trace(cue_event_timer_);
visitor->Trace(cue_timestamp_event_timer_);
}
} // namespace blink