/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "mozilla/dom/TextTrack.h"
#include "mozilla/AsyncEventDispatcher.h"
#include "mozilla/BinarySearch.h"
#include "mozilla/dom/HTMLMediaElement.h"
#include "mozilla/dom/HTMLTrackElement.h"
#include "mozilla/dom/TextTrackBinding.h"
#include "mozilla/dom/TextTrackCue.h"
#include "mozilla/dom/TextTrackCueList.h"
#include "mozilla/dom/TextTrackList.h"
#include "mozilla/dom/TextTrackRegion.h"
#include "nsGlobalWindowInner.h"
extern mozilla::LazyLogModule gTextTrackLog;
#define WEBVTT_LOG(msg, ...) \
MOZ_LOG_FMT(gTextTrackLog, LogLevel::Debug,
"TextTrack={}, " msg, \
fmt::ptr(this),
##__VA_ARGS__)
#define WEBVTT_LOGV(msg, ...) \
MOZ_LOG_FMT(gTextTrackLog, LogLevel::Verbose,
"TextTrack={}, " msg, \
fmt::ptr(this),
##__VA_ARGS__)
namespace mozilla::dom {
NS_IMPL_CYCLE_COLLECTION_INHERITED(TextTrack, DOMEventTargetHelper, mCueList,
mActiveCueList, mTextTrackList,
mTrackElement)
NS_IMPL_ADDREF_INHERITED(TextTrack, DOMEventTargetHelper)
NS_IMPL_RELEASE_INHERITED(TextTrack, DOMEventTargetHelper)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TextTrack)
NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
TextTrack::TextTrack(nsPIDOMWindowInner* aOwnerWindow, TextTrackKind aKind,
const nsAString& aLabel,
const nsAString& aLanguage,
TextTrackMode aMode, TextTrackReadyState aReadyState,
TextTrackSource aTextTrackSource)
: DOMEventTargetHelper(aOwnerWindow),
mKind(aKind),
mLabel(aLabel),
mLanguage(aLanguage),
mMode(aMode),
mReadyState(aReadyState),
mTextTrackSource(aTextTrackSource) {
SetDefaultSettings();
}
TextTrack::TextTrack(nsPIDOMWindowInner* aOwnerWindow,
TextTrackList* aTextTrackList, TextTrackKind aKind,
const nsAString& aLabel,
const nsAString& aLanguage,
TextTrackMode aMode, TextTrackReadyState aReadyState,
TextTrackSource aTextTrackSource)
: DOMEventTargetHelper(aOwnerWindow),
mTextTrackList(aTextTrackList),
mKind(aKind),
mLabel(aLabel),
mLanguage(aLanguage),
mMode(aMode),
mReadyState(aReadyState),
mTextTrackSource(aTextTrackSource) {
SetDefaultSettings();
}
TextTrack::~TextTrack() =
default;
void TextTrack::SetDefaultSettings() {
nsPIDOMWindowInner* ownerWindow = GetOwnerWindow();
mCueList =
new TextTrackCueList(ownerWindow);
mActiveCueList =
new TextTrackCueList(ownerWindow);
mCuePos =
0;
mDirty =
false;
}
JSObject* TextTrack::WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) {
return TextTrack_Binding::Wrap(aCx, this, aGivenProto);
}
void TextTrack::SetMode(TextTrackMode aValue) {
RefPtr<TextTrack> kungFuDeathGrip = this;
if (mMode == aValue) {
return;
}
WEBVTT_LOG(
"Set mode={} for track kind {}", GetEnumString(aValue).get(),
GetEnumString(mKind).get());
mMode = aValue;
HTMLMediaElement* mediaElement = GetMediaElement();
if (aValue == TextTrackMode::Disabled) {
for (size_t i =
0; i < mCueList->Length() && mediaElement; ++i) {
mediaElement->NotifyCueRemoved(*(*mCueList)[i]);
}
SetCuesInactive();
}
else {
for (size_t i =
0; i < mCueList->Length() && mediaElement; ++i) {
mediaElement->NotifyCueAdded(*(*mCueList)[i]);
}
}
if (mediaElement) {
mediaElement->NotifyTextTrackModeChanged();
}
// https://html.spec.whatwg.org/multipage/media.html#sourcing-out-of-band-text-tracks:start-the-track-processing-model
// Run the `start-the-track-processing-model` to track's corresponding track
// element whenever track's mode changes.
if (mTrackElement) {
mTrackElement->MaybeDispatchLoadResource();
}
// Ensure the TimeMarchesOn is called in case that the mCueList
// is empty.
NotifyCueUpdated(nullptr);
}
void TextTrack::GetId(nsAString& aId)
const {
// If the track has a track element then its id should be the same as the
// track element's id.
if (mTrackElement) {
mTrackElement->GetAttr(nsGkAtoms::id, aId);
}
}
void TextTrack::AddCue(TextTrackCue& aCue) {
WEBVTT_LOG(
"AddCue {} [{}:{}]", fmt::ptr(&aCue), aCue.StartTime(),
aCue.EndTime());
TextTrack* oldTextTrack = aCue.GetTrack();
if (oldTextTrack) {
ErrorResult dummy;
oldTextTrack->RemoveCue(aCue, dummy);
}
mCueList->AddCue(aCue);
aCue.SetTrack(this);
HTMLMediaElement* mediaElement = GetMediaElement();
if (mediaElement && (mMode != TextTrackMode::Disabled)) {
mediaElement->NotifyCueAdded(aCue);
}
}
void TextTrack::RemoveCue(TextTrackCue& aCue, ErrorResult& aRv) {
WEBVTT_LOG(
"RemoveCue {}", fmt::ptr(&aCue));
// Bug1304948, check the aCue belongs to the TextTrack.
mCueList->RemoveCue(aCue, aRv);
if (aRv.Failed()) {
return;
}
aCue.SetActive(
false);
aCue.SetTrack(nullptr);
HTMLMediaElement* mediaElement = GetMediaElement();
if (mediaElement) {
mediaElement->NotifyCueRemoved(aCue);
}
}
void TextTrack::ClearAllCues() {
WEBVTT_LOG(
"ClearAllCues");
ErrorResult dummy;
while (!mCueList->IsEmpty()) {
RemoveCue(*(*mCueList)[
0], dummy);
}
}
void TextTrack::SetCuesDirty() {
for (uint32_t i =
0; i < mCueList->Length(); i++) {
((*mCueList)[i])->Reset();
}
}
TextTrackCueList* TextTrack::GetActiveCues() {
if (mMode != TextTrackMode::Disabled) {
return mActiveCueList;
}
return nullptr;
}
void TextTrack::GetActiveCueArray(nsTArray<RefPtr<TextTrackCue>>& aCues) {
if (mMode != TextTrackMode::Disabled) {
mActiveCueList->GetArray(aCues);
}
}
TextTrackReadyState TextTrack::ReadyState()
const {
return mReadyState; }
void TextTrack::SetReadyState(TextTrackReadyState aState) {
WEBVTT_LOG(
"SetReadyState={}", EnumValueToString(aState));
mReadyState = aState;
HTMLMediaElement* mediaElement = GetMediaElement();
if (mediaElement && (mReadyState == TextTrackReadyState::Loaded ||
mReadyState == TextTrackReadyState::FailedToLoad)) {
mediaElement->RemoveTextTrack(this, true);
mediaElement->UpdateReadyState();
}
}
TextTrackList* TextTrack::GetTextTrackList() {
return mTextTrackList; }
void TextTrack::SetTextTrackList(TextTrackList* aTextTrackList) {
mTextTrackList = aTextTrackList;
}
HTMLTrackElement* TextTrack::GetTrackElement() {
return mTrackElement; }
void TextTrack::SetTrackElement(HTMLTrackElement* aTrackElement) {
mTrackElement = aTrackElement;
}
void TextTrack::SetCuesInactive() {
WEBVTT_LOG(
"SetCuesInactive");
mCueList->SetCuesInactive();
}
void TextTrack::NotifyCueUpdated(TextTrackCue* aCue) {
WEBVTT_LOG(
"NotifyCueUpdated, cue={}", fmt::ptr(aCue));
mCueList->NotifyCueUpdated(aCue);
HTMLMediaElement* mediaElement = GetMediaElement();
if (mediaElement) {
mediaElement->NotifyCueUpdated(aCue);
}
}
void TextTrack::GetLabel(nsAString& aLabel)
const {
if (mTrackElement) {
mTrackElement->GetLabel(aLabel);
}
else {
aLabel = mLabel;
}
}
void TextTrack::GetLanguage(nsAString& aLanguage)
const {
if (mTrackElement) {
mTrackElement->GetSrclang(aLanguage);
}
else {
aLanguage = mLanguage;
}
}
void TextTrack::DispatchAsyncTrustedEvent(
const nsString& aEventName) {
nsGlobalWindowInner* win = GetOwnerWindow();
if (!win) {
return;
}
win->Dispatch(
NS_NewRunnableFunction(
"dom::TextTrack::DispatchAsyncTrustedEvent",
[self = RefPtr{this}, aEventName]() {
self->DispatchTrustedEvent(aEventName);
}));
}
bool TextTrack::IsLoaded() {
if (mMode == TextTrackMode::Disabled) {
return true;
}
// If the TrackElement's src is null, we can not block the
// MediaElement.
if (mTrackElement) {
nsAutoString src;
if (!(mTrackElement->GetAttr(nsGkAtoms::src, src))) {
return true;
}
}
return mReadyState >= TextTrackReadyState::Loaded;
}
void TextTrack::NotifyCueActiveStateChanged(TextTrackCue* aCue) {
MOZ_ASSERT(aCue);
if (aCue->GetActive()) {
MOZ_ASSERT(!mActiveCueList->IsCueExist(aCue));
WEBVTT_LOG(
"NotifyCueActiveStateChanged, add cue {} to the active list",
fmt::ptr(aCue));
mActiveCueList->AddCue(*aCue);
}
else {
MOZ_ASSERT(mActiveCueList->IsCueExist(aCue));
WEBVTT_LOG(
"NotifyCueActiveStateChanged, remove cue {} from the active list",
fmt::ptr(aCue));
mActiveCueList->RemoveCue(*aCue);
}
}
void TextTrack::GetOverlappingCurrentOtherAndMissCues(
CueBuckets* aCurrentCues, CueBuckets* aOtherCues, CueBuckets* aMissCues,
const media::TimeInterval& aInterval,
const Maybe<
double>& aLastTime)
const {
const HTMLMediaElement* mediaElement = GetMediaElement();
if (!mediaElement || Mode() == TextTrackMode::Disabled ||
mCueList->IsEmpty()) {
return;
}
// According to `time marches on` step1, current cue list contains the cues
// whose start times are less than or equal to the current playback position
// and whose end times are greater than the current playback position.
// https://html.spec.whatwg.org/multipage/media.html#time-marches-on
MOZ_ASSERT(aCurrentCues && aOtherCues);
const double playbackTime = mediaElement->CurrentTime();
const double intervalStart = aInterval.mStart.ToSeconds();
const double intervalEnd = aInterval.mEnd.ToSeconds();
if (intervalEnd < (*mCueList)[
0]->StartTime()) {
WEBVTT_LOGV(
"Abort : interval ends before the first cue starts");
return;
}
// Optimize the loop range by identifying the first cue that starts after the
// interval.
size_t lastIdx =
0;
struct LastIdxComparator {
const double mIntervalEnd;
explicit LastIdxComparator(
double aIntervalEnd)
: mIntervalEnd(aIntervalEnd) {}
int operator()(
const TextTrackCue* aCue)
const {
return aCue->StartTime() > mIntervalEnd ?
0 : -
1;
}
} compLast(intervalEnd);
if (!BinarySearchIf(mCueList->GetCuesArray(),
0, mCueList->Length(), compLast,
&lastIdx)) {
// Failed to find the match, set it to the last idx.
lastIdx = mCueList->Length() -
1;
}
// Search cues in the partial range.
for (size_t idx =
0; idx <= lastIdx; ++idx) {
TextTrackCue* cue = (*mCueList)[idx];
double cueStart = cue->StartTime();
double cueEnd = cue->EndTime();
if (cueStart <= playbackTime && cueEnd > playbackTime) {
WEBVTT_LOG(
"Add cue {} [{}:{}] to current cue list", fmt::ptr(cue),
cueStart, cueEnd);
aCurrentCues->AddCue(cue);
}
else {
// As the spec doesn't have a restriction for the negative duration, it
// does happen sometime if user sets it explicitly. It will be treated as
// a `missing cue` (a subset of the `other cues`) and it won't be
// displayed.
if (cueEnd < cueStart) {
// Add cue into `otherCue` only when its start time is contained by the
// current time interval.
if (intervalStart <= cueStart && cueStart < intervalEnd) {
WEBVTT_LOG(
"[Negative duration] Add cue {} [{}:{}] to other cues and "
"missing cues list",
fmt::ptr(cue), cueStart, cueEnd);
aOtherCues->AddCue(cue);
aMissCues->AddCue(cue);
}
continue;
}
// Cues are completely outside the time interval.
if (cueEnd < intervalStart || cueStart > intervalEnd) {
continue;
}
WEBVTT_LOG(
"Add cue {} [{}:{}] to other cue list", fmt::ptr(cue),
cueStart, cueEnd);
aOtherCues->AddCue(cue);
if (aLastTime && cueStart >= *aLastTime && cueEnd <= playbackTime) {
WEBVTT_LOG(
"Add cue {} [{}:{}] to missing cues list", fmt::ptr(cue),
cueStart, cueEnd);
aMissCues->AddCue(cue);
}
}
}
}
HTMLMediaElement* TextTrack::GetMediaElement()
const {
return mTextTrackList ? mTextTrackList->GetMediaElement() : nullptr;
}
void TextTrack::CueBuckets::AddCue(TextTrackCue* aCue) {
if (aCue->GetActive()) {
ActiveCues().AppendElement(aCue);
if (aCue->PauseOnExit()) {
mHasPauseOnExist[static_cast<uint8_t>(CueActivityState::Active)] = true;
}
}
else {
InactiveCues().AppendElement(aCue);
if (aCue->PauseOnExit()) {
mHasPauseOnExist[static_cast<uint8_t>(CueActivityState::Inactive)] = true;
}
}
AllCues().AppendElement(aCue);
if (aCue->PauseOnExit()) {
mHasPauseOnExist[static_cast<uint8_t>(CueActivityState::All)] = true;
}
}
}
// namespace mozilla::dom
#undef WEBVTT_LOG
#undef WEBVTT_LOGV