diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /dom/media/webvtt | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/media/webvtt')
76 files changed, 14814 insertions, 0 deletions
diff --git a/dom/media/webvtt/TextTrack.cpp b/dom/media/webvtt/TextTrack.cpp new file mode 100644 index 0000000000..5a6bb461b0 --- /dev/null +++ b/dom/media/webvtt/TextTrack.cpp @@ -0,0 +1,385 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 et tw=78: */ +/* 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/AsyncEventDispatcher.h" +#include "mozilla/dom/TextTrack.h" +#include "mozilla/dom/TextTrackBinding.h" +#include "mozilla/dom/TextTrackList.h" +#include "mozilla/dom/TextTrackCue.h" +#include "mozilla/dom/TextTrackCueList.h" +#include "mozilla/dom/TextTrackRegion.h" +#include "mozilla/dom/HTMLMediaElement.h" +#include "mozilla/dom/HTMLTrackElement.h" +#include "nsGlobalWindow.h" + +extern mozilla::LazyLogModule gTextTrackLog; + +#define WEBVTT_LOG(msg, ...) \ + MOZ_LOG(gTextTrackLog, LogLevel::Debug, \ + ("TextTrack=%p, " msg, this, ##__VA_ARGS__)) + +namespace mozilla::dom { + +static const char* ToStateStr(const TextTrackMode aMode) { + switch (aMode) { + case TextTrackMode::Disabled: + return "DISABLED"; + case TextTrackMode::Hidden: + return "HIDDEN"; + case TextTrackMode::Showing: + return "SHOWING"; + default: + MOZ_ASSERT_UNREACHABLE("Invalid state."); + } + return "Unknown"; +} + +static const char* ToReadyStateStr(const TextTrackReadyState aState) { + switch (aState) { + case TextTrackReadyState::NotLoaded: + return "NotLoaded"; + case TextTrackReadyState::Loading: + return "Loading"; + case TextTrackReadyState::Loaded: + return "Loaded"; + case TextTrackReadyState::FailedToLoad: + return "FailedToLoad"; + default: + MOZ_ASSERT_UNREACHABLE("Invalid state."); + } + return "Unknown"; +} + +static const char* ToTextTrackKindStr(const TextTrackKind aKind) { + switch (aKind) { + case TextTrackKind::Subtitles: + return "Subtitles"; + case TextTrackKind::Captions: + return "Captions"; + case TextTrackKind::Descriptions: + return "Descriptions"; + case TextTrackKind::Chapters: + return "Chapters"; + case TextTrackKind::Metadata: + return "Metadata"; + default: + MOZ_ASSERT_UNREACHABLE("Invalid kind."); + } + return "Unknown"; +} + +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 = GetOwner(); + 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) { + if (mMode == aValue) { + return; + } + WEBVTT_LOG("Set mode=%s for track kind %s", ToStateStr(aValue), + ToTextTrackKindStr(mKind)); + 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 %p [%f:%f]", &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 %p", &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=%s", ToReadyStateStr(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=%p", 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) { + nsPIDOMWindowInner* win = GetOwner(); + if (!win) { + return; + } + RefPtr<TextTrack> self = this; + nsGlobalWindowInner::Cast(win)->Dispatch( + TaskCategory::Other, + NS_NewRunnableFunction( + "dom::TextTrack::DispatchAsyncTrustedEvent", + [self, 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(kNameSpaceID_None, 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 %p to the active list", + aCue); + mActiveCueList->AddCue(*aCue); + } else { + MOZ_ASSERT(mActiveCueList->IsCueExist(aCue)); + WEBVTT_LOG( + "NotifyCueActiveStateChanged, remove cue %p from the active list", + aCue); + mActiveCueList->RemoveCue(*aCue); + } +} + +void TextTrack::GetCurrentCuesAndOtherCues( + RefPtr<TextTrackCueList>& aCurrentCues, + RefPtr<TextTrackCueList>& aOtherCues, + const media::TimeInterval& aInterval) const { + const HTMLMediaElement* mediaElement = GetMediaElement(); + if (!mediaElement) { + return; + } + + if (Mode() == TextTrackMode::Disabled) { + 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(); + for (uint32_t idx = 0; idx < mCueList->Length(); idx++) { + TextTrackCue* cue = (*mCueList)[idx]; + WEBVTT_LOG("cue %p [%f:%f], playbackTime=%f", cue, cue->StartTime(), + cue->EndTime(), playbackTime); + if (cue->StartTime() <= playbackTime && cue->EndTime() > playbackTime) { + WEBVTT_LOG("Add cue %p [%f:%f] to current cue list", cue, + cue->StartTime(), cue->EndTime()); + aCurrentCues->AddCue(*cue); + } else { + // As the spec didn't have a restriction for the negative duration, it + // does happen sometime if user sets it explictly. It would be treated as + // a `missing cue` later in the `TimeMarchesOn` but it won't be displayed. + if (cue->EndTime() < cue->StartTime()) { + // Add cue into `otherCue` only when its start time is contained by the + // current time interval. + if (aInterval.Contains( + media::TimeUnit::FromSeconds(cue->StartTime()))) { + WEBVTT_LOG("[Negative duration] Add cue %p [%f:%f] to other cue list", + cue, cue->StartTime(), cue->EndTime()); + aOtherCues->AddCue(*cue); + } + continue; + } + media::TimeInterval cueInterval( + media::TimeUnit::FromSeconds(cue->StartTime()), + media::TimeUnit::FromSeconds(cue->EndTime())); + // cues are completely outside the time interval. + if (!aInterval.Touches(cueInterval)) { + continue; + } + // contains any cues which are overlapping within the time interval. + WEBVTT_LOG("Add cue %p [%f:%f] to other cue list", cue, cue->StartTime(), + cue->EndTime()); + aOtherCues->AddCue(*cue); + } + } +} + +HTMLMediaElement* TextTrack::GetMediaElement() const { + return mTextTrackList ? mTextTrackList->GetMediaElement() : nullptr; +} + +} // namespace mozilla::dom diff --git a/dom/media/webvtt/TextTrack.h b/dom/media/webvtt/TextTrack.h new file mode 100644 index 0000000000..1adf8f1838 --- /dev/null +++ b/dom/media/webvtt/TextTrack.h @@ -0,0 +1,148 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 et tw=78: */ +/* 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/. */ + +#ifndef mozilla_dom_TextTrack_h +#define mozilla_dom_TextTrack_h + +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/TextTrackBinding.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsString.h" +#include "TimeUnits.h" + +namespace mozilla::dom { + +class TextTrackList; +class TextTrackCue; +class TextTrackCueList; +class HTMLTrackElement; +class HTMLMediaElement; + +enum class TextTrackSource : uint8_t { + Track, + AddTextTrack, + MediaResourceSpecific, +}; + +// Constants for numeric readyState property values. +enum class TextTrackReadyState : uint8_t { + NotLoaded = 0U, + Loading = 1U, + Loaded = 2U, + FailedToLoad = 3U +}; + +class TextTrack final : public DOMEventTargetHelper { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(TextTrack, DOMEventTargetHelper) + + TextTrack(nsPIDOMWindowInner* aOwnerWindow, TextTrackKind aKind, + const nsAString& aLabel, const nsAString& aLanguage, + TextTrackMode aMode, TextTrackReadyState aReadyState, + TextTrackSource aTextTrackSource); + TextTrack(nsPIDOMWindowInner* aOwnerWindow, TextTrackList* aTextTrackList, + TextTrackKind aKind, const nsAString& aLabel, + const nsAString& aLanguage, TextTrackMode aMode, + TextTrackReadyState aReadyState, TextTrackSource aTextTrackSource); + + void SetDefaultSettings(); + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + TextTrackKind Kind() const { return mKind; } + void GetLabel(nsAString& aLabel) const; + void GetLanguage(nsAString& aLanguage) const; + void GetInBandMetadataTrackDispatchType(nsAString& aType) const { + aType = mType; + } + void GetId(nsAString& aId) const; + + TextTrackMode Mode() const { return mMode; } + void SetMode(TextTrackMode aValue); + + TextTrackCueList* GetCues() const { + if (mMode == TextTrackMode::Disabled) { + return nullptr; + } + return mCueList; + } + + TextTrackCueList* GetActiveCues(); + void GetActiveCueArray(nsTArray<RefPtr<TextTrackCue> >& aCues); + + TextTrackReadyState ReadyState() const; + void SetReadyState(TextTrackReadyState aState); + + void AddCue(TextTrackCue& aCue); + void RemoveCue(TextTrackCue& aCue, ErrorResult& aRv); + void SetDirty() { mDirty = true; } + void SetCuesDirty(); + + TextTrackList* GetTextTrackList(); + void SetTextTrackList(TextTrackList* aTextTrackList); + + IMPL_EVENT_HANDLER(cuechange) + + HTMLTrackElement* GetTrackElement(); + void SetTrackElement(HTMLTrackElement* aTrackElement); + + TextTrackSource GetTextTrackSource() { return mTextTrackSource; } + + void SetCuesInactive(); + + void NotifyCueUpdated(TextTrackCue* aCue); + + void DispatchAsyncTrustedEvent(const nsString& aEventName); + + bool IsLoaded(); + + // Called when associated cue's active flag has been changed, and then we + // would add or remove the cue to the active cue list. + void NotifyCueActiveStateChanged(TextTrackCue* aCue); + + // Use this function to request current cues, which start time are less than + // or equal to the current playback position and whose end times are greater + // than the current playback position, and other cues, which are not in the + // current cues. Because there would be LOTS of cues in the other cues, and we + // don't actually need all of them. Therefore, we use a time interval to get + // the cues which are overlapping within the time interval. + void GetCurrentCuesAndOtherCues(RefPtr<TextTrackCueList>& aCurrentCues, + RefPtr<TextTrackCueList>& aOtherCues, + const media::TimeInterval& aInterval) const; + + void ClearAllCues(); + + private: + ~TextTrack(); + + HTMLMediaElement* GetMediaElement() const; + + RefPtr<TextTrackList> mTextTrackList; + + TextTrackKind mKind; + nsString mLabel; + nsString mLanguage; + nsString mType; + TextTrackMode mMode; + + RefPtr<TextTrackCueList> mCueList; + RefPtr<TextTrackCueList> mActiveCueList; + RefPtr<HTMLTrackElement> mTrackElement; + + uint32_t mCuePos; + TextTrackReadyState mReadyState; + bool mDirty; + + // An enum that represents where the track was sourced from. + TextTrackSource mTextTrackSource; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_TextTrack_h diff --git a/dom/media/webvtt/TextTrackCue.cpp b/dom/media/webvtt/TextTrackCue.cpp new file mode 100644 index 0000000000..434337a337 --- /dev/null +++ b/dom/media/webvtt/TextTrackCue.cpp @@ -0,0 +1,260 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/TextTrackCue.h" + +#include "mozilla/dom/Document.h" +#include "mozilla/dom/HTMLTrackElement.h" +#include "mozilla/dom/TextTrackList.h" +#include "mozilla/dom/TextTrackRegion.h" +#include "nsComponentManagerUtils.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/intl/Bidi.h" + +extern mozilla::LazyLogModule gTextTrackLog; + +#define LOG(msg, ...) \ + MOZ_LOG(gTextTrackLog, LogLevel::Debug, \ + ("TextTrackCue=%p, " msg, this, ##__VA_ARGS__)) + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(TextTrackCue, DOMEventTargetHelper, + mDocument, mTrack, mTrackElement, + mDisplayState, mRegion) + +NS_IMPL_ADDREF_INHERITED(TextTrackCue, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(TextTrackCue, DOMEventTargetHelper) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TextTrackCue) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +StaticRefPtr<nsIWebVTTParserWrapper> TextTrackCue::sParserWrapper; + +// Set default value for cue, spec https://w3c.github.io/webvtt/#model-cues +void TextTrackCue::SetDefaultCueSettings() { + mPositionIsAutoKeyword = true; + // Spec https://www.w3.org/TR/webvtt1/#webvtt-cue-position-automatic-alignment + mPositionAlign = PositionAlignSetting::Auto; + mSize = 100.0; + mPauseOnExit = false; + mSnapToLines = true; + mLineIsAutoKeyword = true; + mAlign = AlignSetting::Center; + mLineAlign = LineAlignSetting::Start; + mVertical = DirectionSetting::_empty; + mActive = false; +} + +TextTrackCue::TextTrackCue(nsPIDOMWindowInner* aOwnerWindow, double aStartTime, + double aEndTime, const nsAString& aText, + ErrorResult& aRv) + : DOMEventTargetHelper(aOwnerWindow), + mText(aText), + mStartTime(aStartTime), + mEndTime(aEndTime), + mPosition(0.0), + mLine(0.0), + mReset(false, "TextTrackCue::mReset"), + mHaveStartedWatcher(false), + mWatchManager( + this, GetOwnerGlobal()->AbstractMainThreadFor(TaskCategory::Other)) { + LOG("create TextTrackCue"); + SetDefaultCueSettings(); + MOZ_ASSERT(aOwnerWindow); + if (NS_FAILED(StashDocument())) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + } +} + +TextTrackCue::TextTrackCue(nsPIDOMWindowInner* aOwnerWindow, double aStartTime, + double aEndTime, const nsAString& aText, + HTMLTrackElement* aTrackElement, ErrorResult& aRv) + : DOMEventTargetHelper(aOwnerWindow), + mText(aText), + mStartTime(aStartTime), + mEndTime(aEndTime), + mTrackElement(aTrackElement), + mPosition(0.0), + mLine(0.0), + mReset(false, "TextTrackCue::mReset"), + mHaveStartedWatcher(false), + mWatchManager( + this, GetOwnerGlobal()->AbstractMainThreadFor(TaskCategory::Other)) { + LOG("create TextTrackCue"); + SetDefaultCueSettings(); + MOZ_ASSERT(aOwnerWindow); + if (NS_FAILED(StashDocument())) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + } +} + +TextTrackCue::~TextTrackCue() = default; + +/** Save a reference to our creating document so we don't have to + * keep getting it from our window. + */ +nsresult TextTrackCue::StashDocument() { + nsPIDOMWindowInner* window = GetOwner(); + if (!window) { + return NS_ERROR_NO_INTERFACE; + } + mDocument = window->GetDoc(); + if (!mDocument) { + return NS_ERROR_NOT_AVAILABLE; + } + return NS_OK; +} + +already_AddRefed<DocumentFragment> TextTrackCue::GetCueAsHTML() { + // mDocument may be null during cycle collector shutdown. + // See bug 941701. + if (!mDocument) { + return nullptr; + } + + if (!sParserWrapper) { + nsresult rv; + nsCOMPtr<nsIWebVTTParserWrapper> parserWrapper = + do_CreateInstance(NS_WEBVTTPARSERWRAPPER_CONTRACTID, &rv); + if (NS_FAILED(rv)) { + return mDocument->CreateDocumentFragment(); + } + sParserWrapper = parserWrapper; + ClearOnShutdown(&sParserWrapper); + } + + nsPIDOMWindowInner* window = mDocument->GetInnerWindow(); + if (!window) { + return mDocument->CreateDocumentFragment(); + } + + RefPtr<DocumentFragment> frag; + sParserWrapper->ConvertCueToDOMTree(window, static_cast<EventTarget*>(this), + getter_AddRefs(frag)); + if (!frag) { + return mDocument->CreateDocumentFragment(); + } + return frag.forget(); +} + +void TextTrackCue::SetTrackElement(HTMLTrackElement* aTrackElement) { + mTrackElement = aTrackElement; +} + +JSObject* TextTrackCue::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return VTTCue_Binding::Wrap(aCx, this, aGivenProto); +} + +TextTrackRegion* TextTrackCue::GetRegion() { return mRegion; } + +void TextTrackCue::SetRegion(TextTrackRegion* aRegion) { + if (mRegion == aRegion) { + return; + } + mRegion = aRegion; + mReset = true; +} + +double TextTrackCue::ComputedLine() { + // See spec https://w3c.github.io/webvtt/#cue-computed-line + if (!mLineIsAutoKeyword && !mSnapToLines && (mLine < 0.0 || mLine > 100.0)) { + return 100.0; + } else if (!mLineIsAutoKeyword) { + return mLine; + } else if (mLineIsAutoKeyword && !mSnapToLines) { + return 100.0; + } else if (!mTrack || !mTrack->GetTextTrackList() || + !mTrack->GetTextTrackList()->GetMediaElement()) { + return -1.0; + } + + RefPtr<TextTrackList> trackList = mTrack->GetTextTrackList(); + bool dummy; + uint32_t showingTracksNum = 0; + for (uint32_t idx = 0; idx < trackList->Length(); idx++) { + RefPtr<TextTrack> track = trackList->IndexedGetter(idx, dummy); + if (track->Mode() == TextTrackMode::Showing) { + showingTracksNum++; + } + + if (mTrack == track) { + break; + } + } + + return (-1.0) * showingTracksNum; +} + +double TextTrackCue::ComputedPosition() { + // See spec https://w3c.github.io/webvtt/#cue-computed-position + if (!mPositionIsAutoKeyword) { + return mPosition; + } + if (ComputedPositionAlign() == PositionAlignSetting::Line_left) { + return 0.0; + } + if (ComputedPositionAlign() == PositionAlignSetting::Line_right) { + return 100.0; + } + return 50.0; +} + +PositionAlignSetting TextTrackCue::ComputedPositionAlign() { + // See spec https://w3c.github.io/webvtt/#cue-computed-position-alignment + if (mPositionAlign != PositionAlignSetting::Auto) { + return mPositionAlign; + } else if (mAlign == AlignSetting::Left) { + return PositionAlignSetting::Line_left; + } else if (mAlign == AlignSetting::Right) { + return PositionAlignSetting::Line_right; + } else if (mAlign == AlignSetting::Start) { + return IsTextBaseDirectionLTR() ? PositionAlignSetting::Line_left + : PositionAlignSetting::Line_right; + } else if (mAlign == AlignSetting::End) { + return IsTextBaseDirectionLTR() ? PositionAlignSetting::Line_right + : PositionAlignSetting::Line_left; + } + return PositionAlignSetting::Center; +} + +bool TextTrackCue::IsTextBaseDirectionLTR() const { + // The returned result by `ubidi_getBaseDirection` might be `neutral` if the + // text only contains netural charaters. In this case, we would treat its + // base direction as LTR. + return intl::Bidi::GetBaseDirection(mText) != intl::Bidi::BaseDirection::RTL; +} + +void TextTrackCue::NotifyDisplayStatesChanged() { + if (!mReset) { + return; + } + + if (!mTrack || !mTrack->GetTextTrackList() || + !mTrack->GetTextTrackList()->GetMediaElement()) { + return; + } + + mTrack->GetTextTrackList() + ->GetMediaElement() + ->NotifyCueDisplayStatesChanged(); +} + +void TextTrackCue::SetActive(bool aActive) { + if (mActive == aActive) { + return; + } + + LOG("TextTrackCue, SetActive=%d", aActive); + mActive = aActive; + mDisplayState = mActive ? mDisplayState : nullptr; + if (mTrack) { + mTrack->NotifyCueActiveStateChanged(this); + } +} + +#undef LOG + +} // namespace mozilla::dom diff --git a/dom/media/webvtt/TextTrackCue.h b/dom/media/webvtt/TextTrackCue.h new file mode 100644 index 0000000000..90ce0a571d --- /dev/null +++ b/dom/media/webvtt/TextTrackCue.h @@ -0,0 +1,342 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 et tw=78: */ +/* 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/. */ + +#ifndef mozilla_dom_TextTrackCue_h +#define mozilla_dom_TextTrackCue_h + +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/DocumentFragment.h" +#include "mozilla/dom/VTTCueBinding.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIWebVTTParserWrapper.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/dom/HTMLDivElement.h" +#include "mozilla/dom/TextTrack.h" +#include "mozilla/StateWatching.h" + +namespace mozilla::dom { + +class Document; +class HTMLTrackElement; +class TextTrackRegion; + +class TextTrackCue final : public DOMEventTargetHelper { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(TextTrackCue, DOMEventTargetHelper) + + // TextTrackCue WebIDL + // See bug 868509 about splitting out the WebVTT-specific interfaces. + static already_AddRefed<TextTrackCue> Constructor(GlobalObject& aGlobal, + double aStartTime, + double aEndTime, + const nsAString& aText, + ErrorResult& aRv) { + nsCOMPtr<nsPIDOMWindowInner> window = + do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<TextTrackCue> ttcue = + new TextTrackCue(window, aStartTime, aEndTime, aText, aRv); + return ttcue.forget(); + } + TextTrackCue(nsPIDOMWindowInner* aGlobal, double aStartTime, double aEndTime, + const nsAString& aText, ErrorResult& aRv); + + TextTrackCue(nsPIDOMWindowInner* aGlobal, double aStartTime, double aEndTime, + const nsAString& aText, HTMLTrackElement* aTrackElement, + ErrorResult& aRv); + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + TextTrack* GetTrack() const { return mTrack; } + + void GetId(nsAString& aId) const { aId = mId; } + + void SetId(const nsAString& aId) { + if (mId == aId) { + return; + } + + mId = aId; + } + + double StartTime() const { return mStartTime; } + + void SetStartTime(double aStartTime) { + if (mStartTime == aStartTime) { + return; + } + + mStartTime = aStartTime; + mReset = true; + NotifyCueUpdated(this); + } + + double EndTime() const { return mEndTime; } + + void SetEndTime(double aEndTime) { + if (mEndTime == aEndTime) { + return; + } + + mEndTime = aEndTime; + mReset = true; + NotifyCueUpdated(this); + } + + bool PauseOnExit() { return mPauseOnExit; } + + void SetPauseOnExit(bool aPauseOnExit) { + if (mPauseOnExit == aPauseOnExit) { + return; + } + + mPauseOnExit = aPauseOnExit; + NotifyCueUpdated(nullptr); + } + + TextTrackRegion* GetRegion(); + void SetRegion(TextTrackRegion* aRegion); + + DirectionSetting Vertical() const { return mVertical; } + + void SetVertical(const DirectionSetting& aVertical) { + if (mVertical == aVertical) { + return; + } + + mReset = true; + mVertical = aVertical; + } + + bool SnapToLines() { return mSnapToLines; } + + void SetSnapToLines(bool aSnapToLines) { + if (mSnapToLines == aSnapToLines) { + return; + } + + mReset = true; + mSnapToLines = aSnapToLines; + } + + void GetLine(OwningDoubleOrAutoKeyword& aLine) const { + if (mLineIsAutoKeyword) { + aLine.SetAsAutoKeyword() = AutoKeyword::Auto; + return; + } + aLine.SetAsDouble() = mLine; + } + + void SetLine(const DoubleOrAutoKeyword& aLine) { + if (aLine.IsDouble() && + (mLineIsAutoKeyword || (aLine.GetAsDouble() != mLine))) { + mLineIsAutoKeyword = false; + mLine = aLine.GetAsDouble(); + mReset = true; + return; + } + if (aLine.IsAutoKeyword() && !mLineIsAutoKeyword) { + mLineIsAutoKeyword = true; + mReset = true; + } + } + + LineAlignSetting LineAlign() const { return mLineAlign; } + + void SetLineAlign(LineAlignSetting& aLineAlign, ErrorResult& aRv) { + if (mLineAlign == aLineAlign) { + return; + } + + mReset = true; + mLineAlign = aLineAlign; + } + + void GetPosition(OwningDoubleOrAutoKeyword& aPosition) const { + if (mPositionIsAutoKeyword) { + aPosition.SetAsAutoKeyword() = AutoKeyword::Auto; + return; + } + aPosition.SetAsDouble() = mPosition; + } + + void SetPosition(const DoubleOrAutoKeyword& aPosition, ErrorResult& aRv) { + if (!aPosition.IsAutoKeyword() && + (aPosition.GetAsDouble() > 100.0 || aPosition.GetAsDouble() < 0.0)) { + aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); + return; + } + + if (aPosition.IsDouble() && + (mPositionIsAutoKeyword || (aPosition.GetAsDouble() != mPosition))) { + mPositionIsAutoKeyword = false; + mPosition = aPosition.GetAsDouble(); + mReset = true; + return; + } + + if (aPosition.IsAutoKeyword() && !mPositionIsAutoKeyword) { + mPositionIsAutoKeyword = true; + mReset = true; + } + } + + PositionAlignSetting PositionAlign() const { return mPositionAlign; } + + void SetPositionAlign(PositionAlignSetting aPositionAlign, ErrorResult& aRv) { + if (mPositionAlign == aPositionAlign) { + return; + } + + mReset = true; + mPositionAlign = aPositionAlign; + } + + double Size() const { return mSize; } + + void SetSize(double aSize, ErrorResult& aRv) { + if (mSize == aSize) { + return; + } + + if (aSize < 0.0 || aSize > 100.0) { + aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); + return; + } + + mReset = true; + mSize = aSize; + } + + AlignSetting Align() const { return mAlign; } + + void SetAlign(AlignSetting& aAlign) { + if (mAlign == aAlign) { + return; + } + + mReset = true; + mAlign = aAlign; + } + + void GetText(nsAString& aText) const { aText = mText; } + + void SetText(const nsAString& aText) { + if (mText == aText) { + return; + } + + mReset = true; + mText = aText; + } + + IMPL_EVENT_HANDLER(enter) + IMPL_EVENT_HANDLER(exit) + + HTMLDivElement* GetDisplayState() { + return static_cast<HTMLDivElement*>(mDisplayState.get()); + } + + void SetDisplayState(HTMLDivElement* aDisplayState) { + mDisplayState = aDisplayState; + mReset = false; + } + + void Reset() { mReset = true; } + + bool HasBeenReset() { return mReset; } + + double ComputedLine(); + double ComputedPosition(); + PositionAlignSetting ComputedPositionAlign(); + + // Helper functions for implementation. + const nsAString& Id() const { return mId; } + + void SetTrack(TextTrack* aTextTrack) { + mTrack = aTextTrack; + if (!mHaveStartedWatcher && aTextTrack) { + mHaveStartedWatcher = true; + mWatchManager.Watch(mReset, &TextTrackCue::NotifyDisplayStatesChanged); + } else if (mHaveStartedWatcher && !aTextTrack) { + mHaveStartedWatcher = false; + mWatchManager.Unwatch(mReset, &TextTrackCue::NotifyDisplayStatesChanged); + } + } + + /** + * Produces a tree of anonymous content based on the tree of the processed + * cue text. + * + * Returns a DocumentFragment that is the head of the tree of anonymous + * content. + */ + already_AddRefed<DocumentFragment> GetCueAsHTML(); + + void SetTrackElement(HTMLTrackElement* aTrackElement); + + void SetActive(bool aActive); + + bool GetActive() { return mActive; } + + private: + ~TextTrackCue(); + + void NotifyCueUpdated(TextTrackCue* aCue) { + if (mTrack) { + mTrack->NotifyCueUpdated(aCue); + } + } + + void NotifyDisplayStatesChanged(); + + void SetDefaultCueSettings(); + nsresult StashDocument(); + + bool IsTextBaseDirectionLTR() const; + + RefPtr<Document> mDocument; + nsString mText; + double mStartTime; + double mEndTime; + + RefPtr<TextTrack> mTrack; + RefPtr<HTMLTrackElement> mTrackElement; + nsString mId; + double mPosition; + bool mPositionIsAutoKeyword; + PositionAlignSetting mPositionAlign; + double mSize; + bool mPauseOnExit; + bool mSnapToLines; + RefPtr<TextTrackRegion> mRegion; + DirectionSetting mVertical; + bool mLineIsAutoKeyword; + double mLine; + AlignSetting mAlign; + LineAlignSetting mLineAlign; + + // Holds the computed DOM elements that represent the parsed cue text. + // http://www.whatwg.org/specs/web-apps/current-work/#text-track-cue-display-state + RefPtr<nsGenericHTMLElement> mDisplayState; + // Tells whether or not we need to recompute mDisplayState. This is set + // anytime a property that relates to the display of the TextTrackCue is + // changed. + Watchable<bool> mReset; + + bool mActive; + + static StaticRefPtr<nsIWebVTTParserWrapper> sParserWrapper; + + // Only start watcher after the cue has text track. + bool mHaveStartedWatcher; + WatchManager<TextTrackCue> mWatchManager; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_TextTrackCue_h diff --git a/dom/media/webvtt/TextTrackCueList.cpp b/dom/media/webvtt/TextTrackCueList.cpp new file mode 100644 index 0000000000..d6fb8baedc --- /dev/null +++ b/dom/media/webvtt/TextTrackCueList.cpp @@ -0,0 +1,125 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/TextTrackCueList.h" +#include "mozilla/dom/TextTrackCueListBinding.h" +#include "mozilla/dom/TextTrackCue.h" + +namespace mozilla::dom { + +class CompareCuesByTime { + public: + bool Equals(TextTrackCue* aOne, TextTrackCue* aTwo) const { return false; } + bool LessThan(TextTrackCue* aOne, TextTrackCue* aTwo) const { + return aOne->StartTime() < aTwo->StartTime() || + (aOne->StartTime() == aTwo->StartTime() && + aOne->EndTime() >= aTwo->EndTime()); + } +}; + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(TextTrackCueList, mParent, mList) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(TextTrackCueList) +NS_IMPL_CYCLE_COLLECTING_RELEASE(TextTrackCueList) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TextTrackCueList) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +TextTrackCueList::TextTrackCueList(nsISupports* aParent) : mParent(aParent) {} + +TextTrackCueList::~TextTrackCueList() = default; + +JSObject* TextTrackCueList::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return TextTrackCueList_Binding::Wrap(aCx, this, aGivenProto); +} + +TextTrackCue* TextTrackCueList::IndexedGetter(uint32_t aIndex, bool& aFound) { + aFound = aIndex < mList.Length(); + if (!aFound) { + return nullptr; + } + return mList[aIndex]; +} + +TextTrackCue* TextTrackCueList::operator[](uint32_t aIndex) { + return mList.SafeElementAt(aIndex, nullptr); +} + +TextTrackCueList& TextTrackCueList::operator=(const TextTrackCueList& aOther) { + mList = aOther.mList.Clone(); + return *this; +} + +TextTrackCue* TextTrackCueList::GetCueById(const nsAString& aId) { + if (aId.IsEmpty()) { + return nullptr; + } + + for (uint32_t i = 0; i < mList.Length(); i++) { + if (aId.Equals(mList[i]->Id())) { + return mList[i]; + } + } + return nullptr; +} + +void TextTrackCueList::AddCue(TextTrackCue& aCue) { + if (mList.Contains(&aCue)) { + return; + } + mList.InsertElementSorted(&aCue, CompareCuesByTime()); +} + +void TextTrackCueList::RemoveCue(TextTrackCue& aCue, ErrorResult& aRv) { + if (!mList.Contains(&aCue)) { + aRv.Throw(NS_ERROR_DOM_NOT_FOUND_ERR); + return; + } + mList.RemoveElement(&aCue); +} + +void TextTrackCueList::RemoveCue(TextTrackCue& aCue) { + mList.RemoveElement(&aCue); +} + +void TextTrackCueList::RemoveCueAt(uint32_t aIndex) { + if (aIndex < mList.Length()) { + mList.RemoveElementAt(aIndex); + } +} + +void TextTrackCueList::RemoveAll() { mList.Clear(); } + +void TextTrackCueList::GetArray(nsTArray<RefPtr<TextTrackCue>>& aCues) { + aCues = mList.Clone(); +} + +void TextTrackCueList::SetCuesInactive() { + for (uint32_t i = 0; i < mList.Length(); ++i) { + mList[i]->SetActive(false); + } +} + +void TextTrackCueList::NotifyCueUpdated(TextTrackCue* aCue) { + if (aCue) { + mList.RemoveElement(aCue); + mList.InsertElementSorted(aCue, CompareCuesByTime()); + } +} + +bool TextTrackCueList::IsCueExist(TextTrackCue* aCue) { + if (aCue && mList.Contains(aCue)) { + return true; + } + return false; +} + +nsTArray<RefPtr<TextTrackCue>>& TextTrackCueList::GetCuesArray() { + return mList; +} + +} // namespace mozilla::dom diff --git a/dom/media/webvtt/TextTrackCueList.h b/dom/media/webvtt/TextTrackCueList.h new file mode 100644 index 0000000000..f590f94d8c --- /dev/null +++ b/dom/media/webvtt/TextTrackCueList.h @@ -0,0 +1,73 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 et tw=78: */ +/* 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/. */ + +#ifndef mozilla_dom_TextTrackCueList_h +#define mozilla_dom_TextTrackCueList_h + +#include "nsTArray.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class TextTrackCue; + +class TextTrackCueList final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(TextTrackCueList) + + // TextTrackCueList WebIDL + explicit TextTrackCueList(nsISupports* aParent); + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + nsISupports* GetParentObject() const { return mParent; } + + uint32_t Length() const { return mList.Length(); } + + bool IsEmpty() const { return mList.Length() == 0; } + + TextTrackCue* IndexedGetter(uint32_t aIndex, bool& aFound); + TextTrackCue* operator[](uint32_t aIndex); + TextTrackCue* GetCueById(const nsAString& aId); + TextTrackCueList& operator=(const TextTrackCueList& aOther); + // Adds a cue to mList by performing an insertion sort on mList. + // We expect most files to already be sorted, so an insertion sort starting + // from the end of the current array should be more efficient than a general + // sort step after all cues are loaded. + void AddCue(TextTrackCue& aCue); + void RemoveCue(TextTrackCue& aCue); + void RemoveCue(TextTrackCue& aCue, ErrorResult& aRv); + void RemoveCueAt(uint32_t aIndex); + void RemoveAll(); + void GetArray(nsTArray<RefPtr<TextTrackCue>>& aCues); + + void SetCuesInactive(); + + void NotifyCueUpdated(TextTrackCue* aCue); + bool IsCueExist(TextTrackCue* aCue); + nsTArray<RefPtr<TextTrackCue>>& GetCuesArray(); + + private: + ~TextTrackCueList(); + + nsCOMPtr<nsISupports> mParent; + + // A sorted list of TextTrackCues sorted by earliest start time. If the start + // times are equal then it will be sorted by end time, earliest first. + nsTArray<RefPtr<TextTrackCue>> mList; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_TextTrackCueList_h diff --git a/dom/media/webvtt/TextTrackList.cpp b/dom/media/webvtt/TextTrackList.cpp new file mode 100644 index 0000000000..d5611bdaf9 --- /dev/null +++ b/dom/media/webvtt/TextTrackList.cpp @@ -0,0 +1,192 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/TextTrackList.h" + +#include "mozilla/DebugOnly.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/TextTrackListBinding.h" +#include "mozilla/dom/TrackEvent.h" +#include "nsThreadUtils.h" +#include "nsGlobalWindow.h" +#include "mozilla/dom/TextTrackCue.h" +#include "mozilla/dom/TextTrackManager.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(TextTrackList, DOMEventTargetHelper, + mTextTracks, mTextTrackManager) + +NS_IMPL_ADDREF_INHERITED(TextTrackList, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(TextTrackList, DOMEventTargetHelper) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TextTrackList) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +TextTrackList::TextTrackList(nsPIDOMWindowInner* aOwnerWindow) + : DOMEventTargetHelper(aOwnerWindow) {} + +TextTrackList::TextTrackList(nsPIDOMWindowInner* aOwnerWindow, + TextTrackManager* aTextTrackManager) + : DOMEventTargetHelper(aOwnerWindow), + mTextTrackManager(aTextTrackManager) {} + +TextTrackList::~TextTrackList() = default; + +void TextTrackList::GetShowingCues(nsTArray<RefPtr<TextTrackCue>>& aCues) { + // Only Subtitles and Captions can show on the screen. + nsTArray<RefPtr<TextTrackCue>> cues; + for (uint32_t i = 0; i < Length(); i++) { + if (mTextTracks[i]->Mode() == TextTrackMode::Showing && + (mTextTracks[i]->Kind() == TextTrackKind::Subtitles || + mTextTracks[i]->Kind() == TextTrackKind::Captions)) { + mTextTracks[i]->GetActiveCueArray(cues); + aCues.AppendElements(cues); + } + } +} + +JSObject* TextTrackList::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return TextTrackList_Binding::Wrap(aCx, this, aGivenProto); +} + +TextTrack* TextTrackList::IndexedGetter(uint32_t aIndex, bool& aFound) { + aFound = aIndex < mTextTracks.Length(); + if (!aFound) { + return nullptr; + } + return mTextTracks[aIndex]; +} + +TextTrack* TextTrackList::operator[](uint32_t aIndex) { + return mTextTracks.SafeElementAt(aIndex, nullptr); +} + +already_AddRefed<TextTrack> TextTrackList::AddTextTrack( + TextTrackKind aKind, const nsAString& aLabel, const nsAString& aLanguage, + TextTrackMode aMode, TextTrackReadyState aReadyState, + TextTrackSource aTextTrackSource, const CompareTextTracks& aCompareTT) { + RefPtr<TextTrack> track = + new TextTrack(GetOwner(), this, aKind, aLabel, aLanguage, aMode, + aReadyState, aTextTrackSource); + AddTextTrack(track, aCompareTT); + return track.forget(); +} + +void TextTrackList::AddTextTrack(TextTrack* aTextTrack, + const CompareTextTracks& aCompareTT) { + if (mTextTracks.Contains(aTextTrack)) { + return; + } + mTextTracks.InsertElementSorted(aTextTrack, aCompareTT); + aTextTrack->SetTextTrackList(this); + CreateAndDispatchTrackEventRunner(aTextTrack, u"addtrack"_ns); +} + +TextTrack* TextTrackList::GetTrackById(const nsAString& aId) { + nsAutoString id; + for (uint32_t i = 0; i < Length(); i++) { + mTextTracks[i]->GetId(id); + if (aId.Equals(id)) { + return mTextTracks[i]; + } + } + return nullptr; +} + +void TextTrackList::RemoveTextTrack(TextTrack* aTrack) { + if (mTextTracks.RemoveElement(aTrack)) { + CreateAndDispatchTrackEventRunner(aTrack, u"removetrack"_ns); + } +} + +class TrackEventRunner : public Runnable { + public: + TrackEventRunner(TextTrackList* aList, Event* aEvent) + : Runnable("dom::TrackEventRunner"), mList(aList), mEvent(aEvent) {} + + NS_IMETHOD Run() override { return mList->DispatchTrackEvent(mEvent); } + + RefPtr<TextTrackList> mList; + + private: + RefPtr<Event> mEvent; +}; + +nsresult TextTrackList::DispatchTrackEvent(Event* aEvent) { + return DispatchTrustedEvent(aEvent); +} + +void TextTrackList::CreateAndDispatchChangeEvent() { + MOZ_ASSERT(NS_IsMainThread()); + nsPIDOMWindowInner* win = GetOwner(); + if (!win) { + return; + } + + RefPtr<Event> event = NS_NewDOMEvent(this, nullptr, nullptr); + + event->InitEvent(u"change"_ns, false, false); + event->SetTrusted(true); + + nsCOMPtr<nsIRunnable> eventRunner = new TrackEventRunner(this, event); + nsGlobalWindowInner::Cast(win)->Dispatch(TaskCategory::Other, + eventRunner.forget()); +} + +void TextTrackList::CreateAndDispatchTrackEventRunner( + TextTrack* aTrack, const nsAString& aEventName) { + DebugOnly<nsresult> rv; + nsCOMPtr<nsIEventTarget> target = GetMainThreadSerialEventTarget(); + if (!target) { + // If we are not able to get the main-thread object we are shutting down. + return; + } + + TrackEventInit eventInit; + eventInit.mTrack.SetValue().SetAsTextTrack() = aTrack; + RefPtr<TrackEvent> event = + TrackEvent::Constructor(this, aEventName, eventInit); + + // Dispatch the TrackEvent asynchronously. + rv = target->Dispatch(do_AddRef(new TrackEventRunner(this, event)), + NS_DISPATCH_NORMAL); + + // If we are shutting down this can file but it's still ok. + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Dispatch failed"); +} + +HTMLMediaElement* TextTrackList::GetMediaElement() { + if (mTextTrackManager) { + return mTextTrackManager->mMediaElement; + } + return nullptr; +} + +void TextTrackList::SetTextTrackManager(TextTrackManager* aTextTrackManager) { + mTextTrackManager = aTextTrackManager; +} + +void TextTrackList::SetCuesInactive() { + for (uint32_t i = 0; i < Length(); i++) { + mTextTracks[i]->SetCuesInactive(); + } +} + +bool TextTrackList::AreTextTracksLoaded() { + // Return false if any texttrack is not loaded. + for (uint32_t i = 0; i < Length(); i++) { + if (!mTextTracks[i]->IsLoaded()) { + return false; + } + } + return true; +} + +nsTArray<RefPtr<TextTrack>>& TextTrackList::GetTextTrackArray() { + return mTextTracks; +} + +} // namespace mozilla::dom diff --git a/dom/media/webvtt/TextTrackList.h b/dom/media/webvtt/TextTrackList.h new file mode 100644 index 0000000000..712e3ae9c4 --- /dev/null +++ b/dom/media/webvtt/TextTrackList.h @@ -0,0 +1,79 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 et tw=78: */ +/* 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/. */ + +#ifndef mozilla_dom_TextTrackList_h +#define mozilla_dom_TextTrackList_h + +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/TextTrack.h" +#include "nsCycleCollectionParticipant.h" + +namespace mozilla::dom { + +class Event; +class HTMLMediaElement; +class TextTrackManager; +class CompareTextTracks; +class TrackEvent; +class TrackEventRunner; + +class TextTrackList final : public DOMEventTargetHelper { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(TextTrackList, DOMEventTargetHelper) + + explicit TextTrackList(nsPIDOMWindowInner* aOwnerWindow); + TextTrackList(nsPIDOMWindowInner* aOwnerWindow, + TextTrackManager* aTextTrackManager); + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + uint32_t Length() const { return mTextTracks.Length(); } + + // Get all the current active cues. + void GetShowingCues(nsTArray<RefPtr<TextTrackCue>>& aCues); + + TextTrack* IndexedGetter(uint32_t aIndex, bool& aFound); + TextTrack* operator[](uint32_t aIndex); + + already_AddRefed<TextTrack> AddTextTrack( + TextTrackKind aKind, const nsAString& aLabel, const nsAString& aLanguage, + TextTrackMode aMode, TextTrackReadyState aReadyState, + TextTrackSource aTextTrackSource, const CompareTextTracks& aCompareTT); + TextTrack* GetTrackById(const nsAString& aId); + + void AddTextTrack(TextTrack* aTextTrack, const CompareTextTracks& aCompareTT); + + void RemoveTextTrack(TextTrack* aTrack); + + HTMLMediaElement* GetMediaElement(); + void SetTextTrackManager(TextTrackManager* aTextTrackManager); + + nsresult DispatchTrackEvent(Event* aEvent); + void CreateAndDispatchChangeEvent(); + void SetCuesInactive(); + + bool AreTextTracksLoaded(); + nsTArray<RefPtr<TextTrack>>& GetTextTrackArray(); + + IMPL_EVENT_HANDLER(change) + IMPL_EVENT_HANDLER(addtrack) + IMPL_EVENT_HANDLER(removetrack) + + private: + ~TextTrackList(); + + nsTArray<RefPtr<TextTrack>> mTextTracks; + RefPtr<TextTrackManager> mTextTrackManager; + + void CreateAndDispatchTrackEventRunner(TextTrack* aTrack, + const nsAString& aEventName); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_TextTrackList_h diff --git a/dom/media/webvtt/TextTrackRegion.cpp b/dom/media/webvtt/TextTrackRegion.cpp new file mode 100644 index 0000000000..d883659579 --- /dev/null +++ b/dom/media/webvtt/TextTrackRegion.cpp @@ -0,0 +1,58 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 et tw=78: */ +/* 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/TextTrackRegion.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(TextTrackRegion, mParent) +NS_IMPL_CYCLE_COLLECTING_ADDREF(TextTrackRegion) +NS_IMPL_CYCLE_COLLECTING_RELEASE(TextTrackRegion) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TextTrackRegion) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +JSObject* TextTrackRegion::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return VTTRegion_Binding::Wrap(aCx, this, aGivenProto); +} + +already_AddRefed<TextTrackRegion> TextTrackRegion::Constructor( + const GlobalObject& aGlobal, ErrorResult& aRv) { + nsCOMPtr<nsPIDOMWindowInner> window = + do_QueryInterface(aGlobal.GetAsSupports()); + if (!window) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr<TextTrackRegion> region = new TextTrackRegion(aGlobal.GetAsSupports()); + return region.forget(); +} + +TextTrackRegion::TextTrackRegion(nsISupports* aGlobal) + : mParent(aGlobal), + mWidth(100), + mLines(3), + mRegionAnchorX(0), + mRegionAnchorY(100), + mViewportAnchorX(0), + mViewportAnchorY(100), + mScroll(ScrollSetting::_empty) {} + +void TextTrackRegion::CopyValues(TextTrackRegion& aRegion) { + mId = aRegion.Id(); + mWidth = aRegion.Width(); + mLines = aRegion.Lines(); + mRegionAnchorX = aRegion.RegionAnchorX(); + mRegionAnchorY = aRegion.RegionAnchorY(); + mViewportAnchorX = aRegion.ViewportAnchorX(); + mViewportAnchorY = aRegion.ViewportAnchorY(); + mScroll = aRegion.Scroll(); +} + +} // namespace mozilla::dom diff --git a/dom/media/webvtt/TextTrackRegion.h b/dom/media/webvtt/TextTrackRegion.h new file mode 100644 index 0000000000..d316d7a30c --- /dev/null +++ b/dom/media/webvtt/TextTrackRegion.h @@ -0,0 +1,138 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 et tw=78: */ +/* 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/. */ + +#ifndef mozilla_dom_TextTrackRegion_h +#define mozilla_dom_TextTrackRegion_h + +#include "nsCycleCollectionParticipant.h" +#include "nsString.h" +#include "nsWrapperCache.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/TextTrack.h" +#include "mozilla/dom/VTTRegionBinding.h" +#include "mozilla/Preferences.h" + +namespace mozilla::dom { + +class GlobalObject; +class TextTrack; + +class TextTrackRegion final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(TextTrackRegion) + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + nsISupports* GetParentObject() const { return mParent; } + + explicit TextTrackRegion(nsISupports* aGlobal); + + /** WebIDL Methods. */ + + static already_AddRefed<TextTrackRegion> Constructor( + const GlobalObject& aGlobal, ErrorResult& aRv); + + double Lines() const { return mLines; } + + void SetLines(double aLines, ErrorResult& aRv) { + if (aLines < 0) { + aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); + } else { + mLines = aLines; + } + } + + double Width() const { return mWidth; } + + void SetWidth(double aWidth, ErrorResult& aRv) { + if (!InvalidValue(aWidth, aRv)) { + mWidth = aWidth; + } + } + + double RegionAnchorX() const { return mRegionAnchorX; } + + void SetRegionAnchorX(double aVal, ErrorResult& aRv) { + if (!InvalidValue(aVal, aRv)) { + mRegionAnchorX = aVal; + } + } + + double RegionAnchorY() const { return mRegionAnchorY; } + + void SetRegionAnchorY(double aVal, ErrorResult& aRv) { + if (!InvalidValue(aVal, aRv)) { + mRegionAnchorY = aVal; + } + } + + double ViewportAnchorX() const { return mViewportAnchorX; } + + void SetViewportAnchorX(double aVal, ErrorResult& aRv) { + if (!InvalidValue(aVal, aRv)) { + mViewportAnchorX = aVal; + } + } + + double ViewportAnchorY() const { return mViewportAnchorY; } + + void SetViewportAnchorY(double aVal, ErrorResult& aRv) { + if (!InvalidValue(aVal, aRv)) { + mViewportAnchorY = aVal; + } + } + + ScrollSetting Scroll() const { return mScroll; } + + void SetScroll(const ScrollSetting& aScroll) { + if (aScroll == ScrollSetting::_empty || aScroll == ScrollSetting::Up) { + mScroll = aScroll; + } + } + + void GetId(nsAString& aId) const { aId = mId; } + + void SetId(const nsAString& aId) { mId = aId; } + + /** end WebIDL Methods. */ + + // Helper to aid copying of a given TextTrackRegion's width, lines, + // anchor, viewport and scroll values. + void CopyValues(TextTrackRegion& aRegion); + + // -----helpers------- + const nsAString& Id() const { return mId; } + + private: + ~TextTrackRegion() = default; + + nsCOMPtr<nsISupports> mParent; + nsString mId; + double mWidth; + long mLines; + double mRegionAnchorX; + double mRegionAnchorY; + double mViewportAnchorX; + double mViewportAnchorY; + ScrollSetting mScroll; + + // Helper to ensure new value is in the range: 0.0% - 100.0%; throws + // an IndexSizeError otherwise. + inline bool InvalidValue(double aValue, ErrorResult& aRv) { + if (aValue < 0.0 || aValue > 100.0) { + aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); + return true; + } + + return false; + } +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_TextTrackRegion_h diff --git a/dom/media/webvtt/WebVTTListener.cpp b/dom/media/webvtt/WebVTTListener.cpp new file mode 100644 index 0000000000..3f8d99a8f3 --- /dev/null +++ b/dom/media/webvtt/WebVTTListener.cpp @@ -0,0 +1,212 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "WebVTTListener.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/HTMLTrackElement.h" +#include "mozilla/dom/TextTrackCue.h" +#include "mozilla/dom/TextTrackRegion.h" +#include "mozilla/dom/VTTRegionBinding.h" +#include "nsComponentManagerUtils.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsIInputStream.h" + +extern mozilla::LazyLogModule gTextTrackLog; +#define LOG(msg, ...) \ + MOZ_LOG(gTextTrackLog, LogLevel::Debug, \ + ("WebVTTListener=%p, " msg, this, ##__VA_ARGS__)) +#define LOG_WIHTOUT_ADDRESS(msg, ...) \ + MOZ_LOG(gTextTrackLog, LogLevel::Debug, (msg, ##__VA_ARGS__)) + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION(WebVTTListener, mElement, mParserWrapper) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WebVTTListener) + NS_INTERFACE_MAP_ENTRY(nsIWebVTTListener) + NS_INTERFACE_MAP_ENTRY(nsIStreamListener) + NS_INTERFACE_MAP_ENTRY(nsIChannelEventSink) + NS_INTERFACE_MAP_ENTRY(nsIInterfaceRequestor) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIWebVTTListener) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(WebVTTListener) +NS_IMPL_CYCLE_COLLECTING_RELEASE(WebVTTListener) + +WebVTTListener::WebVTTListener(HTMLTrackElement* aElement) + : mElement(aElement), mParserWrapperError(NS_OK) { + MOZ_ASSERT(mElement, "Must pass an element to the callback"); + LOG("Created listener for track element %p", aElement); + MOZ_DIAGNOSTIC_ASSERT( + CycleCollectedJSContext::Get() && + !CycleCollectedJSContext::Get()->IsInStableOrMetaStableState()); + mParserWrapper = do_CreateInstance(NS_WEBVTTPARSERWRAPPER_CONTRACTID, + &mParserWrapperError); + if (NS_SUCCEEDED(mParserWrapperError)) { + nsPIDOMWindowInner* window = mElement->OwnerDoc()->GetInnerWindow(); + mParserWrapperError = mParserWrapper->LoadParser(window); + } + if (NS_SUCCEEDED(mParserWrapperError)) { + mParserWrapperError = mParserWrapper->Watch(this); + } +} + +WebVTTListener::~WebVTTListener() { LOG("destroyed."); } + +NS_IMETHODIMP +WebVTTListener::GetInterface(const nsIID& aIID, void** aResult) { + return QueryInterface(aIID, aResult); +} + +nsresult WebVTTListener::LoadResource() { + if (IsCanceled()) { + return NS_OK; + } + // Exit if we failed to create the WebVTTParserWrapper (vtt.jsm) + NS_ENSURE_SUCCESS(mParserWrapperError, mParserWrapperError); + + mElement->SetReadyState(TextTrackReadyState::Loading); + return NS_OK; +} + +NS_IMETHODIMP +WebVTTListener::AsyncOnChannelRedirect(nsIChannel* aOldChannel, + nsIChannel* aNewChannel, uint32_t aFlags, + nsIAsyncVerifyRedirectCallback* cb) { + if (IsCanceled()) { + return NS_OK; + } + if (mElement) { + mElement->OnChannelRedirect(aOldChannel, aNewChannel, aFlags); + } + cb->OnRedirectVerifyCallback(NS_OK); + return NS_OK; +} + +NS_IMETHODIMP +WebVTTListener::OnStartRequest(nsIRequest* aRequest) { + if (IsCanceled()) { + return NS_OK; + } + + LOG("OnStartRequest"); + mElement->DispatchTestEvent(u"mozStartedLoadingTextTrack"_ns); + return NS_OK; +} + +NS_IMETHODIMP +WebVTTListener::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) { + if (IsCanceled()) { + return NS_OK; + } + + LOG("OnStopRequest"); + if (NS_FAILED(aStatus)) { + LOG("Got error status"); + mElement->SetReadyState(TextTrackReadyState::FailedToLoad); + } + // Attempt to parse any final data the parser might still have. + mParserWrapper->Flush(); + if (mElement->ReadyState() != TextTrackReadyState::FailedToLoad) { + mElement->SetReadyState(TextTrackReadyState::Loaded); + } + + mElement->CancelChannelAndListener(); + + return aStatus; +} + +nsresult WebVTTListener::ParseChunk(nsIInputStream* aInStream, void* aClosure, + const char* aFromSegment, + uint32_t aToOffset, uint32_t aCount, + uint32_t* aWriteCount) { + nsCString buffer(aFromSegment, aCount); + WebVTTListener* listener = static_cast<WebVTTListener*>(aClosure); + MOZ_ASSERT(!listener->IsCanceled()); + + if (NS_FAILED(listener->mParserWrapper->Parse(buffer))) { + LOG_WIHTOUT_ADDRESS( + "WebVTTListener=%p, Unable to parse chunk of WEBVTT text. Aborting.", + listener); + *aWriteCount = 0; + return NS_ERROR_FAILURE; + } + + *aWriteCount = aCount; + return NS_OK; +} + +NS_IMETHODIMP +WebVTTListener::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aStream, + uint64_t aOffset, uint32_t aCount) { + if (IsCanceled()) { + return NS_OK; + } + + LOG("OnDataAvailable"); + uint32_t count = aCount; + while (count > 0) { + uint32_t read; + nsresult rv = aStream->ReadSegments(ParseChunk, this, count, &read); + NS_ENSURE_SUCCESS(rv, rv); + if (!read) { + return NS_ERROR_FAILURE; + } + count -= read; + } + + return NS_OK; +} + +NS_IMETHODIMP +WebVTTListener::OnCue(JS::Handle<JS::Value> aCue, JSContext* aCx) { + MOZ_ASSERT(!IsCanceled()); + if (!aCue.isObject()) { + return NS_ERROR_FAILURE; + } + + JS::Rooted<JSObject*> obj(aCx, &aCue.toObject()); + TextTrackCue* cue = nullptr; + nsresult rv = UNWRAP_OBJECT(VTTCue, &obj, cue); + NS_ENSURE_SUCCESS(rv, rv); + + cue->SetTrackElement(mElement); + mElement->mTrack->AddCue(*cue); + + return NS_OK; +} + +NS_IMETHODIMP +WebVTTListener::OnRegion(JS::Handle<JS::Value> aRegion, JSContext* aCx) { + MOZ_ASSERT(!IsCanceled()); + // Nothing for this callback to do. + return NS_OK; +} + +NS_IMETHODIMP +WebVTTListener::OnParsingError(int32_t errorCode, JSContext* cx) { + MOZ_ASSERT(!IsCanceled()); + // We only care about files that have a bad WebVTT file signature right now + // as that means the file failed to load. + if (errorCode == ErrorCodes::BadSignature) { + LOG("parsing error"); + mElement->SetReadyState(TextTrackReadyState::FailedToLoad); + } + return NS_OK; +} + +bool WebVTTListener::IsCanceled() const { return mCancel; } + +void WebVTTListener::Cancel() { + MOZ_ASSERT(!IsCanceled(), "Do not cancel canceled listener again!"); + LOG("Cancel listen to channel's response."); + mCancel = true; + mParserWrapper->Cancel(); + mParserWrapper = nullptr; + mElement = nullptr; +} + +} // namespace mozilla::dom diff --git a/dom/media/webvtt/WebVTTListener.h b/dom/media/webvtt/WebVTTListener.h new file mode 100644 index 0000000000..45f6bc90d6 --- /dev/null +++ b/dom/media/webvtt/WebVTTListener.h @@ -0,0 +1,69 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_dom_WebVTTLoadListener_h +#define mozilla_dom_WebVTTLoadListener_h + +#include "nsIWebVTTListener.h" +#include "nsIStreamListener.h" +#include "nsIChannelEventSink.h" +#include "nsIInterfaceRequestor.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" + +class nsIWebVTTParserWrapper; + +namespace mozilla::dom { + +class HTMLTrackElement; + +class WebVTTListener final : public nsIWebVTTListener, + public nsIStreamListener, + public nsIChannelEventSink, + public nsIInterfaceRequestor { + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_NSIWEBVTTLISTENER + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSICHANNELEVENTSINK + NS_DECL_NSIINTERFACEREQUESTOR + + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(WebVTTListener, nsIStreamListener) + + public: + explicit WebVTTListener(HTMLTrackElement* aElement); + + /** + * Loads the WebVTTListener. Must call this in order for the listener to be + * ready to parse data that is passed to it. + */ + nsresult LoadResource(); + + /** + * When this listener is not going to be used anymore, its owner should take + * a responsibility to call `Cancel()` to prevent this listener making any + * changes for the track element. + */ + bool IsCanceled() const; + void Cancel(); + + private: + ~WebVTTListener(); + + // List of error codes returned from the WebVTT parser that we care about. + enum ErrorCodes { BadSignature = 0 }; + static nsresult ParseChunk(nsIInputStream* aInStream, void* aClosure, + const char* aFromSegment, uint32_t aToOffset, + uint32_t aCount, uint32_t* aWriteCount); + + RefPtr<HTMLTrackElement> mElement; + nsCOMPtr<nsIWebVTTParserWrapper> mParserWrapper; + nsresult mParserWrapperError; + bool mCancel = false; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_WebVTTListener_h diff --git a/dom/media/webvtt/WebVTTParserWrapper.sys.mjs b/dom/media/webvtt/WebVTTParserWrapper.sys.mjs new file mode 100644 index 0000000000..19cf08ca55 --- /dev/null +++ b/dom/media/webvtt/WebVTTParserWrapper.sys.mjs @@ -0,0 +1,56 @@ +/* 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/. */ + +import { WebVTT } from "resource://gre/modules/vtt.sys.mjs"; + +export function WebVTTParserWrapper() { + // Nothing +} + +WebVTTParserWrapper.prototype = { + loadParser(window) { + this.parser = new WebVTT.Parser(window, new TextDecoder("utf8")); + }, + + parse(data) { + // We can safely translate the string data to a Uint8Array as we are + // guaranteed character codes only from \u0000 => \u00ff + var buffer = new Uint8Array(data.length); + for (var i = 0; i < data.length; i++) { + buffer[i] = data.charCodeAt(i); + } + + this.parser.parse(buffer); + }, + + flush() { + this.parser.flush(); + }, + + watch(callback) { + this.parser.oncue = callback.onCue; + this.parser.onregion = callback.onRegion; + this.parser.onparsingerror = function (e) { + // Passing the just the error code back is enough for our needs. + callback.onParsingError("code" in e ? e.code : -1); + }; + }, + + cancel() { + this.parser.oncue = null; + this.parser.onregion = null; + this.parser.onparsingerror = null; + }, + + convertCueToDOMTree(window, cue) { + return WebVTT.convertCueToDOMTree(window, cue.text); + }, + + processCues(window, cues, overlay, controls) { + WebVTT.processCues(window, cues, overlay, controls); + }, + + classDescription: "Wrapper for the JS WebVTT implementation (vtt.js)", + QueryInterface: ChromeUtils.generateQI(["nsIWebVTTParserWrapper"]), +}; diff --git a/dom/media/webvtt/components.conf b/dom/media/webvtt/components.conf new file mode 100644 index 0000000000..21fc95c13d --- /dev/null +++ b/dom/media/webvtt/components.conf @@ -0,0 +1,14 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +Classes = [ + { + 'cid': '{acf6e493-0092-4b26-b172-241e375c57ab}', + 'contract_ids': ['@mozilla.org/webvttParserWrapper;1'], + 'esModule': 'resource://gre/modules/WebVTTParserWrapper.sys.mjs', + 'constructor': 'WebVTTParserWrapper', + }, +] diff --git a/dom/media/webvtt/moz.build b/dom/media/webvtt/moz.build new file mode 100644 index 0000000000..6125406b7a --- /dev/null +++ b/dom/media/webvtt/moz.build @@ -0,0 +1,52 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +EXPORTS.mozilla.dom += [ + "TextTrack.h", + "TextTrackCue.h", + "TextTrackCueList.h", + "TextTrackList.h", + "TextTrackRegion.h", + "WebVTTListener.h", +] + +UNIFIED_SOURCES += [ + "TextTrack.cpp", + "TextTrackCue.cpp", + "TextTrackCueList.cpp", + "TextTrackList.cpp", + "TextTrackRegion.cpp", + "WebVTTListener.cpp", +] + +XPIDL_SOURCES += [ + "nsIWebVTTListener.idl", + "nsIWebVTTParserWrapper.idl", +] + +XPIDL_MODULE = "webvtt" + +EXTRA_JS_MODULES += [ + "WebVTTParserWrapper.sys.mjs", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +EXTRA_JS_MODULES += [ + "vtt.sys.mjs", +] + +XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.ini"] + +MOCHITEST_MANIFESTS += ["test/mochitest/mochitest.ini"] + +REFTEST_MANIFESTS += ["test/reftest/reftest.list"] + +CRASHTEST_MANIFESTS += ["test/crashtests/crashtests.list"] + +FINAL_LIBRARY = "xul" diff --git a/dom/media/webvtt/nsIWebVTTListener.idl b/dom/media/webvtt/nsIWebVTTListener.idl new file mode 100644 index 0000000000..b99d4cf517 --- /dev/null +++ b/dom/media/webvtt/nsIWebVTTListener.idl @@ -0,0 +1,37 @@ +/* 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 "nsISupports.idl" + +/** + * Listener for a JS WebVTT parser (vtt.js). + */ +[scriptable, uuid(8a2d7780-2045-4a29-99f4-df15cae5fc49)] +interface nsIWebVTTListener : nsISupports +{ + /** + * Is called when the WebVTTParser successfully parses a WebVTT cue. + * + * @param cue An object representing the data of a parsed WebVTT cue. + */ + [implicit_jscontext] + void onCue(in jsval cue); + + /** + * Is called when the WebVTT parser successfully parses a WebVTT region. + * + * @param region An object representing the data of a parsed + * WebVTT region. + */ + [implicit_jscontext] + void onRegion(in jsval region); + + /** + * Is called when the WebVTT parser encounters a parsing error. + * + * @param error The error code of the ParserError the occured. + */ + [implicit_jscontext] + void onParsingError(in long errorCode); +}; diff --git a/dom/media/webvtt/nsIWebVTTParserWrapper.idl b/dom/media/webvtt/nsIWebVTTParserWrapper.idl new file mode 100644 index 0000000000..76725d568b --- /dev/null +++ b/dom/media/webvtt/nsIWebVTTParserWrapper.idl @@ -0,0 +1,94 @@ +/* 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 "nsISupports.idl" + +interface nsIWebVTTListener; +interface mozIDOMWindow; +interface nsIVariant; + +webidl DocumentFragment; + +/** + * Interface for a wrapper of a JS WebVTT parser (vtt.js). + */ +[scriptable, uuid(8dfe016e-1701-4618-9f5e-9a6154e853f0)] +interface nsIWebVTTParserWrapper : nsISupports +{ + /** + * Loads the JS WebVTTParser and sets it to use the passed window to create + * VTTRegions and VTTCues. This function must be called before calling + * parse, flush, or watch. + * + * @param window The window that the parser will use to create VTTCues and + * VTTRegions. + * + */ + void loadParser(in mozIDOMWindow window); + + /** + * Attempts to parse the stream's data as WebVTT format. When it successfully + * parses a WebVTT region or WebVTT cue it will create a VTTRegion or VTTCue + * object and pass it back to the callee through its callbacks. + * + * @param data The buffer that contains the WebVTT data received by the + * Necko consumer so far. + */ + void parse(in ACString data); + + /** + * Flush indicates that no more data is expected from the stream. As such the + * parser should try to parse any kind of partial data it has. + */ + void flush(); + + /** + * Set this parser object to use an nsIWebVTTListener object for its onCue + * and onRegion callbacks. + * + * @param callback The nsIWebVTTListener object that exposes onCue and + * onRegion callbacks for the parser. + */ + void watch(in nsIWebVTTListener callback); + + /** + * Cancel watching notifications which parser would send. + */ + void cancel(); + + /** + * Convert the text content of a WebVTT cue to a document fragment so that + * we can display it on the page. + * + * @param window A window object with which the document fragment will be + * created. + * @param cue The cue whose content will be converted to a document + * fragment. + */ + DocumentFragment convertCueToDOMTree(in mozIDOMWindow window, + in nsISupports cue); + + + /** + * Compute the display state of the VTTCues in cues along with any VTTRegions + * that they might be in. First, it computes the positioning and styling of + * the cues and regions passed and converts them into a DOM tree rooted at + * a containing HTMLDivElement. It then adjusts those computed divs for + * overlap avoidance using the dimensions of 'overlay'. Finally, it adds the + * computed divs to the VTTCues display state property for use later. + * + * @param window A window object with which it will create the DOM tree + * and containing div element. + * @param cues An array of VTTCues who need there display state to be + * computed. + * @param overlay The HTMLElement that the cues will be displayed within. + * @param controls The video control element that will affect cues position. + */ + void processCues(in mozIDOMWindow window, in nsIVariant cues, + in nsISupports overlay, in nsISupports controls); +}; + +%{C++ +#define NS_WEBVTTPARSERWRAPPER_CONTRACTID "@mozilla.org/webvttParserWrapper;1" +%} diff --git a/dom/media/webvtt/package.json b/dom/media/webvtt/package.json new file mode 100644 index 0000000000..2952b78c01 --- /dev/null +++ b/dom/media/webvtt/package.json @@ -0,0 +1,6 @@ +{ + "devDependencies": { + "gift": "~0.0.6", + "optimist": "~0.6.0" + } +} diff --git a/dom/media/webvtt/test/crashtests/1304948.html b/dom/media/webvtt/test/crashtests/1304948.html new file mode 100644 index 0000000000..667a13d06a --- /dev/null +++ b/dom/media/webvtt/test/crashtests/1304948.html @@ -0,0 +1,33 @@ +<html class="reftest-wait"> +<head> + <title> Bug 1304948 : Crash if a texttrack remove a cue not belongs to it. </title> +</head> +<meta charset="utf-8"> +<script type="text/javascript"> + +window.onload = function() { + var a = document.createElementNS('http://www.w3.org/1999/xhtml', 'video'); + a.src = ""; + document.body.appendChild(a); + var b = a.addTextTrack('chapters', "AAAAAAAAAAAAAAAA", "de"); + var c = new VTTCue(0.6, 0.3, "AA"); + b.addCue(c); + var d = document.createElementNS('http://www.w3.org/1999/xhtml', 'video'); + var e = d.addTextTrack('chapters', "AAAA", "en-US"); + a.currentTime = 2; + a.play(); + try { + // This will queue a TimeMarchesOn task on mainthread, so use + // timer to wait the TimeMarchesOn crash. + e.removeCue(c); + } catch (e) { + if (e.name == "NotFoundError") { + setTimeout(function() { + document.documentElement.removeAttribute("class");}, 0); + } + } +}; + +</script> +</body> +</html> diff --git a/dom/media/webvtt/test/crashtests/1319486.html b/dom/media/webvtt/test/crashtests/1319486.html new file mode 100644 index 0000000000..74bdb2147c --- /dev/null +++ b/dom/media/webvtt/test/crashtests/1319486.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<html> +<head> + <title> Bug 1319486 : Crash if a texttrackcue added to different texttracks. </title> +</head> +<body> +<video id=v1></video> +<video id=v2></video> +</body> +<meta charset="utf-8"> +<script type="text/javascript"> + +addEventListener('DOMContentLoaded', function(){ + let cue,tt1,tt2; + v1.play(); + tt1 = v1.addTextTrack('metadata', "", ""); + v1.play(); + v1.currentTime = 8; + cue = new VTTCue(0, 0.5, ""); + tt1.addCue(cue); + tt2 = v2.addTextTrack('captions', "", ""); + tt2.addCue(cue); + tt2.removeCue(cue); + tt1.addCue(new VTTCue(0.7, 2, "")); +}); +</script> +</html> diff --git a/dom/media/webvtt/test/crashtests/1533909.html b/dom/media/webvtt/test/crashtests/1533909.html new file mode 100644 index 0000000000..ee73ecb7b8 --- /dev/null +++ b/dom/media/webvtt/test/crashtests/1533909.html @@ -0,0 +1,17 @@ +<script> +function eh1() { + d.track.addCue(new VTTCue(0.01, 0.69, "Y")) +} +function eh2() { + a.currentTime = 0.43 + c.addEventListener("DOMNodeRemoved", eh1) + a.append(b) + a.appendChild(c) +} +</script> +<canvas id="c"> +<q id="b"> +</canvas> +<audio id="a" src="data:audio/mpeg;base64,/+M4wAAAAAAAAAAAAEluZm8AAAAPAAAAAwAAAbAAqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////////////////////////////////////////AAAAAExhdmM1Ny4zOAAAAAAAAAAAAAAAACQAAAAAAAAAAAGwU/hwzwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+MYxAAMkI7huAhMDGQAndv+H5jfIAPmPzGBGeTAZNN7yjsnqd6Fny79M+o4UifnMu/TlDiIP5Q5l9Z/Kdv6VY0gE3JIAAwE/+MYxAgMYJr6WBhGIivjMy/CEK+zM39VVXjMArKgq6DQdrBV0THlqBp0ShrBp8qGsTPw7lXZFVZZY6GytZZYGChgYIGZaiJI/+MYxBEMUJnAABmMYUigMQLIE1yVpphA1UQMRVVVV000iVu000VVVX///6aaVUxBTUUzLjk5LjVVVVVVVVVVVVVVVVVVVVVV"> +<track id="d"></track> +<iframe onload="eh2()"> diff --git a/dom/media/webvtt/test/crashtests/882549.html b/dom/media/webvtt/test/crashtests/882549.html new file mode 100644 index 0000000000..8a720c3e11 --- /dev/null +++ b/dom/media/webvtt/test/crashtests/882549.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> + <head> + <script> + var o0 = new VTTCue(0.000, 1.000, 'Bug882549'); + var o1 = o0.getCueAsHTML(); + </script> + </head> + <body> + </body> +</html> +<script> +</script> diff --git a/dom/media/webvtt/test/crashtests/894104.html b/dom/media/webvtt/test/crashtests/894104.html new file mode 100644 index 0000000000..d021994e74 --- /dev/null +++ b/dom/media/webvtt/test/crashtests/894104.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="UTF-8"> +<script> + +function boom() +{ + var frame = document.getElementById("f"); + var frameWin = frame.contentWindow; + frameWin.VTTCue; + document.body.removeChild(frame); + new frameWin.VTTCue(0, 1, "Bug 894104").getCueAsHTML(); +} + +</script> +</head> + +<body onload="boom();"><iframe id="f" src="data:text/html,1"></iframe></body> +</html> diff --git a/dom/media/webvtt/test/crashtests/crashtests.list b/dom/media/webvtt/test/crashtests/crashtests.list new file mode 100644 index 0000000000..c9776984bc --- /dev/null +++ b/dom/media/webvtt/test/crashtests/crashtests.list @@ -0,0 +1,5 @@ +load 1304948.html +load 1319486.html +load 1533909.html +load 882549.html +load 894104.html diff --git a/dom/media/webvtt/test/mochitest/bad-signature.vtt b/dom/media/webvtt/test/mochitest/bad-signature.vtt new file mode 100644 index 0000000000..c9a59b35e9 --- /dev/null +++ b/dom/media/webvtt/test/mochitest/bad-signature.vtt @@ -0,0 +1 @@ +WEB diff --git a/dom/media/webvtt/test/mochitest/basic.vtt b/dom/media/webvtt/test/mochitest/basic.vtt new file mode 100644 index 0000000000..45646ab868 --- /dev/null +++ b/dom/media/webvtt/test/mochitest/basic.vtt @@ -0,0 +1,29 @@ +WEBVTT +REGION +id:testOne lines:2 width:30% +REGION +id:testTwo lines:4 width:20% + +1 +00:00.500 --> 00:00.700 region:testOne +This + +2 +00:01.200 --> 00:02.400 region:testTwo +Is + +2.5 +00:02.000 --> 00:03.500 region:testOne +(Over here?!) + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +5 +00:03.217 --> 00:03.989 +And more! diff --git a/dom/media/webvtt/test/mochitest/bug883173.vtt b/dom/media/webvtt/test/mochitest/bug883173.vtt new file mode 100644 index 0000000000..61f086bcce --- /dev/null +++ b/dom/media/webvtt/test/mochitest/bug883173.vtt @@ -0,0 +1,16 @@ +WEBVTT + +00:03.000 --> 00:04.000 +Should display fifth. + +00:01.000 --> 00:02.000 +Should display first. + +00:01.000 --> 00:03.000 +Should display second. + +00:02.000 --> 00:04.000 +Should display forth. + +00:02.000 --> 00:03.000 +Should display third. diff --git a/dom/media/webvtt/test/mochitest/long.vtt b/dom/media/webvtt/test/mochitest/long.vtt new file mode 100644 index 0000000000..23984b0c8d --- /dev/null +++ b/dom/media/webvtt/test/mochitest/long.vtt @@ -0,0 +1,8001 @@ +WEBVTT + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test + +1 +00:00.500 --> 00:00.700 +This + +2 +00:01.200 --> 00:02.400 +Is + +3 +00:02.710 --> 00:02.910 +A + +4 +00:03.217 --> 00:03.989 +Test diff --git a/dom/media/webvtt/test/mochitest/manifest.js b/dom/media/webvtt/test/mochitest/manifest.js new file mode 100644 index 0000000000..91c481feb9 --- /dev/null +++ b/dom/media/webvtt/test/mochitest/manifest.js @@ -0,0 +1,27 @@ +// Force releasing decoder to avoid timeout in waiting for decoding resource. +function removeNodeAndSource(n) { + n.remove(); + // reset |srcObject| first since it takes precedence over |src|. + n.srcObject = null; + n.removeAttribute("src"); + n.load(); + while (n.firstChild) { + n.firstChild.remove(); + } +} + +function once(target, name, cb) { + var p = new Promise(function (resolve, reject) { + target.addEventListener( + name, + function () { + resolve(); + }, + { once: true } + ); + }); + if (cb) { + p.then(cb); + } + return p; +} diff --git a/dom/media/webvtt/test/mochitest/mochitest.ini b/dom/media/webvtt/test/mochitest/mochitest.ini new file mode 100644 index 0000000000..ee905144b8 --- /dev/null +++ b/dom/media/webvtt/test/mochitest/mochitest.ini @@ -0,0 +1,50 @@ +[DEFAULT] +subsuite = media +tags = webvtt +skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1536604 +support-files = + ../../../test/gizmo.mp4 + ../../../test/seek.webm + ../../../test/vp9cake.webm + bad-signature.vtt + basic.vtt + bug883173.vtt + long.vtt + manifest.js + parser.vtt + region.vtt + sequential.vtt + vttPositionAlign.vtt + +[test_bug1018933.html] +[test_bug1242594.html] +[test_bug883173.html] +skip-if = (android_version == '25' && debug) # android(bug 1232305) +[test_bug895091.html] +skip-if = (android_version == '25' && debug) # android(bug 1232305) +[test_bug957847.html] +skip-if = (android_version == '25' && debug) # android(bug 1232305) +[test_trackelementevent.html] +[test_trackelementsrc.html] +[test_texttrack.html] +[test_texttrackcue.html] +[test_texttrackcue_moz.html] +[test_trackevent.html] +[test_texttrackevents_video.html] +[test_texttracklist.html] +[test_texttracklist_moz.html] +[test_texttrackregion.html] +[test_testtrack_cors_no_response.html] +[test_texttrack_cors_preload_none.html] +[test_texttrack_mode_change_during_loading.html] +skip-if = true + toolkit == 'android' # Bug 1636572, android(bug 1562021) +[test_texttrack_moz.html] +[test_vttparser.html] +[test_webvtt_empty_displaystate.html] +[test_webvtt_event_same_time.html] +[test_webvtt_infinite_processing_loop.html] +[test_webvtt_overlapping_time.html] +[test_webvtt_positionalign.html] +[test_webvtt_seeking.html] +[test_webvtt_update_display_after_adding_or_removing_cue.html] diff --git a/dom/media/webvtt/test/mochitest/parser.vtt b/dom/media/webvtt/test/mochitest/parser.vtt new file mode 100644 index 0000000000..2a56c65f80 --- /dev/null +++ b/dom/media/webvtt/test/mochitest/parser.vtt @@ -0,0 +1,6 @@ +WEBVTT + +00:00.500 --> 00:00.700 +Test +00:00.500 --> 00:00.700 +Stuff diff --git a/dom/media/webvtt/test/mochitest/region.vtt b/dom/media/webvtt/test/mochitest/region.vtt new file mode 100644 index 0000000000..1e351dbcfb --- /dev/null +++ b/dom/media/webvtt/test/mochitest/region.vtt @@ -0,0 +1,6 @@ +WEBVTT +REGION +id:fred width:62% lines:5 regionanchor:4%,78% viewportanchor:10%,90% scroll:up + +00:01.000 --> 00:02.000 region:fred +Test here. diff --git a/dom/media/webvtt/test/mochitest/sequential.vtt b/dom/media/webvtt/test/mochitest/sequential.vtt new file mode 100644 index 0000000000..94e92e38ae --- /dev/null +++ b/dom/media/webvtt/test/mochitest/sequential.vtt @@ -0,0 +1,10 @@ +WEBVTT + +00:01.000 --> 00:02.000 +This + +00:03.000 --> 00:04.000 +Is + +00:05.000 --> 00:06.000 +A Test diff --git a/dom/media/webvtt/test/mochitest/test_bug1018933.html b/dom/media/webvtt/test/mochitest/test_bug1018933.html new file mode 100644 index 0000000000..bff1db6021 --- /dev/null +++ b/dom/media/webvtt/test/mochitest/test_bug1018933.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1018933 +--> +<head> + <meta charset='utf-8'> + <title>Regression test for bug 1018933 - HTMLTrackElement should create only one TextTrack</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +var video = document.createElement("video"); +video.src = "seek.webm"; +video.preload = "auto"; + +var trackElement = document.createElement("track"); +trackElement.src = "basic.vtt"; +trackElement.kind = "subtitles"; + +document.getElementById("content").appendChild(video); +video.appendChild(trackElement); + +// Accessing the track now would have caused the bug as the track element +// shouldn't have had time to bind to the tree yet. +trackElement.track.mode = 'showing'; + +video.addEventListener("loadedmetadata", function run_tests() { + // Re-que run_tests() at the end of the event loop until the track + // element has loaded its data. + if (trackElement.readyState == 1) { + setTimeout(run_tests, 0); + return; + } + + is(video.textTracks.length, 1, "Video should have one TextTrack."); + SimpleTest.finish(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webvtt/test/mochitest/test_bug1242594.html b/dom/media/webvtt/test/mochitest/test_bug1242594.html new file mode 100644 index 0000000000..25c47948bb --- /dev/null +++ b/dom/media/webvtt/test/mochitest/test_bug1242594.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1242594 +--> +<head> + <meta charset='utf-8'> + <title>Bug 1242594 - Unbind a video element with HTMLTrackElement + should not remove the TextTrack</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +var video = document.createElement("video"); +video.src = "seek.webm"; +video.preload = "auto"; + +var trackElement = document.createElement("track"); +trackElement.src = "basic.vtt"; +trackElement.kind = "subtitles"; + +document.getElementById("content").appendChild(video); +video.appendChild(trackElement); + +// Bug 1242599, access video.textTracks.length immediately after +// the track element binds into the media element. +is(video.textTracks.length, 1, "Video should have one TextTrack."); +var parent = video.parentNode; +parent.removeChild(video); +is(video.textTracks.length, 1, "After unbind the video element, should have one TextTrack."); +parent.appendChild(video); +is(video.textTracks.length, 1, "After bind the video element, should have one TextTrack."); +SimpleTest.finish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webvtt/test/mochitest/test_bug883173.html b/dom/media/webvtt/test/mochitest/test_bug883173.html new file mode 100644 index 0000000000..6e95f4e3ca --- /dev/null +++ b/dom/media/webvtt/test/mochitest/test_bug883173.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Bug 883173 - TextTrackCue(List) Sorting</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<video id="v" src="seek.webm" preload="metadata"> + <track src="bug883173.vtt" kind="subtitles" id="default" default> +</video> +<script type="text/javascript"> +/** + * This test is used to ensure that the cues in the cue list should be sorted by + * cues' start time and end time, not the present order in the file. + */ +function runTest() { + let trackElement = document.getElementById("default"); + is(trackElement.readyState, 2, "Track::ReadyState should be set to LOADED."); + + let expected = [[1, 3], [1, 2], [2, 4], [2, 3], [3, 4]]; + let cueList = trackElement.track.cues; + is(cueList.length, expected.length, "Cue list length should be 5."); + + for (let i = 0; i < expected.length; i++) { + is(cueList[i].startTime, expected[i][0], + `Cue's start time should be ${expected[i][0]}`); + is(cueList[i].endTime, expected[i][1], + `Cue's end time should be ${expected[i][1]}`); + } + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +onload = runTest; +</script> +</body> +</html> diff --git a/dom/media/webvtt/test/mochitest/test_bug895091.html b/dom/media/webvtt/test/mochitest/test_bug895091.html new file mode 100644 index 0000000000..6fa2629283 --- /dev/null +++ b/dom/media/webvtt/test/mochitest/test_bug895091.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Bug 895091 - Integrating vtt.js</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="manifest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<video id="v" src="seek.webm" preload="metadata"> + <track src="long.vtt" kind="subtitles" id="track1"> + <track src="long.vtt" kind="subtitles" id="track2"> +</video> +<script type="text/javascript"> +/** + * This test is used to ensure that we can load two track elements with large + * amount of cues at same time. In this test, both tracks are disable by default, + * we have to enable them in order to start loading. + */ +var trackElement = document.getElementById("track1"); +var trackElementTwo = document.getElementById("track2"); + +async function runTest() { + enableBothTracks(); + await waitUntilBothTracksLoaded(); + checkTrackReadyStateShouldBeLoaded(); + checkCuesAmount(); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +onload = runTest; + +/** + * The following are test helper functions. + */ +function enableBothTracks() { + // All tracks are `disable` on default. As we won't start loading for disabled + // tracks, we have to change their mode in order to start loading. + trackElement.track.mode = "hidden"; + trackElementTwo.track.mode = "hidden"; +} + +async function waitUntilBothTracksLoaded() { + info(`wait until both tracks finish loading`); + await Promise.all([once(trackElement, "load"), once(trackElementTwo, "load")]); +} + +function checkTrackReadyStateShouldBeLoaded() { + is(trackElement.readyState, 2, "Track::ReadyState should be set to LOADED."); + is(trackElementTwo.readyState, 2, "Track::ReadyState should be set to LOADED."); +} + +function checkCuesAmount() { + is(trackElement.track.cues.length, 2000, "Cue list length should be 2000."); + is(trackElementTwo.track.cues.length, 2000, "Cue list length should be 2000."); +} +</script> +</body> +</html> diff --git a/dom/media/webvtt/test/mochitest/test_bug957847.html b/dom/media/webvtt/test/mochitest/test_bug957847.html new file mode 100644 index 0000000000..8bbea81bf9 --- /dev/null +++ b/dom/media/webvtt/test/mochitest/test_bug957847.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=957847 +--> +<head> + <meta charset='utf-8'> + <title>Regression test for bug 957847 - Crash on TextTrack::AddCue </title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +var trackElement = document.createElement('track'); +trackElement.track.addCue(new VTTCue(0, 1, "A")); + +// We need to assert something for Mochitest to be happy. +ok(true); +SimpleTest.finish(); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webvtt/test/mochitest/test_testtrack_cors_no_response.html b/dom/media/webvtt/test/mochitest/test_testtrack_cors_no_response.html new file mode 100644 index 0000000000..e047f74eb3 --- /dev/null +++ b/dom/media/webvtt/test/mochitest/test_testtrack_cors_no_response.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Should not load CORS vtt file when server doesn't respond with correct CORS header</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="manifest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<video preload="none" crossorigin="anonymous"> + <track src="http://example.com/tests/dom/canvas/test/crossorigin/video.sjs?name=tests/dom/media/webvtt/test/mochitest/basic.vtt&type=text/vtt" kind="subtitles" id="default" default> +</video> +<script type="text/javascript"> +/** + * This test is used to ensure that we shouldn't load CORS resource if server + * doesn't respond with correct CORS header. In this situation, loading should + * be expected to fail. + */ +async function runTest() { + await waitUntiTrackLoadError(); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +runTest(); + +/** + * The following are test helper functions. + */ +async function waitUntiTrackLoadError() { + const trackElement = document.getElementById("default"); + if (trackElement.readyState != 3) { + info(`wait until receiving error event`); + await once(trackElement, "error"); + } + is(trackElement.readyState, 3, "Track::ReadyState should be set to ERROR."); + is(trackElement.track.cues.length, 0, "Cue list length should be 0."); +} +</script> +</body> +</html> diff --git a/dom/media/webvtt/test/mochitest/test_texttrack.html b/dom/media/webvtt/test/mochitest/test_texttrack.html new file mode 100644 index 0000000000..69c4f24bec --- /dev/null +++ b/dom/media/webvtt/test/mochitest/test_texttrack.html @@ -0,0 +1,158 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Bug 833386 - TextTrackList</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="manifest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<video id="v"> +<script type="text/javascript"> +/** + * This test is used to check different things. + * (1) the default value of track element's attributes + * (2) readonly attributes can't be modifted + * (3) the order of tracks in the media element's track list + */ +var enabledTrackElement = null; + +async function runTest() { + addFourTextTrackElementsToVideo(); + startLoadingVideo(); + await waitUntilEnableTrackLoaded(); + checkTracksStatus(); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +onload = runTest; + +/** + * The following are test helper functions. + */ +function addFourTextTrackElementsToVideo() { + let video = document.getElementById("v"); + isnot(video.textTracks, undefined, + "HTMLMediaElement::TextTrack() property should be available.") + + let trackList = video.textTracks; + is(trackList.length, 0, "Length should be 0."); + + ok(typeof video.addTextTrack == "function", + "HTMLMediaElement::AddTextTrack() function should be available.") + + // Insert some tracks in an order that is not sorted, we will test if they + // are sorted later. + info(`- Add a track element with label 'third' -`); + video.addTextTrack("subtitles", "third", "en-CA"); + is(trackList.length, 1, "Length should be 1."); + + let textTrack = video.textTracks[0]; + checkAttributesDefaultValue(textTrack); + checkTextTrackMode(textTrack); + checkReadOnlyAttributes(textTrack); + + info(`- Add a track element with label 'first' -`); + let trackOne = document.createElement("track"); + video.appendChild(trackOne); + trackOne.label = "first"; + trackOne.src = "basic.vtt"; + trackOne.default = true; + trackOne.id = "2"; + // The automatic track selection would choose the first track element with + // `default` attribute, so this track would be enable later. + enabledTrackElement = trackOne; + + info(`- Add a track element with label 'fourth' -`); + video.addTextTrack("subtitles", "fourth", "en-CA"); + + info(`- Add a track element with label 'second' -`); + let trackTwo = document.createElement("track"); + video.appendChild(trackTwo); + trackTwo.label = "second"; + trackTwo.src = "basic.vtt"; + // Although this track has `default` attribute as well, it won't be enable by + // the automatic track selection because it's not the first default track in + // the media element's track list. + trackTwo.default = true; +} + +function checkAttributesDefaultValue(track) { + is(track.label, "third", "Label should be set to third."); + is(track.language, "en-CA", "Language should be en-CA."); + is(track.kind, "subtitles", "Default kind should be subtitles."); + is(track.mode, "hidden", "Default mode should be hidden."); +} + +function checkTextTrackMode(track) { + // Mode should not allow a bogus value. + track.mode = 'bogus'; + is(track.mode, 'hidden', "Mode should be not allow a bogus value."); + + // Should allow all these values for mode. + changeTextTrackMode("showing"); + changeTextTrackMode("disabled"); + changeTextTrackMode("hidden"); + + function changeTextTrackMode(mode) { + track.mode = mode; + is(track.mode, mode, `Mode should allow \"${mode}\"`); + } +} + +function checkReadOnlyAttributes(track) { + // All below are read-only properties and so should not allow setting. + track.label = "French subtitles"; + is(track.label, "third", "Label is read-only so should still be \"label\"."); + track.language = "en"; + is(track.language, "en-CA", "Language is read-only so should still be \"en-CA\"."); + track.kind = "captions"; + is(track.kind, "subtitles", "Kind is read-only so should still be \"subtitles\""); +} + +function startLoadingVideo() { + let video = document.getElementById("v"); + video.src = "seek.webm"; + video.preload = "metadata"; +} + +async function waitUntilEnableTrackLoaded() { + info(`wait until the enabled track finishes loading`); + await once(enabledTrackElement, "load"); + is(enabledTrackElement.readyState, 2, "Track::ReadyState should be set to LOADED."); +} + +function checkTracksStatus() { + // We're testing two things here, + // (1) the tracks created from a track element have a default mode 'disabled' + // and tracks created from 'addTextTrack' method have a default + // mode of 'hidden'. + // (2) we're testing that the tracks are sorted properly. For the tracks to + // be sorted the first two tracks, added through a TrackElement, must occupy + // the first two indexes in their TrackElement tree order. The second two + // tracks, added through the 'addTextTrack' method, will occupy the last two + // indexes in the order that they were added in. + let trackData = [ + { label: "first", mode: "showing", id: "2" }, + { label: "second", mode: "disabled", id: "" }, + { label: "third", mode: "hidden", id: "" }, + { label: "fourth", mode: "hidden", id: "" } + ]; + let video = document.getElementById("v"); + is(video.textTracks.length, trackData.length, + `TextTracks length should be ${trackData.length}`); + for (let i = 0; i < trackData.length; i++) { + let track = video.textTracks[i]; + isnot(track, null, `Video should have a text track at index ${i}`); + let info = trackData[i]; + for (let key in info) { + is(track[key], info[key], + `Track at index ${i} should have a '${key}' property with a value of '${info[key]}'.`); + } + } +} + +</script> +</body> +</html> diff --git a/dom/media/webvtt/test/mochitest/test_texttrack_cors_preload_none.html b/dom/media/webvtt/test/mochitest/test_texttrack_cors_preload_none.html new file mode 100644 index 0000000000..6743e8c4cd --- /dev/null +++ b/dom/media/webvtt/test/mochitest/test_texttrack_cors_preload_none.html @@ -0,0 +1,40 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>load CORS Text track correctly when its parent media element's preload is none</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="manifest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<video preload="none" crossorigin="anonymous"> + <track src="http://example.com/tests/dom/canvas/test/crossorigin/video.sjs?name=tests/dom/media/webvtt/test/mochitest/basic.vtt&type=text/vtt&cors=anonymous" kind="subtitles" id="default" default> +</video> +<script type="text/javascript"> +/** + * This test is used to test the text track element with CORS resource can starts + * loaded correctly when its parent media element's preload attribute is none. + */ +async function runTest() { + await waitUntiTrackLoaded(); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +runTest(); + +/** + * The following are test helper functions. + */ +async function waitUntiTrackLoaded() { + let trackElement = document.getElementById("default"); + if (trackElement.readyState != 2) { + info(`wait until the track finishes loading`); + await once(trackElement, "load"); + } + is(trackElement.readyState, 2, "Track::ReadyState should be set to LOADED."); + is(trackElement.track.cues.length, 6, "Cue list length should be 6."); +} +</script> +</body> +</html> diff --git a/dom/media/webvtt/test/mochitest/test_texttrack_mode_change_during_loading.html b/dom/media/webvtt/test/mochitest/test_texttrack_mode_change_during_loading.html new file mode 100644 index 0000000000..974f452092 --- /dev/null +++ b/dom/media/webvtt/test/mochitest/test_texttrack_mode_change_during_loading.html @@ -0,0 +1,75 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebVTT : changing track's mode during loading</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="manifest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<script class="testbody" type="text/javascript"> +/** + * This test is to ensure that we won't get `error` event when we change track's + * mode during loading. In this test, track element starts loading after setting + * the src and we would start another load later just after the channel which is + * used to fetch data starts. The second load is triggered by mode changes, and + * it should stop the prevous load and won't generate any error. + */ +async function startTest() { + const video = createVideo(); + const trackElement = createAndAppendtrackElemententToVideo(video); + + await changeTrackModeDuringLoading(trackElement); + await waitUntilTrackLoaded(trackElement); + + removeNodeAndSource(video); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +SpecialPowers.pushPrefEnv({"set": [["media.webvtt.testing.events", true]]}, + startTest); + +/** + * The following are test helper functions. + */ +function createVideo() { + info(`create video`); + let video = document.createElement("video"); + video.src = "gizmo.mp4"; + document.body.appendChild(video); + return video; +} + +function createAndAppendtrackElemententToVideo(video) { + let trackElement = document.createElement("track"); + trackElement.default = true; + video.append(trackElement); + return trackElement; +} + +async function changeTrackModeDuringLoading(trackElement) { + info(`set src to start loading`); + trackElement.src = "basic.vtt"; + + info(`wait until starting loading resource.`); + await once(trackElement, "mozStartedLoadingTextTrack"); + + info(`changeing track's mode during loading should not cause loading failed.`); + trackElement.onerror = () => { + ok(false, `Should not get error event!`); + } + trackElement.track.mode = "hidden"; +} + +async function waitUntilTrackLoaded(trackElement) { + if (trackElement.readyState != 2) { + info(`wait until the track finishes loading`); + await once(trackElement, "load"); + } + is(trackElement.readyState, 2, "Track::ReadyState should be set to LOADED."); + is(trackElement.track.cues.length, 6, "Cue list length should be 6."); +} +</script> +</body> +</html> diff --git a/dom/media/webvtt/test/mochitest/test_texttrack_moz.html b/dom/media/webvtt/test/mochitest/test_texttrack_moz.html new file mode 100644 index 0000000000..b39562b3a4 --- /dev/null +++ b/dom/media/webvtt/test/mochitest/test_texttrack_moz.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Bug 881976 - TextTrackList</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="manifest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<video id="v" src="seek.webm" preload="metadata"> +<script type="text/javascript"> +/** + * This test is used to ensure the text track list we got from video is as same + * as the one in the text track. + */ +var video = document.getElementById("v"); + +async function runTest() { + addTrackViaAddTrackAPI(); + await addTrackViaTrackElement(); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +onload = runTest; + +/** + * The following are test helper functions. + */ +function addTrackViaAddTrackAPI() { + // Check if adding a text track manually sets the TextTrackList correctly. + video.addTextTrack("subtitles", "", ""); + // TextTrack.textTrackList is an extension available only to privileged code, + // so we need to access it through the SpecialPowers object. + is(SpecialPowers.unwrap(SpecialPowers.wrap(video.textTracks[0]).textTrackList), + video.textTracks, + "The Track's TextTrackList should be the Video's TextTrackList."); +} + +async function addTrackViaTrackElement() { + // Check if loading a Track via a TrackElement sets the TextTrackList correctly. + let trackElement = document.createElement("track"); + trackElement.src = "basic.vtt"; + trackElement.kind = "subtitles"; + trackElement.default = true; + video.appendChild(trackElement); + + info(`wait until the track finishes loading`); + await once(trackElement, "load"); + + is(trackElement.readyState, HTMLTrackElement.LOADED, + "Track::ReadyState should be set to LOADED."); + is(SpecialPowers.unwrap(SpecialPowers.wrap(trackElement.track).textTrackList), + video.textTracks, + "TrackElement's Track's TextTrackList should be the Video's TextTrackList."); +} + +</script> +</body> +</html> diff --git a/dom/media/webvtt/test/mochitest/test_texttrackcue.html b/dom/media/webvtt/test/mochitest/test_texttrackcue.html new file mode 100644 index 0000000000..3d9783c07d --- /dev/null +++ b/dom/media/webvtt/test/mochitest/test_texttrackcue.html @@ -0,0 +1,298 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Bug 833386 - HTMLTrackElement</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="manifest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<video id="v" src="seek.webm" preload="metadata"> + <track src="basic.vtt" kind="subtitles" id="default" default> +</video> +<script type="text/javascript"> +/** + * This test is used to test the VTTCue's different behaviors and check whether + * cues would be activatived correctly during video playback. + */ +var video = document.getElementById("v"); +var trackElement = document.getElementById("default"); + +async function runTest() { + await waitUntiTrackLoaded(); + checkCueDefinition(); + checkFirstCueParsedContent(); + checkCueStartTimeAndEndtime(); + checkCueSizeAndPosition(); + checkCueSnapToLines(); + checkCueAlignmentAndWritingDirection(); + checkCueLine(); + checkCreatingNewCue(); + checkRemoveNonExistCue(); + checkActiveCues(); + checkCueRegion(); + await checkCActiveCuesDuringVideoPlaying(); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +SpecialPowers.pushPrefEnv({"set": [["media.webvtt.regions.enabled", true]]}, + runTest); +/** + * The following are test helper functions. + */ +async function waitUntiTrackLoaded() { + if (trackElement.readyState != 2) { + info(`wait until the track finishes loading`); + await once(trackElement, "load"); + } + is(trackElement.readyState, 2, "Track::ReadyState should be set to LOADED."); + is(trackElement.track.cues.length, 6, "Cue list length should be 6."); +} + +function checkCueDefinition() { + // Check that the typedef of TextTrackCue works in Gecko. + isnot(window.TextTrackCue, undefined, "TextTrackCue should be defined."); + isnot(window.VTTCue, undefined, "VTTCue should be defined."); +} + +function checkFirstCueParsedContent() { + // Check if first cue was parsed correctly. + const cue = trackElement.track.cues[0]; + ok(cue instanceof TextTrackCue, "Cue should be an instanceof TextTrackCue."); + ok(cue instanceof VTTCue, "Cue should be an instanceof VTTCue."); + is(cue.id, "1", "Cue's ID should be 1."); + is(cue.startTime, 0.5, "Cue's start time should be 0.5."); + is(cue.endTime, 0.7, "Cue's end time should be 0.7."); + is(cue.pauseOnExit, false, "Cue's pause on exit flag should be false."); + is(cue.text, "This", "Cue's text should be set correctly."); + is(cue.track, trackElement.track, "Cue's track should be defined."); + cue.track = null; + isnot(cue.track, null, "Cue's track should not be able to be set."); +} + +function checkCueStartTimeAndEndtime() { + const cueList = trackElement.track.cues; + // Check that all cue times were not rounded + is(cueList[1].startTime, 1.2, "Second cue's start time should be 1.2."); + is(cueList[1].endTime, 2.4, "Second cue's end time should be 2.4."); + is(cueList[2].startTime, 2, "Third cue's start time should be 2."); + is(cueList[2].endTime, 3.5, "Third cue's end time should be 3.5."); + is(cueList[3].startTime, 2.71, "Fourth cue's start time should be 2.71."); + is(cueList[3].endTime, 2.91, "Fourth cue's end time should be 2.91."); + is(cueList[4].startTime, 3.217, "Fifth cue's start time should be 3.217."); + is(cueList[4].endTime, 3.989, "Fifth cue's end time should be 3.989."); + is(cueList[5].startTime, 3.217, "Sixth cue's start time should be 3.217."); + is(cueList[5].endTime, 3.989, "Sixth cue's end time should be 3.989."); + + // Check that Cue setters are working correctly. + const cue = trackElement.track.cues[0]; + cue.id = "Cue 01"; + is(cue.id, "Cue 01", "Cue's ID should be 'Cue 01'."); + cue.startTime = 0.51; + is(cue.startTime, 0.51, "Cue's start time should be 0.51."); + cue.endTime = 0.71; + is(cue.endTime, 0.71, "Cue's end time should be 0.71."); + cue.pauseOnExit = true; + is(cue.pauseOnExit, true, "Cue's pause on exit flag should be true."); + video.addEventListener("pause", function() { + video.play(); + }, {once: true}); +} + +function checkCueSizeAndPosition() { + function checkPercentageValue(prop, initialVal) { + ok(prop in cue, prop + " should be a property on VTTCue."); + cue[prop] = initialVal; + is(cue[prop], initialVal, `Cue's ${prop} should initially be ${initialVal}`); + [ 101, -1 ].forEach(function(val) { + let exceptionHappened = false; + try { + cue[prop] = val; + } catch(e) { + exceptionHappened = true; + is(e.name, "IndexSizeError", "Should have thrown IndexSizeError."); + } + ok(exceptionHappened, "Exception should have happened."); + }); + } + + const cue = trackElement.track.cues[0]; + checkPercentageValue("size", 100.0); + cue.size = 50.5; + is(cue.size, 50.5, "Cue's size should be 50.5.") + + // Check cue.position + checkPercentageValue("position", "auto"); + cue.position = 50.5; + is(cue.position, 50.5, "Cue's position value should now be 50.5."); +} + +function checkCueSnapToLines() { + const cue = trackElement.track.cues[0]; + ok(cue.snapToLines, "Cue's snapToLines should be set by set."); + cue.snapToLines = false; + ok(!cue.snapToLines, "Cue's snapToLines should not be set."); +} + +function checkCueAlignmentAndWritingDirection() { + function checkEnumValue(prop, initialVal, acceptedValues) { + ok(prop in cue, `${prop} should be a property on VTTCue.`); + is(cue[prop], initialVal, `Cue's ${prop} should be ${initialVal}`); + cue[prop] = "bogus"; + is(cue[prop], initialVal, `Cue's ${prop} should be ${initialVal}`); + acceptedValues.forEach(function(val) { + cue[prop] = val; + is(cue[prop], val, `Cue's ${prop} should be ${val}`); + if (typeof val === "string") { + cue[prop] = val.toUpperCase(); + is(cue[prop], val, `Cue's ${prop} should be ${val}`); + } + }); + } + + const cue = trackElement.track.cues[0]; + checkEnumValue("align", "center", [ "start", "left", "center", "right", "end" ]); + checkEnumValue("lineAlign", "start", [ "start", "center", "end" ]); + checkEnumValue("vertical", "", [ "", "lr", "rl" ]); + + cue.lineAlign = "center"; + is(cue.lineAlign, "center", "Cue's line align should be center."); + cue.lineAlign = "START"; + is(cue.lineAlign, "center", "Cue's line align should be center."); + cue.lineAlign = "end"; + is(cue.lineAlign, "end", "Cue's line align should be end."); + + // Check that cue position align works properly + is(cue.positionAlign, "auto", "Cue's default position alignment should be auto."); + + cue.positionAlign = "line-left"; + is(cue.positionAlign, "line-left", "Cue's position align should be line-left."); + cue.positionAlign = "auto"; + is(cue.positionAlign, "auto", "Cue's position align should be auto."); + cue.positionAlign = "line-right"; + is(cue.positionAlign, "line-right", "Cue's position align should be line-right."); +} + +function checkCueLine() { + const cue = trackElement.track.cues[0]; + // Check cue.line + is(cue.line, "auto", "Cue's line value should initially be auto."); + cue.line = 0.5; + is(cue.line, 0.5, "Cue's line value should now be 0.5."); + cue.line = "auto"; + is(cue.line, "auto", "Cue's line value should now be auto."); +} + +function checkCreatingNewCue() { + const cueList = trackElement.track.cues; + + // Check that we can create and add new VTTCues + let vttCue = new VTTCue(3.999, 4, "foo"); + is(vttCue.track, null, "Cue's track should be null."); + trackElement.track.addCue(vttCue); + is(vttCue.track, trackElement.track, "Cue's track should be defined."); + is(cueList.length, 7, "Cue list length should now be 7."); + + // Check that new VTTCue was added correctly + let cue = cueList[6]; + is(cue.startTime, 3.999, "Cue's start time should be 3.999."); + is(cue.endTime, 4, "Cue's end time should be 4."); + is(cue.text, "foo", "Cue's text should be foo."); + + // Adding the same cue again should not increase the cue count. + trackElement.track.addCue(vttCue); + is(cueList.length, 7, "Cue list length should be 7."); + + // Check that we are able to remove cues. + trackElement.track.removeCue(cue); + is(cueList.length, 6, "Cue list length should be 6."); +} + +function checkRemoveNonExistCue() { + is(trackElement.track.cues.length, 6, "Cue list length should be 6."); + let exceptionHappened = false; + try { + // We should not be able to remove a cue that is not in the list. + trackElement.track.removeCue(new VTTCue(1, 2, "foo")); + } catch (e) { + // "NotFoundError" should be thrown when trying to remove a cue that is + // not in the list. + is(e.name, "NotFoundError", "Should have thrown NotFoundError."); + exceptionHappened = true; + } + // If this is false then we did not throw an error and probably removed a cue + // when we shouln't have. + ok(exceptionHappened, "Exception should have happened."); + is(trackElement.track.cues.length, 6, "Cue list length should still be 6."); +} + +function checkActiveCues() { + video.currentTime = 2; + isnot(trackElement.track.activeCues, null); + + trackElement.track.mode = "disabled"; + is(trackElement.track.activeCues, null, "No active cue when track is disabled."); + trackElement.track.mode = "showing"; +} + +function checkCueRegion() { + let regionInfo = [ + { lines: 2, width: 30 }, + { lines: 4, width: 20 }, + { lines: 2, width: 30 } + ]; + + for (let i = 0; i < regionInfo.length; i++) { + let cue = trackElement.track.cues[i]; + isnot(cue.region, null, `Cue at ${i} should have a region.`); + for (let key in regionInfo[i]) { + is(cue.region[key], regionInfo[i][key], + `Region should have a ${key} property with a value of ${regionInfo[i][key]}`); + } + } +} + +async function checkCActiveCuesDuringVideoPlaying() { + // Test TextTrack::ActiveCues. + let cueInfo = [ + { startTime: 0.51, endTime: 0.71, ids: ["Cue 01"] }, + { startTime: 0.72, endTime: 1.19, ids: [] }, + { startTime: 1.2, endTime: 1.9, ids: [2] }, + { startTime: 2, endTime: 2.4, ids: [2, 2.5] }, + { startTime: 2.41, endTime: 2.70, ids: [2.5] }, + { startTime: 2.71, endTime: 2.91, ids: [2.5, 3] }, + { startTime: 2.92, endTime: 3.216, ids: [2.5] }, + { startTime: 3.217, endTime: 3.5, ids: [2.5, 4, 5] }, + { startTime: 3.51, endTime: 3.989, ids: [4, 5] }, + { startTime: 3.99, endTime: 4, ids: [] } + ]; + + video.addEventListener("timeupdate", function() { + let activeCues = trackElement.track.activeCues, + playbackTime = video.currentTime; + + for (let i = 0; i < cueInfo.length; i++) { + let cue = cueInfo[i]; + if (playbackTime >= cue.startTime && playbackTime < cue.endTime) { + is(activeCues.length, cue.ids.length, `There should be ${cue.ids.length} currently active cue(s).`); + for (let j = 0; j < cue.ids.length; j++) { + isnot(activeCues.getCueById(cue.ids[j]), undefined, + `The cue with ID ${cue.ids[j]} should be active.`); + } + break; + } + } + }); + + info(`start video from 0s.`); + video.currentTime = 0; + video.play(); + await once(video, "playing"); + info(`video starts playing.`); + await once(video, "ended"); +} + +</script> +</body> +</html> diff --git a/dom/media/webvtt/test/mochitest/test_texttrackcue_moz.html b/dom/media/webvtt/test/mochitest/test_texttrackcue_moz.html new file mode 100644 index 0000000000..29f6661f85 --- /dev/null +++ b/dom/media/webvtt/test/mochitest/test_texttrackcue_moz.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=967157 +--> +<head> + <meta charset='utf-8'> + <title>Test for Bug 967157 - Setting TextTrackCue::DisplayState should set TextTrackCue::HasBeenReset to false</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + + var cue = SpecialPowers.wrap(new VTTCue(0, 1, "Some text.")); + is(cue.hasBeenReset, false, "Cue's hasBeenReset flag should be false."); + is(cue.displayState, null, "Cue's displayState should be null."); + + cue.startTime = 0.5; + is(cue.hasBeenReset, true, "Cue's hasBeenReset flag should now be true."); + + cue.displayState = document.createElement("div"); + is(cue.hasBeenReset, false, "Cue's hasBeenReset flag should now be false."); + + SimpleTest.finish(); +</script> +</body> +</html> diff --git a/dom/media/webvtt/test/mochitest/test_texttrackevents_video.html b/dom/media/webvtt/test/mochitest/test_texttrackevents_video.html new file mode 100644 index 0000000000..83104633a4 --- /dev/null +++ b/dom/media/webvtt/test/mochitest/test_texttrackevents_video.html @@ -0,0 +1,91 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tests for TextTrack DOM Events</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +var video = document.createElement("video"); +video.src = "vp9cake.webm"; +video.preload = "auto"; +video.controls = true; +var trackElement = document.createElement("track"); +trackElement.src = "sequential.vtt"; +trackElement.kind = "subtitles"; +trackElement.default = true; +document.getElementById("content").appendChild(video); +video.appendChild(trackElement); + +var trackElementCueChangeCount = 0; +var trackCueChangeCount = 0; +var cueEnterCount = 0; +var cueExitCount = 0; + +video.addEventListener("loadedmetadata", function run_tests() { + // Re-queue run_tests() at the end of the event loop until the track + // element has loaded its data. + if (trackElement.readyState == 1) { + setTimeout(run_tests, 0); + return; + } + is(trackElement.readyState, 2, "Track::ReadyState should be set to LOADED."); + ok('oncuechange' in trackElement.track, "Track::OnCueChange should exist."); + + var textTrack = trackElement.track; + is(textTrack.cues.length, 3, "textTrack.cues.length should 3."); + textTrack.cues[0].onenter = function() { + ++cueEnterCount; + }; + textTrack.cues[0].onexit = function() { + ++cueExitCount; + }; + textTrack.cues[1].onenter = function() { + ++cueEnterCount; + }; + textTrack.cues[1].onexit = function() { + ++cueExitCount; + }; + textTrack.cues[2].onenter = function() { + ++cueEnterCount; + }; + textTrack.cues[2].onexit = function() { + ++cueExitCount; + }; + + trackElement.track.oncuechange = function() { + ++trackElementCueChangeCount; + }; + + trackElement.addEventListener("cuechange", function() { + ++trackCueChangeCount; + }); + + video.play(); +}); + +video.addEventListener('ended', function() { + // Should be fired 1 to 6 times, as there are 3 cues, + // with a change event for when it is activated/deactivated + // (6 events at most). + isnot(trackElementCueChangeCount, 0, "TrackElement should fire cue change at least one time."); + ok(trackElementCueChangeCount <= 6, 'trackElementCueChangeCount should <= 6'); + isnot(trackCueChangeCount, 0, "TrackElement.track should fire cue change at least one time."); + ok(trackCueChangeCount <= 6, 'trackCueChangeCount should <= 6'); + is(cueEnterCount, 3, "cueEnterCount should fire three times."); + is(cueExitCount, 3, "cueExitCount should fire three times."); + SimpleTest.finish() +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webvtt/test/mochitest/test_texttracklist.html b/dom/media/webvtt/test/mochitest/test_texttracklist.html new file mode 100644 index 0000000000..c1d2296289 --- /dev/null +++ b/dom/media/webvtt/test/mochitest/test_texttracklist.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=882703 +--> +<head> + <meta charset="utf-8"> + <title>Media test: TextTrackList change event</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +let video = document.createElement("video"); + +isnot(video.textTracks, null, "Video should have a list of TextTracks."); + +video.addTextTrack("subtitles", "", ""); + +let track = video.textTracks[0]; +video.textTracks.addEventListener("change", changed); + +is(track.mode, "hidden", "New TextTrack's mode should be hidden."); +track.mode = "showing"; +// Bug882674: change the mode again to see if we receive only one +// change event. +track.mode = "hidden"; + +var eventCount = 0; +function changed(event) { + eventCount++; + is(eventCount, 1, "change event dispatched multiple times."); + is(event.target, video.textTracks, "change event's target should be video.textTracks."); + ok(event instanceof window.Event, "change event should be a simple event."); + ok(!event.bubbles, "change event should not bubble."); + ok(event.isTrusted, "change event should be trusted."); + ok(!event.cancelable, "change event should not be cancelable."); + + // Delay the finish function call for testing the change event count. + setTimeout(SimpleTest.finish, 0); +} +</script> +</pre> +</body> +</html> diff --git a/dom/media/webvtt/test/mochitest/test_texttracklist_moz.html b/dom/media/webvtt/test/mochitest/test_texttracklist_moz.html new file mode 100644 index 0000000000..6cae8323fd --- /dev/null +++ b/dom/media/webvtt/test/mochitest/test_texttracklist_moz.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=881976 +--> +<head> + <meta charset='utf-8'> + <title>Test for Bug 881976 - TextTrackCue Computed Position</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +var video = document.createElement("video"); +var trackList = video.textTracks; +ok(trackList instanceof TextTrackList, + "Video's textTracks should be a TextTrackList"); +var trackParent = SpecialPowers.unwrap( + SpecialPowers.wrap(trackList).mediaElement +); +is(trackParent, video, + "Video's TextTrackList's MediaElement reference should be set to the video."); +SimpleTest.finish(); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webvtt/test/mochitest/test_texttrackregion.html b/dom/media/webvtt/test/mochitest/test_texttrackregion.html new file mode 100644 index 0000000000..f41c404445 --- /dev/null +++ b/dom/media/webvtt/test/mochitest/test_texttrackregion.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Bug 917945 - VTTRegion</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="manifest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<video id="v" src="seek.webm" preload="auto"> + <track src="region.vtt" kind="subtitles" id="default" default> +</video> +<script type="text/javascript"> +/** + * This test is used to ensure that we can parse VTT region attributes correctly + * from vtt file. + */ +var trackElement = document.getElementById("default"); + +async function runTest() { + await waitUntiTrackLoaded(); + checkRegionAttributes(); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +SpecialPowers.pushPrefEnv({"set": [["media.webvtt.regions.enabled", true]]}, + runTest); +/** + * The following are test helper functions. + */ +async function waitUntiTrackLoaded() { + if (trackElement.readyState != 2) { + info(`wait until the track finishes loading`); + await once(trackElement, "load"); + } + is(trackElement.readyState, 2, "Track::ReadyState should be set to LOADED."); +} + +function checkRegionAttributes() { + let cues = trackElement.track.cues; + is(cues.length, 1, "Cue list length should be 1."); + + let region = cues[0].region; + isnot(region, null, "Region should not be null."); + is(region.width, 62, "Region width should be 50."); + is(region.lines, 5, "Region lines should be 5."); + is(region.regionAnchorX, 4, "Region regionAnchorX should be 4."); + is(region.regionAnchorY, 78, "Region regionAnchorY should be 78."); + is(region.viewportAnchorX, 10, "Region viewportAnchorX should be 10."); + is(region.viewportAnchorY, 90, "Region viewportAnchorY should be 90."); + is(region.scroll, "up", "Region scroll should be 'up'"); +} + +</script> +</body> +</html> diff --git a/dom/media/webvtt/test/mochitest/test_trackelementevent.html b/dom/media/webvtt/test/mochitest/test_trackelementevent.html new file mode 100644 index 0000000000..f78033a89d --- /dev/null +++ b/dom/media/webvtt/test/mochitest/test_trackelementevent.html @@ -0,0 +1,77 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Bug 882677 - Implement the 'sourcing out of band text tracks' algorithm</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="manifest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<video id="v" src="seek.webm" preload="auto"> +<script type="text/javascript"> +/** + * This test is used to ensure that we can load resource from vtt files correctly + * and will dispatch `error` event for invalid vtt files. + */ +var video = document.getElementById("v"); + +async function runTest() { + let tracks = createTextTrackElements(); + appendTracksToVideo(tracks); + await waitUntilsTrackLoadedOrGetError(tracks); + SimpleTest.finish() +} + +SimpleTest.waitForExplicitFinish(); +onload = runTest; + +/** + * The following are test helper functions. + */ +function createTextTrackElements() { + // Only first track has valid vtt resource, other tracks should get the error + // event because of invalid vtt resources. + let trackOne = document.createElement("track"); + trackOne.src = "basic.vtt"; + trackOne.kind = "subtitles"; + trackOne.expectedLoaded = true; + + let trackTwo = document.createElement("track"); + trackTwo.src = "bad-signature.vtt"; + trackTwo.kind = "captions"; + trackTwo.expectedLoaded = false; + + let trackThree = document.createElement("track"); + trackThree.src = "bad.vtt"; + trackThree.kind = "chapters"; + trackThree.expectedLoaded = false; + + return [trackOne, trackTwo, trackThree]; +} + +function appendTracksToVideo(tracks) { + for (let track of tracks) { + video.appendChild(track); + } +} + +async function waitUntilsTrackLoadedOrGetError(tracks) { + let promises = []; + for (let track of tracks) { + // explictly enable those track in order to start loading. + track.track.mode = "hidden"; + if (track.expectedLoaded) { + info(`adding 'load' event to wait list.`); + promises.push(once(track, "load")); + } else { + info(`adding 'error' event to wait list.`); + promises.push(once(track, "error")); + } + } + info(`wait until tracks finish loading or get error.`); + await Promise.all(promises); + ok(true, "all tracks finish loading or get error."); +} +</script> +</body> +</html> diff --git a/dom/media/webvtt/test/mochitest/test_trackelementsrc.html b/dom/media/webvtt/test/mochitest/test_trackelementsrc.html new file mode 100644 index 0000000000..f98e79c605 --- /dev/null +++ b/dom/media/webvtt/test/mochitest/test_trackelementsrc.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1281418 - Change the src attribue for TrackElement.</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +SpecialPowers.pushPrefEnv({"set": [["media.webvtt.regions.enabled", true]]}, + function() { + var video = document.createElement("video"); + video.src = "seek.webm"; + video.preload = "metadata"; + var trackElement = document.createElement("track"); + trackElement.src = "basic.vtt"; + trackElement.default = true; + + document.getElementById("content").appendChild(video); + video.appendChild(trackElement); + + video.addEventListener("loadedmetadata", function metadata() { + if (trackElement.readyState <= 1) { + setTimeout(metadata, 0); + return; + } + is(video.textTracks.length, 1, "Length should be 1."); + is(video.textTracks[0].cues.length, 6, "Cue length should be 6."); + + trackElement.src = "sequential.vtt"; + trackElement.track.mode = "showing"; + video.play(); + }); + + video.addEventListener("ended", function end() { + is(trackElement.readyState, 2, "readyState should be 2.") + is(video.textTracks.length, 1, "Length should be 1."); + is(video.textTracks[0].cues.length, 3, "Cue length should be 3."); + SimpleTest.finish(); + }); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webvtt/test/mochitest/test_trackevent.html b/dom/media/webvtt/test/mochitest/test_trackevent.html new file mode 100644 index 0000000000..ffba7d33ec --- /dev/null +++ b/dom/media/webvtt/test/mochitest/test_trackevent.html @@ -0,0 +1,69 @@ + +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Bug 893309 - Implement TrackEvent</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +var video = document.createElement("video"); +isnot(video.textTracks, undefined, "HTMLMediaElement::TextTrack() property should be available.") +ok(typeof video.addTextTrack == "function", "HTMLMediaElement::AddTextTrack() function should be available.") + +var trackList = video.textTracks; +is(trackList.length, 0, "Length should be 0."); + +var evtTextTrack, numOfCalls = 0, tt; +trackList.onaddtrack = function(event) { + ok(event instanceof TrackEvent, "Fired event from onaddtrack should be a TrackEvent"); + is(event.type, "addtrack", "Event type should be addtrack"); + ok(event.isTrusted, "Event should be trusted!"); + ok(!event.bubbles, "Event shouldn't bubble!"); + ok(!event.cancelable, "Event shouldn't be cancelable!"); + + evtTextTrack = event.track; + tt = textTrack[numOfCalls].track || textTrack[numOfCalls]; + + ok(tt === evtTextTrack, "Text tracks should be the same"); + is(evtTextTrack.label, label[numOfCalls], "Label should be set to "+ label[numOfCalls]); + is(evtTextTrack.language, language[numOfCalls], "Language should be " + language[numOfCalls]); + is(evtTextTrack.kind, kind[numOfCalls], "Kind should be " + kind[numOfCalls]); + + if (++numOfCalls == 4) { + SimpleTest.finish(); + } +}; + +var label = ["Oasis", "Coldplay", "t.A.T.u", ""]; +var language = ["en-CA", "en-GB", "ru", ""]; +var kind = ["subtitles", "captions", "chapters", "subtitles"]; + +var textTrack = new Array(4); +for (var i = 0; i < 3; ++i) { + textTrack[i] = video.addTextTrack(kind[i], label[i], language[i]); + is(trackList.length, i + 1, "Length should be " + (i+1)); +} + +video.src = "seek.webm"; +video.preload = "auto"; +var trackElement = document.createElement("track"); +trackElement.src = "basic.vtt"; +textTrack[3] = trackElement; + +document.getElementById("content").appendChild(video); +video.appendChild(trackElement); + +//TODO: Tests for removetrack event to be added along with bug 882677 +</script> +</pre> +</body> +</html> diff --git a/dom/media/webvtt/test/mochitest/test_vttparser.html b/dom/media/webvtt/test/mochitest/test_vttparser.html new file mode 100644 index 0000000000..419723a1d4 --- /dev/null +++ b/dom/media/webvtt/test/mochitest/test_vttparser.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset='utf-8'> + <title>WebVTT Parser Regression Tests</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +var video = document.createElement("video"); +video.src = "seek.webm"; +video.preload = "auto"; + +var trackElement = document.createElement("track"); +trackElement.src = "parser.vtt"; +trackElement.kind = "subtitles"; +trackElement.default = true; + +document.getElementById("content").appendChild(video); +video.appendChild(trackElement); +video.addEventListener("loadedmetadata", function run_tests() { + // Re-que run_tests() at the end of the event loop until the track + // element has loaded its data. + if (trackElement.readyState == 1) { + setTimeout(run_tests, 0); + return; + } + + is(trackElement.readyState, 2, "Track::ReadyState should be set to LOADED."); + is(trackElement.track.cues.length, 2, "Track should have two Cues."); + SimpleTest.finish(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webvtt/test/mochitest/test_webvtt_empty_displaystate.html b/dom/media/webvtt/test/mochitest/test_webvtt_empty_displaystate.html new file mode 100644 index 0000000000..e1a8ddc555 --- /dev/null +++ b/dom/media/webvtt/test/mochitest/test_webvtt_empty_displaystate.html @@ -0,0 +1,98 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset='utf-8'> + <title>WebVTT : cue's displaystate should be empty when its active flag is unset</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<div id="content"> +</div> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +var isReceivedOnEnterEvent = false; +var isReceivedOnExitEvent = false; + +function checkCueEvents() { + ok(isReceivedOnEnterEvent, "Already received cue's onEnter event."); + ok(isReceivedOnExitEvent, "Already received cue's onExit event."); + SimpleTest.finish(); +} + +function checkCueDisplayState(cue, expectedState) { + var cueChrome = SpecialPowers.wrap(cue); + if (expectedState) { + ok(cueChrome.displayState, "Cue's displayState shouldn't be empty."); + } else { + ok(!cueChrome.displayState, "Cue's displayState should be empty."); + } +} + +function runTest() { + info("--- create video ---"); + var video = document.createElement("video"); + video.src = "seek.webm"; + video.autoplay = true; + document.getElementById("content").appendChild(video); + + video.onended = function () { + video.onended = null; + checkCueEvents(); + }; + + video.onpause = function () { + video.onpause = null; + checkCueEvents(); + } + + video.onloadedmetadata = function () { + ok(video.duration > 2, "video.duration should larger than 2"); + } + + info("--- create the type of track ---"); + isnot(window.TextTrack, undefined, "TextTrack should be defined."); + + var track = video.addTextTrack("subtitles", "A", "en"); + track.mode = "showing"; + ok(track instanceof TextTrack, "Track should be an instanceof TextTrack."); + + info("--- check the type of cue ---"); + isnot(window.TextTrackCue, undefined, "TextTrackCue should be defined."); + isnot(window.VTTCue, undefined, "VTTCue should be defined."); + + var cue = new VTTCue(1, 2, "Test cue"); + ok(cue instanceof TextTrackCue, "Cue should be an instanceof TextTrackCue."); + ok(cue instanceof VTTCue, "Cue should be an instanceof VTTCue."); + + info("--- add cue ---"); + track.addCue(cue); + video.ontimeupdate = function () { + info("--- video.currentTime is " + video.currentTime); + }; + cue.onenter = function () { + cue.onenter = null; + isReceivedOnEnterEvent = true; + var cueChrome = SpecialPowers.wrap(cue); + info("cueChrome.getActive " + cueChrome.getActive); + if (cueChrome.getActive) { + checkCueDisplayState(cue, true /* has display-state */); + } else { + info("This is a missing cue, video.currentTime is "+ video.currentTime); + } + + cue.onexit = function () { + cue.onexit = null; + isReceivedOnExitEvent = true; + checkCueDisplayState(cue, false /* no display-state */); + video.pause(); + } + } +} + +onload = runTest; +</script> +</body> +</html> diff --git a/dom/media/webvtt/test/mochitest/test_webvtt_event_same_time.html b/dom/media/webvtt/test/mochitest/test_webvtt_event_same_time.html new file mode 100644 index 0000000000..e70ff558e1 --- /dev/null +++ b/dom/media/webvtt/test/mochitest/test_webvtt_event_same_time.html @@ -0,0 +1,63 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset='utf-8'> + <title>WebVTT : cue's onenter/onexit event order </title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<div id="content"> +</div> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +var c1exit = false; +var c3enter = false; + +function runTest() { + info("--- create video ---"); + var video = document.createElement("video"); + video.src = "seek.webm"; + video.autoplay = true; + document.getElementById("content").appendChild(video); + + var track = video.addTextTrack("subtitles", "A", "en"); + track.mode = "showing"; + + var cue1 = new VTTCue(1, 2, "Test cue1"); + var cue2 = new VTTCue(2, 3, "Test cue2"); + track.addCue(cue1); + track.addCue(cue2); + + cue1.onexit = function () { + cue1.onexit = null; + c1exit = true; + } + cue2.onenter = function () { + cue2.onenter = null; + ok(c1exit, "cue1 onexit event before than cue2 onenter"); + video.pause(); + SimpleTest.finish(); + } + + var cue3 = new VTTCue(1, 2, "Test cue3"); + var cue4 = new VTTCue(1, 2, "Test cue4"); + track.addCue(cue3); + track.addCue(cue4); + + cue3.onenter = function () { + cue3.onenter = null; + c3enter = true; + } + cue4.onenter = function () { + cue4.onenter = null; + ok(c3enter, "cue3 onenter event before than cue4 onenter"); + } +} + +onload = runTest; +</script> +</body> +</html> diff --git a/dom/media/webvtt/test/mochitest/test_webvtt_infinite_processing_loop.html b/dom/media/webvtt/test/mochitest/test_webvtt_infinite_processing_loop.html new file mode 100644 index 0000000000..c8a9380ca2 --- /dev/null +++ b/dom/media/webvtt/test/mochitest/test_webvtt_infinite_processing_loop.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1580015 - video hangs infinitely during playing subtitle</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="manifest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> + .container { + width: 500px; + height: 300px; + background: pink; + display: flex; + justify-content: center; + } + + video { + min-width: 95%; + max-width: 95%; + max-height: 95%; + } + </style> +</head> +<body> +<div class="container"> +<video id="v" src="gizmo.mp4" controls> + <track src="basic.vtt" kind="subtitles" default> +</video> +</div> +<script type="text/javascript"> +/** + * This test is used to ensure that we don't go into an infinite processing loop + * during playing subtitle when setting those CSS properties on video. + */ +SimpleTest.waitForExplicitFinish(); + +let video = document.getElementById("v"); +// We don't need to play whole video, in order to reduce test time, we can start +// from the half, which can also reproduce the issue. +video.currentTime = 3.0; +video.play(); +video.onended = () => { + ok(true, "video ends without entering an infinite processing loop"); + SimpleTest.finish(); +} + +</script> +</body> +</html> diff --git a/dom/media/webvtt/test/mochitest/test_webvtt_overlapping_time.html b/dom/media/webvtt/test/mochitest/test_webvtt_overlapping_time.html new file mode 100644 index 0000000000..5ce08ae77a --- /dev/null +++ b/dom/media/webvtt/test/mochitest/test_webvtt_overlapping_time.html @@ -0,0 +1,100 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebVTT : cues with overlapping time should be displayed correctly </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="manifest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<video id ="v" src="gizmo.mp4" controls> +<script class="testbody" type="text/javascript"> +/** + * This test is used to ensure that when cues with overlapping times, the one + * with earlier end timestamp should disappear when the media time reaches its + * end time. In this test, we have two cues with overlapping time, when the video + * starts, both cues should be displayed. When the time passes 1 seconds, the + * first cue should disappear and the second cues should be still displayed. + */ +var CUES_INFO = [ + { id: 0, startTime: 0, endTime: 1, text: "This is cue 0."}, + { id: 1, startTime: 0, endTime: 6, text: "This is cue 1."}, +]; + +var video = document.getElementById("v"); + +async function startTest() { + const cues = createCues(); + await startVideo(); + + await waitUntilCueIsShowing(cues[0]); + await waitUntilCueIsShowing(cues[1]); + + await waitUntilCueIsHiding(cues[0]); + await waitUntilCueIsShowing(cues[1]); + IsVideoStillPlaying(); + + endTestAndClearVideo(); +} + +SimpleTest.waitForExplicitFinish(); +onload = startTest; + +/** + * The following are test helper functions. + */ +function createCues() { + let track = video.addTextTrack("subtitles"); + track.mode = "showing"; + let cue0 = new VTTCue(CUES_INFO[0].startTime, CUES_INFO[0].endTime, + CUES_INFO[0].text); + cue0.id = CUES_INFO[0].id; + let cue1 = new VTTCue(CUES_INFO[1].startTime, CUES_INFO[1].endTime, + CUES_INFO[1].text); + cue1.id = CUES_INFO[1].id; + track.addCue(cue0); + track.addCue(cue1); + // Convert them to chrome objects in order to use chrome privilege APIs. + cue0 = SpecialPowers.wrap(cue0); + cue1 = SpecialPowers.wrap(cue1); + return [cue0, cue1]; +} + +async function startVideo() { + info(`start play video`); + const played = video && await video.play().then(() => true, () => false); + ok(played, "video has started playing"); +} + +async function waitUntilCueIsShowing(cue) { + info(`wait until cue ${cue.id} is showing`); + // cue has not been showing yet. + if (!cue.getActive) { + await once(cue, "enter"); + } + info(`video current time=${video.currentTime}`); + ok(cue.getActive, `cue ${cue.id} is showing`); +} + +async function waitUntilCueIsHiding(cue) { + info(`wait until cue ${cue.id} is hiding`); + // cue has not been hidden yet. + if (cue.getActive) { + await once(cue, "exit"); + } + info(`video current time=${video.currentTime}`); + ok(!cue.getActive, `cue ${cue.id} is hidding`); +} + +function IsVideoStillPlaying() { + ok(!video.paused, `video is still playing, currentTime=${video.currentTime}`); +} + +function endTestAndClearVideo() { + removeNodeAndSource(video); + SimpleTest.finish(); +} + +</script> +</body> +</html> diff --git a/dom/media/webvtt/test/mochitest/test_webvtt_positionalign.html b/dom/media/webvtt/test/mochitest/test_webvtt_positionalign.html new file mode 100644 index 0000000000..267fd52f93 --- /dev/null +++ b/dom/media/webvtt/test/mochitest/test_webvtt_positionalign.html @@ -0,0 +1,113 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset='utf-8'> + <title>WebVTT : position align test</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<div id="content"> +</div> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +var video = document.createElement("video"); +var trackElement = document.createElement("track"); +var cuesNumber = 22; + +function isTrackElemenLoaded() { + // Re-que isTrackElemenLoaded() at the end of the event loop until the track + // element has loaded its data. + if (trackElement.readyState == 1) { + setTimeout(isTrackElemenLoaded, 0); + return; + } + + is(trackElement.readyState, 2, "Track::ReadyState should be set to LOADED."); + runTest(); +} + +function runTest() { + info("--- check cues number ---"); + var cues = trackElement.track.cues; + is(cues.length, cuesNumber, "Cues number is correct."); + + info("--- check the typedef of TextTrackCue and VTTCue ---"); + isnot(window.TextTrackCue, undefined, "TextTrackCue should be defined."); + isnot(window.VTTCue, undefined, "VTTCue should be defined."); + + info("--- check the type of first parsed cue ---"); + ok(cues[0] instanceof TextTrackCue, "Cue should be an instanceof TextTrackCue."); + ok(cues[0] instanceof VTTCue, "Cue should be an instanceof VTTCue."); + + info("--- check the cue's position alignment ---"); + let expectedAlignment = ["auto", "line-left", "center", "line-right", "auto"]; + let idx = 0; + for (;idx < expectedAlignment.length; idx++) { + is(cues[idx].positionAlign, expectedAlignment[idx], cues[idx].text); + } + + info("--- check the cue's computed position alignment ---"); + // The "computedPositionAlign" is the chrome-only attributes, we need to get + // the chrome privilege for cues. + let cuesChrome = SpecialPowers.wrap(cues); + expectedAlignment.push("line-left", "line-right", "center"); + for (;idx < expectedAlignment.length; idx++) { + is(cuesChrome[idx].computedPositionAlign, expectedAlignment[idx], + cuesChrome[idx].text); + } + + info(`test only setting text alignment with "start"`); + expectedAlignment.push("line-left", "line-right", "line-left", "line-right", + "line-left", "line-left", "line-right"); + for (;idx < expectedAlignment.length; idx++) { + is(cuesChrome[idx].computedPositionAlign, expectedAlignment[idx], + cuesChrome[idx].text); + } + + info(`test only setting text alignment with "end"`); + expectedAlignment.push("line-right", "line-left", "line-right", "line-left", + "line-right", "line-right", "line-left"); + for (;idx < expectedAlignment.length; idx++) { + is(cuesChrome[idx].computedPositionAlign, expectedAlignment[idx], + cuesChrome[idx].text); + } + is(idx, cuesNumber, "finished checking all cues"); + + info("--- check the cue's computed position alignment from DOM API ---"); + is(cuesChrome[0].computedPositionAlign, "center", "Cue's computedPositionAlign align is center."); + + cuesChrome[0].positionAlign = "auto"; + is(cuesChrome[0].positionAlign, "auto", "Change cue's position align to \"auto\""); + + cuesChrome[0].align = "left"; + is(cuesChrome[0].align, "left", "Change cue's align to \"left\"."); + + is(cuesChrome[0].computedPositionAlign, "line-left", "Cue's computedPositionAlign becomes to \"line-left\""); + + info("--- finish test ---"); + SimpleTest.finish(); +} + +function setupTest() { + info("--- setup test ---"); + video.src = "seek.webm"; + video.preload = "auto"; + + trackElement.src = "vttPositionAlign.vtt"; + trackElement.kind = "subtitles"; + trackElement.default = true; + + document.getElementById("content").appendChild(video); + video.appendChild(trackElement); + video.addEventListener("loadedmetadata", function() { + isTrackElemenLoaded(); + }, {once: true}); +} + +onload = setupTest; +</script> +</body> +</html> diff --git a/dom/media/webvtt/test/mochitest/test_webvtt_seeking.html b/dom/media/webvtt/test/mochitest/test_webvtt_seeking.html new file mode 100644 index 0000000000..1f85969743 --- /dev/null +++ b/dom/media/webvtt/test/mochitest/test_webvtt_seeking.html @@ -0,0 +1,110 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebVTT : cue should be displayed properly after seeking</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="manifest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<script class="testbody" type="text/javascript"> +/** + * This test is used to ensure that the cue should be showed or hid correctly + * after seeking. In this test, we have two cues which are not overlapped, so + * there should only have one cue showing at a time. + */ +var CUES_INFO = [ + { id: 0, startTime: 1, endTime: 3, text: "This is cue 0."}, + { id: 1, startTime: 4, endTime: 6, text: "This is cue 1."}, +]; + +async function startTest() { + const video = createVideo(); + const cues = createCues(video); + await startVideo(video); + + await seekVideo(video, cues[0].startTime); + await waitUntilCueIsShowing(cues[0]); + checkActiveCueAndInactiveCue(cues[0], cues[1]); + + await seekVideo(video, cues[1].startTime); + await waitUntilCueIsShowing(cues[1]); + checkActiveCueAndInactiveCue(cues[1], cues[0]); + + // seek forward again + await seekVideo(video, cues[0].startTime); + await waitUntilCueIsShowing(cues[0]); + checkActiveCueAndInactiveCue(cues[0], cues[1]); + + removeNodeAndSource(video); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +onload = startTest; +/** + * The following are test helper functions. + */ +function checkActiveCueAndInactiveCue(activeCue, inactiveCue) { + ok(activeCue.getActive, + `cue ${activeCue.id} [${activeCue.startTime}:${activeCue.endTime}] is active`); + ok(!inactiveCue.getActive, + `cue ${inactiveCue.id} [${inactiveCue.startTime}:${inactiveCue.endTime}] is inactive`); +} + +function createVideo() { + let video = document.createElement("video"); + video.src = "gizmo.mp4"; + video.controls = true; + document.body.appendChild(video); + return video; +} + +function createCues(video) { + let track = video.addTextTrack("subtitles"); + track.mode = "showing"; + let cue0 = new VTTCue(CUES_INFO[0].startTime, CUES_INFO[0].endTime, + CUES_INFO[0].text); + cue0.id = CUES_INFO[0].id; + let cue1 = new VTTCue(CUES_INFO[1].startTime, CUES_INFO[1].endTime, + CUES_INFO[1].text); + cue1.id = CUES_INFO[1].id; + track.addCue(cue0); + track.addCue(cue1); + // Convert them to chrome objects in order to use chrome privilege APIs. + cue0 = SpecialPowers.wrap(cue0); + cue1 = SpecialPowers.wrap(cue1); + return [cue0, cue1]; +} + +async function startVideo(video) { + info(`start play video`); + const played = video && await video.play().then(() => true, () => false); + ok(played, "video has started playing"); +} + +async function waitUntilCueIsShowing(cue) { + info(`wait until cue ${cue.id} shows`); + // cue has not been showing yet. + if (!cue.getActive) { + await once(cue, "enter"); + } + info(`cue ${cue.id} is showing`); +} + +async function seekVideo(video, time) { + ok(isInRange(time, CUES_INFO[0].startTime, CUES_INFO[0].endTime) || + isInRange(time, CUES_INFO[1].startTime, CUES_INFO[1].endTime), + `seek target time ${time} is within the correct range`) + info(`seek video to ${time}`); + video.currentTime = time; + await once(video, "seeked"); + info(`seek succeeded, current time=${video.currentTime}`); +} + +function isInRange(value, lowerBound, higherBound) { + return lowerBound <= value && value <= higherBound; +} +</script> +</body> +</html> diff --git a/dom/media/webvtt/test/mochitest/test_webvtt_update_display_after_adding_or_removing_cue.html b/dom/media/webvtt/test/mochitest/test_webvtt_update_display_after_adding_or_removing_cue.html new file mode 100644 index 0000000000..7fb8e6761b --- /dev/null +++ b/dom/media/webvtt/test/mochitest/test_webvtt_update_display_after_adding_or_removing_cue.html @@ -0,0 +1,93 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebVTT : cue display should be updated immediately after adding or removing cue</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="manifest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<script class="testbody" type="text/javascript"> +/** + * This test is used to ensure that we will update cue display immediately after + * adding or removing cue after video starts, because `show-poster` flag would be + * reset after video starts, which allows us to process cues instead of showing + * a poster. In this test, we start with adding a cue [0:5] to video, which + * should be showed in the beginning, and then remove the cue later. The cue + * should be removed immediately, not show + */ +async function startTest() { + const video = await createVideo(); + await startVideo(video); + + info(`cue should be showed immediately after it was added.`); + const cue = createCueAndAddCueToVideo(video); + await waitUntilCueShows(cue); + + info(`cue should be hid immediately after it was removed.`); + removeCueFromVideo(cue, video); + checkIfCueHides(cue, video); + + endTestAndClearVideo(video); +} + +SimpleTest.waitForExplicitFinish(); +onload = startTest; + +/** + * The following are test helper functions. + */ +async function createVideo() { + let video = document.createElement("video"); + video.src = "gizmo.mp4"; + video.controls = true; + document.body.appendChild(video); + // wait until media has loaded any data, because we won't update cue if it has + // not got any data. + await once(video, "loadedmetadata"); + return video; +} + +async function startVideo(video) { + info(`start play video`); + const played = video && await video.play().then(() => true, () => false); + ok(played, "video has started playing"); +} + +function createCueAndAddCueToVideo(video) { + let track = video.addTextTrack("subtitles"); + track.mode = "showing"; + let cue = new VTTCue(0, 5, "Test"); + track.addCue(cue); + return cue; +} + +function removeCueFromVideo(cue, video) { + let track = video.textTracks[0]; + track.removeCue(cue); +} + +async function waitUntilCueShows(cue) { + info(`wait until cue shows`); + // cue has not been showed yet. + cue = SpecialPowers.wrap(cue); + if (!cue.getActive) { + await once(cue, "enter"); + } + ok(cue.getActive, `cue has been showed,`); +} + +function checkIfCueHides(cue, video) { + ok(!SpecialPowers.wrap(cue).getActive, `cue has been hidden.`); + ok(video.currentTime < cue.endTime, + `cue is removed at ${video.currentTime}s before reaching its endtime.`); +} + +function endTestAndClearVideo(video) { + removeNodeAndSource(video); + SimpleTest.finish(); +} + +</script> +</body> +</html> diff --git a/dom/media/webvtt/test/mochitest/vttPositionAlign.vtt b/dom/media/webvtt/test/mochitest/vttPositionAlign.vtt new file mode 100644 index 0000000000..7613f4e7c5 --- /dev/null +++ b/dom/media/webvtt/test/mochitest/vttPositionAlign.vtt @@ -0,0 +1,86 @@ +WEBVTT + +00:00.000 --> 00:00.500 +Cue 0 : PositionAlign should be "auto". + +00:00.700 --> 00:00.800 position:50%,line-left +Cue 1 : PositionAlign should be "line-left". + +00:00.700 --> 00:00.800 position:50%,center +Cue 2 : PositionAlign should be "center". + +00:00.700 --> 00:00.800 position:50%,line-right +Cue 3 : PositionAlign should be "line-right". + +00:00.700 --> 00:00.800 position:50%,auto +Cue 4 : PositionAlign should be "auto" + +00:00.700 --> 00:00.800 position:50%,auto align:left +Cue 5 : PositionAlign should be "auto", but computedPositionAlign should be "line-left". + +00:00.700 --> 00:00.800 position:50%,auto align:right +Cue 6 : PositionAlign should be "auto", but computedPositionAlign should be "line-right". + +00:00.700 --> 00:00.800 position:50%,auto align:middle +Cue 7 : PositionAlign should be "auto", but computedPositionAlign should be "center". + +NOTE ### These following cues are set with `align:start` ### + +00:00.700 --> 00:00.800 align:start +LTR character in the beginning and align is "start", so computedPositionAlign should be "line-left". + +00:00.700 --> 00:00.800 align:start +שלום RTL character in the beginning and align is "start", so computedPositionAlign should be "line-right". + +00:00.700 --> 00:00.800 align:start +@ neutral charater in the beginning, but the first strong charater is LTR in "align:start". So computedPositionAlign should be "line-left". + +00:00.700 --> 00:00.800 align:start +@ש neutral charater in the beginning, but the first strong charater is RTL in "align:start". So computedPositionAlign should be "line-right". + +NOTE +This line contains only neutral charater, we would treat its base direction as +LTR. However, if there are other following lines contains non-neutral +charaters, we would detemine the base direction by the following line. + +00:00.700 --> 00:00.800 align:start +@ + +00:00.700 --> 00:00.800 align:start +@ +The second line starts with LTR charater, computedPositionAlign should be "line-left". + +00:00.700 --> 00:00.800 align:start +@ +שThe second line starts with RTL charater, computedPositionAlign should be "line-right". + +NOTE ### These following cues are set with `align:end` ### + +00:00.700 --> 00:00.800 align:end +LTR character in the beginning and align is "end", so computedPositionAlign should be "line-right". + +00:00.700 --> 00:00.800 align:end +ש RTL character in the beginning and align is "end", so computedPositionAlign should be "line-left". + +00:00.700 --> 00:00.800 align:end +@ neutral charater in the beginning, but the first strong charater is LTR in "align:end". So computedPositionAlign should be "line-right". + +00:00.700 --> 00:00.800 align:end +@ש neutral charater in the beginning, but the first strong charater is RTL in "align:end". So computedPositionAlign should be "line-left". + +NOTE +This line contains only neutral charater, we would treat its base direction as +LTR. However, if there are other following lines contains non-neutral +charaters, we would detemine the base direction by the following line. + +00:00.700 --> 00:00.800 align:end +@ + +00:00.700 --> 00:00.800 align:end +@ +The second line starts with LTR charater, computedPositionAlign should be "line-right". + +00:00.700 --> 00:00.800 align:end +@ +שThe second line starts with RTL charater, computedPositionAlign should be "line-left". + diff --git a/dom/media/webvtt/test/reftest/black.mp4 b/dom/media/webvtt/test/reftest/black.mp4 Binary files differnew file mode 100644 index 0000000000..24eb3be139 --- /dev/null +++ b/dom/media/webvtt/test/reftest/black.mp4 diff --git a/dom/media/webvtt/test/reftest/cues_time_overlapping.webvtt b/dom/media/webvtt/test/reftest/cues_time_overlapping.webvtt new file mode 100644 index 0000000000..8ed899a9d8 --- /dev/null +++ b/dom/media/webvtt/test/reftest/cues_time_overlapping.webvtt @@ -0,0 +1,7 @@ +WEBVTT FILE + +00:00:00.000 --> 00:00:01.000 +First cue + +00:00:00.000 --> 00:00:04.000 +Second cue diff --git a/dom/media/webvtt/test/reftest/reftest.list b/dom/media/webvtt/test/reftest/reftest.list new file mode 100644 index 0000000000..8311bac10f --- /dev/null +++ b/dom/media/webvtt/test/reftest/reftest.list @@ -0,0 +1,3 @@ +skip-if(Android) fuzzy-if(/^Windows\x20NT\x2010\.0/.test(http.oscpu)&&/^aarch64-msvc/.test(xulRuntime.XPCOMABI),0-136,0-427680) == vtt_update_display_after_removed_cue.html vtt_update_display_after_removed_cue_ref.html +skip-if(Android) fuzzy-if(winWidget,0-170,0-170) == vtt_overlapping_time.html vtt_overlapping_time-ref.html +skip-if(Android) != vtt_reflow_display.html vtt_reflow_display-ref.html diff --git a/dom/media/webvtt/test/reftest/vtt_overlapping_time-ref.html b/dom/media/webvtt/test/reftest/vtt_overlapping_time-ref.html new file mode 100644 index 0000000000..a44e24678b --- /dev/null +++ b/dom/media/webvtt/test/reftest/vtt_overlapping_time-ref.html @@ -0,0 +1,29 @@ +<!DOCTYPE HTML> +<html class="reftest-wait"> +<head> +</head> +<body> +<video id="v1" src="black.mp4" width="320" height="180"> + <track label="English" src="cues_time_overlapping.webvtt" default> +</video> +<script type="text/javascript"> +/** + * This test is to ensure that when cues with overlapping times, the one with + * earlier end timestamp should disappear when the media time reaches its + * end time. This vtt file contains two cues, the first cue is [0,1], the second + * cue is [0,4], so if we seek video to 2s, only cue2 should be displayed. + */ +async function testTimeOverlappingCues() { + const video = document.getElementById("v1"); + video.currentTime = 2; + video.onseeked = () => { + video.onseeked = null; + document.documentElement.removeAttribute('class'); + } +}; + +window.addEventListener("MozReftestInvalidate", + testTimeOverlappingCues); +</script> +</body> +</html> diff --git a/dom/media/webvtt/test/reftest/vtt_overlapping_time.html b/dom/media/webvtt/test/reftest/vtt_overlapping_time.html new file mode 100644 index 0000000000..f973b398d5 --- /dev/null +++ b/dom/media/webvtt/test/reftest/vtt_overlapping_time.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html class="reftest-wait"> +<head> +</head> +<body> +<video id="v1" src="black.mp4" autoplay width="320" height="180"> + <track label="English" src="cues_time_overlapping.webvtt" default> +</video> +<script type="text/javascript"> +/** + * This test is to ensure that when cues with overlapping times, the one with + * earlier end timestamp should disappear when the media time reaches its + * end time. This vtt file contains two cues, the first cue is [0,1], the second + * cue is [0,4], so after video is playing over 1s, only cue2 should be displayed. + */ +async function testTimeOverlappingCues() { + const video = document.getElementById("v1"); + video.ontimeupdate = () => { + if (video.currentTime > 1.0) { + document.documentElement.removeAttribute('class'); + video.ontimeupdate = null; + } + } +}; + +window.addEventListener("MozReftestInvalidate", + testTimeOverlappingCues); +</script> +</body> +</html> diff --git a/dom/media/webvtt/test/reftest/vtt_reflow_display-ref.html b/dom/media/webvtt/test/reftest/vtt_reflow_display-ref.html new file mode 100644 index 0000000000..19f3208e6b --- /dev/null +++ b/dom/media/webvtt/test/reftest/vtt_reflow_display-ref.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html> +<link rel="stylesheet" href="vtt_reflow_display.css"> +<body> +<div class="video-player"> + <div class="video-layer"> + <video id="v1" autoplay controls></video> + </div> +</div> +<script> +/** + * Simply play and pause a video without any cues. + */ +async function testDisplayCueDuringFrequentReflowRef() { + const video = document.getElementById("v1"); + video.src = "white.webm"; + video.onplay = _ => { + video.onplay = null; + video.pause(); + document.documentElement.removeAttribute('class'); + } +}; + +window.addEventListener("MozReftestInvalidate", + testDisplayCueDuringFrequentReflowRef); +</script> +</body> +</html> diff --git a/dom/media/webvtt/test/reftest/vtt_reflow_display.css b/dom/media/webvtt/test/reftest/vtt_reflow_display.css new file mode 100644 index 0000000000..4b66a07bd4 --- /dev/null +++ b/dom/media/webvtt/test/reftest/vtt_reflow_display.css @@ -0,0 +1,33 @@ +body { + display: flex; + flex-direction: column; + align-items: center; + max-height: 100%; + width: 100vw; + height: 100vh; +} +.video-player { + display: flex; + max-height: calc(100% - 400px); + flex: 1 1 0; + flex-direction: column; + position: relative; + max-width: 100%; + height: 0; +} +.video-layer { + position: relative; + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + flex: 1 1 0; +} +video { + object-fit: contain; + display: flex; + flex: auto; + max-width: 100%; + min-height: 0; + min-width: 0; +} diff --git a/dom/media/webvtt/test/reftest/vtt_reflow_display.html b/dom/media/webvtt/test/reftest/vtt_reflow_display.html new file mode 100644 index 0000000000..e7ec496bc1 --- /dev/null +++ b/dom/media/webvtt/test/reftest/vtt_reflow_display.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html class="reftest-wait"> +<head> +</head> +<link rel="stylesheet" href="vtt_reflow_display.css"> +<body> +<div class="video-player"> + <div class="video-layer"> + <video id="v1" autoplay controls></video> + </div> +</div> +<script> +/** + * In bug 1733232, setting some CSS properties (from bug 1733232 comment17) + * would cause video frame's reflow called very frequently, which crashed the + * video control and caused no cue showing. We compare this test with another + * white video without any cues, and they should NOT be equal. + */ +function testDisplayCueDuringFrequentReflow() { + let video = document.getElementById("v1"); + video.src = "white.webm"; + let cue = new VTTCue(0, 4, "hello testing"); + cue.onenter = _ => { + cue.onenter = null; + video.pause(); + document.documentElement.removeAttribute('class'); + } + let track = video.addTextTrack("captions"); + track.mode = "showing"; + track.addCue(cue); +}; + +window.addEventListener("MozReftestInvalidate", + testDisplayCueDuringFrequentReflow); +</script> +</body> +</html> diff --git a/dom/media/webvtt/test/reftest/vtt_update_display_after_removed_cue.html b/dom/media/webvtt/test/reftest/vtt_update_display_after_removed_cue.html new file mode 100644 index 0000000000..4cd5d5bbd2 --- /dev/null +++ b/dom/media/webvtt/test/reftest/vtt_update_display_after_removed_cue.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html class="reftest-wait"> +<head> +</head> +<body> +<video id="v1" autoplay></video> +<script type="text/javascript"> + +/** + * This test is used to ensure we would update the cue display after removing + * cue from the text track, the removed cue should not display on the video's + * rendering area. + */ +function testUpdateDisplayAfterRemovedCue() { + let video = document.getElementById("v1"); + video.src = "black.mp4"; + let cue = new VTTCue(0, 4, "hello testing"); + let track = video.addTextTrack("captions"); + track.mode = "showing"; + track.addCue(cue); + cue.onenter = () => { + cue.onenter = null; + track.removeCue(cue); + video.pause(); + video.onpause = () => { + video.onpause = null; + document.documentElement.removeAttribute('class'); + } + } +}; + +window.addEventListener("MozReftestInvalidate", + testUpdateDisplayAfterRemovedCue); +</script> +</body> +</html> diff --git a/dom/media/webvtt/test/reftest/vtt_update_display_after_removed_cue_ref.html b/dom/media/webvtt/test/reftest/vtt_update_display_after_removed_cue_ref.html new file mode 100644 index 0000000000..0b1afdc568 --- /dev/null +++ b/dom/media/webvtt/test/reftest/vtt_update_display_after_removed_cue_ref.html @@ -0,0 +1,6 @@ +<!DOCTYPE HTML> +<html> +<body> +<video id="v1" src="black.mp4"></video> +</body> +</html> diff --git a/dom/media/webvtt/test/reftest/white.webm b/dom/media/webvtt/test/reftest/white.webm Binary files differnew file mode 100644 index 0000000000..bbacad7ffd --- /dev/null +++ b/dom/media/webvtt/test/reftest/white.webm diff --git a/dom/media/webvtt/test/xpcshell/test_parser.js b/dom/media/webvtt/test/xpcshell/test_parser.js new file mode 100644 index 0000000000..5ae70f9762 --- /dev/null +++ b/dom/media/webvtt/test/xpcshell/test_parser.js @@ -0,0 +1,158 @@ +"use strict"; + +const { WebVTT } = ChromeUtils.importESModule( + "resource://gre/modules/vtt.sys.mjs" +); + +let fakeWindow = { + /* eslint-disable object-shorthand */ + VTTCue: function () {}, + VTTRegion: function () {}, + /* eslint-enable object-shorthand */ +}; + +// We have a better parser check in WPT. Here I want to check that incomplete +// lines are correctly parsable. +let tests = [ + // Signature + { input: ["WEBVTT"], cue: 0, region: 0 }, + { input: ["", "WE", "BVT", "T"], cue: 0, region: 0 }, + { input: ["WEBVTT - This file has no cues."], cue: 0, region: 0 }, + { input: ["WEBVTT", " - ", "This file has no cues."], cue: 0, region: 0 }, + + // Body with IDs + { + input: [ + "WEB", + "VTT - This file has cues.\n", + "\n", + "14\n", + "00:01:14", + ".815 --> 00:0", + "1:18.114\n", + "- What?\n", + "- Where are we now?\n", + "\n", + "15\n", + "00:01:18.171 --> 00:01:20.991\n", + "- T", + "his is big bat country.\n", + "\n", + "16\n", + "00:01:21.058 --> 00:01:23.868\n", + "- [ Bat", + "s Screeching ]\n", + "- They won't get in your hair. They're after the bug", + "s.\n", + ], + cue: 3, + region: 0, + }, + + // Body without IDs + { + input: [ + "WEBVTT - This file has c", + "ues.\n", + "\n", + "00:01:14.815 --> 00:01:18.114\n", + "- What?\n", + "- Where are we now?\n", + "\n", + "00:01:18.171 --> 00:01:2", + "0.991\n", + "- ", + "This is big bat country.\n", + "\n", + "00:01:21.058 --> 00:01:23.868\n", + "- [ Bats S", + "creeching ]\n", + "- They won't get in your hair. They're after the bugs.\n", + ], + cue: 3, + region: 0, + }, + + // Note + { + input: ["WEBVTT - This file has no cues.\n", "\n", "NOTE what"], + cue: 0, + region: 0, + }, + + // Regions - This vtt is taken from a WPT + { + input: [ + "WE", + "BVTT\n", + "\n", + "REGION\n", + "id:0\n", + "\n", + "REGION\n", + "id:1\n", + "region", + "an", + "chor:0%,0%\n", + "\n", + "R", + "EGION\n", + "id:2\n", + "regionanchor:18446744073709552000%,18446744", + "073709552000%\n", + "\n", + "REGION\n", + "id:3\n", + "regionanchor: 100%,100%\n", + "regio", + "nanchor :100%,100%\n", + "regionanchor:100% ,100%\n", + "regionanchor:100%, 100%\n", + "regionanchor:100 %,100%\n", + "regionanchor:10", + "0%,100 %\n", + "\n", + "00:00:00.000 --> 00:00:01.000", + " region:0\n", + "text\n", + "\n", + "00:00:00.000 --> 00:00:01.000 region:1\n", + "text\n", + "\n", + "00:00:00.000 --> 00:00:01.000 region:3\n", + "text\n", + ], + cue: 3, + region: 4, + }, +]; + +function run_test() { + tests.forEach(test => { + let parser = new WebVTT.Parser(fakeWindow, null); + ok(!!parser, "Ok... this is a good starting point"); + + let cue = 0; + parser.oncue = () => { + ++cue; + }; + + let region = 0; + parser.onregion = () => { + ++region; + }; + + parser.onparsingerror = () => { + ok(false, "No error accepted"); + }; + + test.input.forEach(input => { + parser.parse(new TextEncoder().encode(input)); + }); + + parser.flush(); + + equal(cue, test.cue, "Cue value matches"); + equal(region, test.region, "Region value matches"); + }); +} diff --git a/dom/media/webvtt/test/xpcshell/xpcshell.ini b/dom/media/webvtt/test/xpcshell/xpcshell.ini new file mode 100644 index 0000000000..07aa5d80d8 --- /dev/null +++ b/dom/media/webvtt/test/xpcshell/xpcshell.ini @@ -0,0 +1,3 @@ +[DEFAULT] + +[test_parser.js] diff --git a/dom/media/webvtt/update-webvtt.js b/dom/media/webvtt/update-webvtt.js new file mode 100644 index 0000000000..20a3e2669f --- /dev/null +++ b/dom/media/webvtt/update-webvtt.js @@ -0,0 +1,61 @@ +#!/usr/bin/env node + +/* eslint-env node */ + +var gift = require("gift"), + fs = require("fs"), + argv = require("optimist") + .usage( + "Update vtt.jsm with the latest from a vtt.js directory.\nUsage:" + + " $0 -d [dir]" + ) + .demand("d") + .options("d", { + alias: "dir", + describe: "Path to WebVTT directory.", + }) + .options("r", { + alias: "rev", + describe: "Revision to update to.", + default: "master", + }) + .options("w", { + alias: "write", + describe: "Path to file to write to.", + default: "./vtt.jsm", + }).argv; + +var repo = gift(argv.d); +repo.status(function (err, status) { + if (!status.clean) { + console.log("The repository's working directory is not clean. Aborting."); + process.exit(1); + } + repo.checkout(argv.r, function () { + repo.commits(argv.r, 1, function (err, commits) { + var vttjs = fs.readFileSync(argv.d + "/lib/vtt.js", "utf8"); + + // Remove settings for VIM and Emacs. + vttjs = vttjs.replace(/\/\* -\*-.*-\*- \*\/\n/, ""); + vttjs = vttjs.replace(/\/\* vim:.* \*\/\n/, ""); + + // Concatenate header and vttjs code. + vttjs = + "/* This Source Code Form is subject to the terms of the Mozilla Public\n" + + " * License, v. 2.0. If a copy of the MPL was not distributed with this\n" + + " * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\n" + + "export var WebVTT;" + + "/**\n" + + " * Code below is vtt.js the JS WebVTT implementation.\n" + + " * Current source code can be found at http://github.com/mozilla/vtt.js\n" + + " *\n" + + " * Code taken from commit " + + commits[0].id + + "\n" + + " */\n" + + vttjs; + + fs.writeFileSync(argv.w, vttjs); + }); + }); +}); diff --git a/dom/media/webvtt/vtt.sys.mjs b/dom/media/webvtt/vtt.sys.mjs new file mode 100644 index 0000000000..8b4a830e7f --- /dev/null +++ b/dom/media/webvtt/vtt.sys.mjs @@ -0,0 +1,1663 @@ +/* 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/. */ + +/** + * Code below is vtt.js the JS WebVTT implementation. + * Current source code can be found at http://github.com/mozilla/vtt.js + * + * Code taken from commit b89bfd06cd788a68c67e03f44561afe833db0849 + */ +/** + * Copyright 2013 vtt.js Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; +XPCOMUtils.defineLazyPreferenceGetter(lazy, "supportPseudo", + "media.webvtt.pseudo.enabled", false); +XPCOMUtils.defineLazyPreferenceGetter(lazy, "DEBUG_LOG", + "media.webvtt.debug.logging", false); + +function LOG(message) { + if (lazy.DEBUG_LOG) { + dump("[vtt] " + message + "\n"); + } +} + +var _objCreate = Object.create || (function() { + function F() {} + return function(o) { + if (arguments.length !== 1) { + throw new Error('Object.create shim only accepts one parameter.'); + } + F.prototype = o; + return new F(); + }; +})(); + +// Creates a new ParserError object from an errorData object. The errorData +// object should have default code and message properties. The default message +// property can be overriden by passing in a message parameter. +// See ParsingError.Errors below for acceptable errors. +function ParsingError(errorData, message) { + this.name = "ParsingError"; + this.code = errorData.code; + this.message = message || errorData.message; +} +ParsingError.prototype = _objCreate(Error.prototype); +ParsingError.prototype.constructor = ParsingError; + +// ParsingError metadata for acceptable ParsingErrors. +ParsingError.Errors = { + BadSignature: { + code: 0, + message: "Malformed WebVTT signature." + }, + BadTimeStamp: { + code: 1, + message: "Malformed time stamp." + } +}; + +// See spec, https://w3c.github.io/webvtt/#collect-a-webvtt-timestamp. +function collectTimeStamp(input) { + function computeSeconds(h, m, s, f) { + if (m > 59 || s > 59) { + return null; + } + // The attribute of the milli-seconds can only be three digits. + if (f.length !== 3) { + return null; + } + return (h | 0) * 3600 + (m | 0) * 60 + (s | 0) + (f | 0) / 1000; + } + + let timestamp = input.match(/^(\d+:)?(\d{2}):(\d{2})\.(\d+)/); + if (!timestamp || timestamp.length !== 5) { + return null; + } + + let hours = timestamp[1]? timestamp[1].replace(":", "") : 0; + let minutes = timestamp[2]; + let seconds = timestamp[3]; + let milliSeconds = timestamp[4]; + + return computeSeconds(hours, minutes, seconds, milliSeconds); +} + +// A settings object holds key/value pairs and will ignore anything but the first +// assignment to a specific key. +function Settings() { + this.values = _objCreate(null); +} + +Settings.prototype = { + set: function(k, v) { + if (v !== "") { + this.values[k] = v; + } + }, + // Return the value for a key, or a default value. + // If 'defaultKey' is passed then 'dflt' is assumed to be an object with + // a number of possible default values as properties where 'defaultKey' is + // the key of the property that will be chosen; otherwise it's assumed to be + // a single value. + get: function(k, dflt, defaultKey) { + if (defaultKey) { + return this.has(k) ? this.values[k] : dflt[defaultKey]; + } + return this.has(k) ? this.values[k] : dflt; + }, + // Check whether we have a value for a key. + has: function(k) { + return k in this.values; + }, + // Accept a setting if its one of the given alternatives. + alt: function(k, v, a) { + for (let n = 0; n < a.length; ++n) { + if (v === a[n]) { + this.set(k, v); + return true; + } + } + return false; + }, + // Accept a setting if its a valid digits value (int or float) + digitsValue: function(k, v) { + if (/^-0+(\.[0]*)?$/.test(v)) { // special case for -0.0 + this.set(k, 0.0); + } else if (/^-?\d+(\.[\d]*)?$/.test(v)) { + this.set(k, parseFloat(v)); + } + }, + // Accept a setting if its a valid percentage. + percent: function(k, v) { + let m; + if ((m = v.match(/^([\d]{1,3})(\.[\d]*)?%$/))) { + v = parseFloat(v); + if (v >= 0 && v <= 100) { + this.set(k, v); + return true; + } + } + return false; + }, + // Delete a setting + del: function (k) { + if (this.has(k)) { + delete this.values[k]; + } + }, +}; + +// Helper function to parse input into groups separated by 'groupDelim', and +// interprete each group as a key/value pair separated by 'keyValueDelim'. +function parseOptions(input, callback, keyValueDelim, groupDelim) { + let groups = groupDelim ? input.split(groupDelim) : [input]; + for (let i in groups) { + if (typeof groups[i] !== "string") { + continue; + } + let kv = groups[i].split(keyValueDelim); + if (kv.length !== 2) { + continue; + } + let k = kv[0]; + let v = kv[1]; + callback(k, v); + } +} + +function parseCue(input, cue, regionList) { + // Remember the original input if we need to throw an error. + let oInput = input; + // 4.1 WebVTT timestamp + function consumeTimeStamp() { + let ts = collectTimeStamp(input); + if (ts === null) { + throw new ParsingError(ParsingError.Errors.BadTimeStamp, + "Malformed timestamp: " + oInput); + } + // Remove time stamp from input. + input = input.replace(/^[^\s\uFFFDa-zA-Z-]+/, ""); + return ts; + } + + // 4.4.2 WebVTT cue settings + function consumeCueSettings(input, cue) { + let settings = new Settings(); + parseOptions(input, function (k, v) { + switch (k) { + case "region": + // Find the last region we parsed with the same region id. + for (let i = regionList.length - 1; i >= 0; i--) { + if (regionList[i].id === v) { + settings.set(k, regionList[i].region); + break; + } + } + break; + case "vertical": + settings.alt(k, v, ["rl", "lr"]); + break; + case "line": { + let vals = v.split(","); + let vals0 = vals[0]; + settings.digitsValue(k, vals0); + settings.percent(k, vals0) ? settings.set("snapToLines", false) : null; + settings.alt(k, vals0, ["auto"]); + if (vals.length === 2) { + settings.alt("lineAlign", vals[1], ["start", "center", "end"]); + } + break; + } + case "position": { + let vals = v.split(","); + if (settings.percent(k, vals[0])) { + if (vals.length === 2) { + if (!settings.alt("positionAlign", vals[1], ["line-left", "center", "line-right"])) { + // Remove the "position" value because the "positionAlign" is not expected value. + // It will be set to default value below. + settings.del(k); + } + } + } + break; + } + case "size": + settings.percent(k, v); + break; + case "align": + settings.alt(k, v, ["start", "center", "end", "left", "right"]); + break; + } + }, /:/, /\t|\n|\f|\r| /); // groupDelim is ASCII whitespace + + // Apply default values for any missing fields. + // https://w3c.github.io/webvtt/#collect-a-webvtt-block step 11.4.1.3 + cue.region = settings.get("region", null); + cue.vertical = settings.get("vertical", ""); + cue.line = settings.get("line", "auto"); + cue.lineAlign = settings.get("lineAlign", "start"); + cue.snapToLines = settings.get("snapToLines", true); + cue.size = settings.get("size", 100); + cue.align = settings.get("align", "center"); + cue.position = settings.get("position", "auto"); + cue.positionAlign = settings.get("positionAlign", "auto"); + } + + function skipWhitespace() { + input = input.replace(/^[ \f\n\r\t]+/, ""); + } + + // 4.1 WebVTT cue timings. + skipWhitespace(); + cue.startTime = consumeTimeStamp(); // (1) collect cue start time + skipWhitespace(); + if (input.substr(0, 3) !== "-->") { // (3) next characters must match "-->" + throw new ParsingError(ParsingError.Errors.BadTimeStamp, + "Malformed time stamp (time stamps must be separated by '-->'): " + + oInput); + } + input = input.substr(3); + skipWhitespace(); + cue.endTime = consumeTimeStamp(); // (5) collect cue end time + + // 4.1 WebVTT cue settings list. + skipWhitespace(); + consumeCueSettings(input, cue); +} + +function emptyOrOnlyContainsWhiteSpaces(input) { + return input == "" || /^[ \f\n\r\t]+$/.test(input); +} + +function containsTimeDirectionSymbol(input) { + return input.includes("-->"); +} + +function maybeIsTimeStampFormat(input) { + return /^\s*(\d+:)?(\d{2}):(\d{2})\.(\d+)\s*-->\s*(\d+:)?(\d{2}):(\d{2})\.(\d+)\s*/.test(input); +} + +var ESCAPE = { + "&": "&", + "<": "<", + ">": ">", + "‎": "\u200e", + "‏": "\u200f", + " ": "\u00a0" +}; + +var TAG_NAME = { + c: "span", + i: "i", + b: "b", + u: "u", + ruby: "ruby", + rt: "rt", + v: "span", + lang: "span" +}; + +var TAG_ANNOTATION = { + v: "title", + lang: "lang" +}; + +var NEEDS_PARENT = { + rt: "ruby" +}; + +const PARSE_CONTENT_MODE = { + NORMAL_CUE: "normal_cue", + PSUEDO_CUE: "pseudo_cue", + DOCUMENT_FRAGMENT: "document_fragment", + REGION_CUE: "region_cue", +} +// Parse content into a document fragment. +function parseContent(window, input, mode) { + function nextToken() { + // Check for end-of-string. + if (!input) { + return null; + } + + // Consume 'n' characters from the input. + function consume(result) { + input = input.substr(result.length); + return result; + } + + let m = input.match(/^([^<]*)(<[^>]+>?)?/); + // The input doesn't contain a complete tag. + if (!m[0]) { + return null; + } + // If there is some text before the next tag, return it, otherwise return + // the tag. + return consume(m[1] ? m[1] : m[2]); + } + + // Unescape a string 's'. + function unescape1(e) { + return ESCAPE[e]; + } + function unescape(s) { + let m; + while ((m = s.match(/&(amp|lt|gt|lrm|rlm|nbsp);/))) { + s = s.replace(m[0], unescape1); + } + return s; + } + + function shouldAdd(current, element) { + return !NEEDS_PARENT[element.localName] || + NEEDS_PARENT[element.localName] === current.localName; + } + + // Create an element for this tag. + function createElement(type, annotation) { + let tagName = TAG_NAME[type]; + if (!tagName) { + return null; + } + let element = window.document.createElement(tagName); + let name = TAG_ANNOTATION[type]; + if (name) { + element[name] = annotation ? annotation.trim() : ""; + } + return element; + } + + // https://w3c.github.io/webvtt/#webvtt-timestamp-object + // Return hhhhh:mm:ss.fff + function normalizedTimeStamp(secondsWithFrag) { + let totalsec = parseInt(secondsWithFrag, 10); + let hours = Math.floor(totalsec / 3600); + let minutes = Math.floor(totalsec % 3600 / 60); + let seconds = Math.floor(totalsec % 60); + if (hours < 10) { + hours = "0" + hours; + } + if (minutes < 10) { + minutes = "0" + minutes; + } + if (seconds < 10) { + seconds = "0" + seconds; + } + let f = secondsWithFrag.toString().split("."); + if (f[1]) { + f = f[1].slice(0, 3).padEnd(3, "0"); + } else { + f = "000"; + } + return hours + ':' + minutes + ':' + seconds + '.' + f; + } + + let root; + switch (mode) { + case PARSE_CONTENT_MODE.PSUEDO_CUE: + root = window.document.createElement("span", {pseudo: "::cue"}); + break; + case PARSE_CONTENT_MODE.NORMAL_CUE: + case PARSE_CONTENT_MODE.REGION_CUE: + root = window.document.createElement("span"); + break; + case PARSE_CONTENT_MODE.DOCUMENT_FRAGMENT: + root = window.document.createDocumentFragment(); + break; + } + + if (!input) { + root.appendChild(window.document.createTextNode("")); + return root; + } + + let current = root, + t, + tagStack = []; + + while ((t = nextToken()) !== null) { + if (t[0] === '<') { + if (t[1] === "/") { + // If the closing tag matches, move back up to the parent node. + if (tagStack.length && + tagStack[tagStack.length - 1] === t.substr(2).replace(">", "")) { + tagStack.pop(); + current = current.parentNode; + } + // Otherwise just ignore the end tag. + continue; + } + let ts = collectTimeStamp(t.substr(1, t.length - 1)); + let node; + if (ts) { + // Timestamps are lead nodes as well. + node = window.document.createProcessingInstruction("timestamp", normalizedTimeStamp(ts)); + current.appendChild(node); + continue; + } + let m = t.match(/^<([^.\s/0-9>]+)(\.[^\s\\>]+)?([^>\\]+)?(\\?)>?$/); + // If we can't parse the tag, skip to the next tag. + if (!m) { + continue; + } + // Try to construct an element, and ignore the tag if we couldn't. + node = createElement(m[1], m[3]); + if (!node) { + continue; + } + // Determine if the tag should be added based on the context of where it + // is placed in the cuetext. + if (!shouldAdd(current, node)) { + continue; + } + // Set the class list (as a list of classes, separated by space). + if (m[2]) { + node.className = m[2].substr(1).replace('.', ' '); + } + // Append the node to the current node, and enter the scope of the new + // node. + tagStack.push(m[1]); + current.appendChild(node); + current = node; + continue; + } + + // Text nodes are leaf nodes. + current.appendChild(window.document.createTextNode(unescape(t))); + } + + return root; +} + +function StyleBox() { +} + +// Apply styles to a div. If there is no div passed then it defaults to the +// div on 'this'. +StyleBox.prototype.applyStyles = function(styles, div) { + div = div || this.div; + for (let prop in styles) { + if (styles.hasOwnProperty(prop)) { + div.style[prop] = styles[prop]; + } + } +}; + +StyleBox.prototype.formatStyle = function(val, unit) { + return val === 0 ? 0 : val + unit; +}; + +// TODO(alwu): remove StyleBox and change other style box to class-based. +class StyleBoxBase { + applyStyles(styles, div) { + div = div || this.div; + Object.assign(div.style, styles); + } + + formatStyle(val, unit) { + return val === 0 ? 0 : val + unit; + } +} + +// Constructs the computed display state of the cue (a div). Places the div +// into the overlay which should be a block level element (usually a div). +class CueStyleBox extends StyleBoxBase { + constructor(window, cue, containerBox) { + super(); + this.cue = cue; + this.div = window.document.createElement("div"); + this.cueDiv = parseContent(window, cue.text, lazy.supportPseudo ? + PARSE_CONTENT_MODE.PSUEDO_CUE : PARSE_CONTENT_MODE.NORMAL_CUE); + this.div.appendChild(this.cueDiv); + + this.containerHeight = containerBox.height; + this.containerWidth = containerBox.width; + this.fontSize = this._getFontSize(containerBox); + this.isCueStyleBox = true; + + // As pseudo element won't inherit the parent div's style, so we have to + // set the font size explicitly. + if (lazy.supportPseudo) { + this._applyDefaultStylesOnPseudoBackgroundNode(); + } else { + this._applyDefaultStylesOnNonPseudoBackgroundNode(); + } + this._applyDefaultStylesOnRootNode(); + } + + getCueBoxPositionAndSize() { + // As `top`, `left`, `width` and `height` are all represented by the + // percentage of the container, we need to convert them to the actual + // number according to the container's size. + const isWritingDirectionHorizontal = this.cue.vertical == ""; + let top = + this.containerHeight * this._tranferPercentageToFloat(this.div.style.top), + left = + this.containerWidth * this._tranferPercentageToFloat(this.div.style.left), + width = isWritingDirectionHorizontal ? + this.containerWidth * this._tranferPercentageToFloat(this.div.style.width) : + this.div.clientWidthDouble, + height = isWritingDirectionHorizontal ? + this.div.clientHeightDouble : + this.containerHeight * this._tranferPercentageToFloat(this.div.style.height); + return { top, left, width, height }; + } + + getFirstLineBoxSize() { + // This size would be automatically adjusted by writing direction. When + // direction is horizontal, it represents box's height. When direction is + // vertical, it represents box's width. + return this.div.firstLineBoxBSize; + } + + setBidiRule() { + // This function is a workaround which is used to force the reflow in order + // to use the correct alignment for bidi text. Now this function would be + // called after calculating the final position of the cue box to ensure the + // rendering result is correct. See bug1557882 comment3 for more details. + // TODO : remove this function and set `unicode-bidi` when initiailizing + // the CueStyleBox, after fixing bug1558431. + this.applyStyles({ "unicode-bidi": "plaintext" }); + } + + /** + * Following methods are private functions, should not use them outside this + * class. + */ + _tranferPercentageToFloat(input) { + return input.replace("%", "") / 100.0; + } + + _getFontSize(containerBox) { + // In https://www.w3.org/TR/webvtt1/#applying-css-properties, the spec + // said the font size is '5vh', which means 5% of the viewport height. + // However, if we use 'vh' as a basic unit, it would eventually become + // 5% of screen height, instead of video's viewport height. Therefore, we + // have to use 'px' here to make sure we have the correct font size. + return containerBox.height * 0.05 + "px"; + } + + _applyDefaultStylesOnPseudoBackgroundNode() { + // most of the properties have been defined in `::cue` in `html.css`, but + // there are some css variables we have to set them dynamically. + this.cueDiv.style.setProperty("--cue-font-size", this.fontSize, "important"); + this.cueDiv.style.setProperty("--cue-writing-mode", this._getCueWritingMode(), "important"); + } + + _applyDefaultStylesOnNonPseudoBackgroundNode() { + // If cue div is not a pseudo element, we should set the default css style + // for it, the reason we need to set these attributes to cueDiv is because + // if we set background on the root node directly, if would cause filling + // too large area for the background color as the size of root node won't + // be adjusted by cue size. + this.applyStyles({ + "background-color": "rgba(0, 0, 0, 0.8)", + }, this.cueDiv); + } + + // spec https://www.w3.org/TR/webvtt1/#applying-css-properties + _applyDefaultStylesOnRootNode() { + // The variables writing-mode, top, left, width, and height are calculated + // in the spec 7.2, https://www.w3.org/TR/webvtt1/#processing-cue-settings + // spec 7.2.1, calculate 'writing-mode'. + const writingMode = this._getCueWritingMode(); + + // spec 7.2.2 ~ 7.2.7, calculate 'width', 'height', 'left' and 'top'. + const {width, height, left, top} = this._getCueSizeAndPosition(); + + this.applyStyles({ + "position": "absolute", + // "unicode-bidi": "plaintext", (uncomment this line after fixing bug1558431) + "writing-mode": writingMode, + "top": top, + "left": left, + "width": width, + "height": height, + "overflow-wrap": "break-word", + // "text-wrap": "balance", (we haven't supported this CSS attribute yet) + "white-space": "pre-line", + "font": this.fontSize + " sans-serif", + "color": "rgba(255, 255, 255, 1)", + "white-space": "pre-line", + "text-align": this.cue.align, + }); + } + + _getCueWritingMode() { + const cue = this.cue; + if (cue.vertical == "") { + return "horizontal-tb"; + } + return cue.vertical == "lr" ? "vertical-lr" : "vertical-rl"; + } + + _getCueSizeAndPosition() { + const cue = this.cue; + // spec 7.2.2, determine the value of maximum size for cue as per the + // appropriate rules from the following list. + let maximumSize; + let computedPosition = cue.computedPosition; + switch (cue.computedPositionAlign) { + case "line-left": + maximumSize = 100 - computedPosition; + break; + case "line-right": + maximumSize = computedPosition; + break; + case "center": + maximumSize = computedPosition <= 50 ? + computedPosition * 2 : (100 - computedPosition) * 2; + break; + } + const size = Math.min(cue.size, maximumSize); + + // spec 7.2.5, determine the value of x-position or y-position for cue as + // per the appropriate rules from the following list. + let xPosition = 0.0, yPosition = 0.0; + const isWritingDirectionHorizontal = cue.vertical == ""; + switch (cue.computedPositionAlign) { + case "line-left": + if (isWritingDirectionHorizontal) { + xPosition = cue.computedPosition; + } else { + yPosition = cue.computedPosition; + } + break; + case "center": + if (isWritingDirectionHorizontal) { + xPosition = cue.computedPosition - (size / 2); + } else { + yPosition = cue.computedPosition - (size / 2); + } + break; + case "line-right": + if (isWritingDirectionHorizontal) { + xPosition = cue.computedPosition - size; + } else { + yPosition = cue.computedPosition - size; + } + break; + } + + // spec 7.2.6, determine the value of whichever of x-position or + // y-position is not yet calculated for cue as per the appropriate rules + // from the following list. + if (!cue.snapToLines) { + if (isWritingDirectionHorizontal) { + yPosition = cue.computedLine; + } else { + xPosition = cue.computedLine; + } + } else { + if (isWritingDirectionHorizontal) { + yPosition = 0; + } else { + xPosition = 0; + } + } + return { + left: xPosition + "%", + top: yPosition + "%", + width: isWritingDirectionHorizontal ? size + "%" : "auto", + height: isWritingDirectionHorizontal ? "auto" : size + "%", + }; + } +} + +function RegionNodeBox(window, region, container) { + StyleBox.call(this); + + let boxLineHeight = container.height * 0.0533 // 0.0533vh ? 5.33vh + let boxHeight = boxLineHeight * region.lines; + let boxWidth = container.width * region.width / 100; // convert percentage to px + + let regionNodeStyles = { + position: "absolute", + height: boxHeight + "px", + width: boxWidth + "px", + top: (region.viewportAnchorY * container.height / 100) - (region.regionAnchorY * boxHeight / 100) + "px", + left: (region.viewportAnchorX * container.width / 100) - (region.regionAnchorX * boxWidth / 100) + "px", + lineHeight: boxLineHeight + "px", + writingMode: "horizontal-tb", + backgroundColor: "rgba(0, 0, 0, 0.8)", + wordWrap: "break-word", + overflowWrap: "break-word", + font: (boxLineHeight/1.3) + "px sans-serif", + color: "rgba(255, 255, 255, 1)", + overflow: "hidden", + minHeight: "0px", + maxHeight: boxHeight + "px", + display: "inline-flex", + flexFlow: "column", + justifyContent: "flex-end", + }; + + this.div = window.document.createElement("div"); + this.div.id = region.id; // useless? + this.applyStyles(regionNodeStyles); +} +RegionNodeBox.prototype = _objCreate(StyleBox.prototype); +RegionNodeBox.prototype.constructor = RegionNodeBox; + +function RegionCueStyleBox(window, cue) { + StyleBox.call(this); + this.cueDiv = parseContent(window, cue.text, PARSE_CONTENT_MODE.REGION_CUE); + + let regionCueStyles = { + position: "relative", + writingMode: "horizontal-tb", + unicodeBidi: "plaintext", + width: "auto", + height: "auto", + textAlign: cue.align, + }; + // TODO: fix me, LTR and RTL ? using margin replace the "left/right" + // 6.1.14.3.3 + let offset = cue.computedPosition * cue.region.width / 100; + // 6.1.14.3.4 + switch (cue.align) { + case "start": + case "left": + regionCueStyles.left = offset + "%"; + regionCueStyles.right = "auto"; + break; + case "end": + case "right": + regionCueStyles.left = "auto"; + regionCueStyles.right = offset + "%"; + break; + case "middle": + break; + } + + this.div = window.document.createElement("div"); + this.applyStyles(regionCueStyles); + this.div.appendChild(this.cueDiv); +} +RegionCueStyleBox.prototype = _objCreate(StyleBox.prototype); +RegionCueStyleBox.prototype.constructor = RegionCueStyleBox; + +// Represents the co-ordinates of an Element in a way that we can easily +// compute things with such as if it overlaps or intersects with other boxes. +class BoxPosition { + constructor(obj) { + // Get dimensions by calling getCueBoxPositionAndSize on a CueStyleBox, by + // getting offset properties from an HTMLElement (from the object or its + // `div` property), otherwise look at the regular box properties on the + // object. + const isHTMLElement = !obj.isCueStyleBox && (obj.div || obj.tagName); + obj = obj.isCueStyleBox ? obj.getCueBoxPositionAndSize() : obj.div || obj; + this.top = isHTMLElement ? obj.offsetTop : obj.top; + this.left = isHTMLElement ? obj.offsetLeft : obj.left; + this.width = isHTMLElement ? obj.offsetWidth : obj.width; + this.height = isHTMLElement ? obj.offsetHeight : obj.height; + // This value is smaller than 1 app unit (~= 0.0166 px). + this.fuzz = 0.01; + } + + get bottom() { + return this.top + this.height; + } + + get right() { + return this.left + this.width; + } + + // This function is used for debugging, it will return the box's information. + getBoxInfoInChars() { + return `top=${this.top}, bottom=${this.bottom}, left=${this.left}, ` + + `right=${this.right}, width=${this.width}, height=${this.height}`; + } + + // Move the box along a particular axis. Optionally pass in an amount to move + // the box. If no amount is passed then the default is the line height of the + // box. + move(axis, toMove) { + switch (axis) { + case "+x": + LOG(`box's left moved from ${this.left} to ${this.left + toMove}`); + this.left += toMove; + break; + case "-x": + LOG(`box's left moved from ${this.left} to ${this.left - toMove}`); + this.left -= toMove; + break; + case "+y": + LOG(`box's top moved from ${this.top} to ${this.top + toMove}`); + this.top += toMove; + break; + case "-y": + LOG(`box's top moved from ${this.top} to ${this.top - toMove}`); + this.top -= toMove; + break; + } + } + + // Check if this box overlaps another box, b2. + overlaps(b2) { + return (this.left < b2.right - this.fuzz) && + (this.right > b2.left + this.fuzz) && + (this.top < b2.bottom - this.fuzz) && + (this.bottom > b2.top + this.fuzz); + } + + // Check if this box overlaps any other boxes in boxes. + overlapsAny(boxes) { + for (let i = 0; i < boxes.length; i++) { + if (this.overlaps(boxes[i])) { + return true; + } + } + return false; + } + + // Check if this box is within another box. + within(container) { + return (this.top >= container.top - this.fuzz) && + (this.bottom <= container.bottom + this.fuzz) && + (this.left >= container.left - this.fuzz) && + (this.right <= container.right + this.fuzz); + } + + // Check whether this box is passed over the specfic axis boundary. The axis + // is based on the canvas coordinates, the `+x` is rightward and `+y` is + // downward. + isOutsideTheAxisBoundary(container, axis) { + switch (axis) { + case "+x": + return this.right > container.right + this.fuzz; + case "-x": + return this.left < container.left - this.fuzz; + case "+y": + return this.bottom > container.bottom + this.fuzz; + case "-y": + return this.top < container.top - this.fuzz; + } + } + + // Find the percentage of the area that this box is overlapping with another + // box. + intersectPercentage(b2) { + let x = Math.max(0, Math.min(this.right, b2.right) - Math.max(this.left, b2.left)), + y = Math.max(0, Math.min(this.bottom, b2.bottom) - Math.max(this.top, b2.top)), + intersectArea = x * y; + return intersectArea / (this.height * this.width); + } +} + +BoxPosition.prototype.clone = function(){ + return new BoxPosition(this); +}; + +function adjustBoxPosition(styleBox, containerBox, controlBarBox, outputBoxes) { + const cue = styleBox.cue; + const isWritingDirectionHorizontal = cue.vertical == ""; + let box = new BoxPosition(styleBox); + if (!box.width || !box.height) { + LOG(`No way to adjust a box with zero width or height.`); + return; + } + + // Spec 7.2.10, adjust the positions of boxes according to the appropriate + // steps from the following list. Also, we use offsetHeight/offsetWidth here + // in order to prevent the incorrect positioning caused by CSS transform + // scale. + const fullDimension = isWritingDirectionHorizontal ? + containerBox.height : containerBox.width; + if (cue.snapToLines) { + LOG(`Adjust position when 'snap-to-lines' is true.`); + // The step is the height or width of the line box. We should use font + // size directly, instead of using text box's width or height, because the + // width or height of the box would be changed when the text is wrapped to + // different line. Ex. if text is wrapped to two line, the height or width + // of the box would become 2 times of font size. + let step = styleBox.getFirstLineBoxSize(); + if (step == 0) { + return; + } + + // spec 7.2.10.4 ~ 7.2.10.6 + let line = Math.floor(cue.computedLine + 0.5); + if (cue.vertical == "rl") { + line = -1 * (line + 1); + } + + // spec 7.2.10.7 ~ 7.2.10.8 + let position = step * line; + if (cue.vertical == "rl") { + position = position - box.width + step; + } + + // spec 7.2.10.9 + if (line < 0) { + position += fullDimension; + step = -1 * step; + } + + // spec 7.2.10.10, move the box to the specific position along the direction. + const movingDirection = isWritingDirectionHorizontal ? "+y" : "+x"; + box.move(movingDirection, position); + + // spec 7.2.10.11, remember the position as specified position. + let specifiedPosition = box.clone(); + + // spec 7.2.10.12, let title area be a box that covers all of the video’s + // rendering area. + const titleAreaBox = containerBox.clone(); + if (controlBarBox) { + titleAreaBox.height -= controlBarBox.height; + } + + function isBoxOutsideTheRenderingArea() { + if (isWritingDirectionHorizontal) { + // the top side of the box is above the rendering area, or the bottom + // side of the box is below the rendering area. + return step < 0 && box.top < 0 || + step > 0 && box.bottom > fullDimension; + } + // the left side of the box is outside the left side of the rendering + // area, or the right side of the box is outside the right side of the + // rendering area. + return step < 0 && box.left < 0 || + step > 0 && box.right > fullDimension; + } + + // spec 7.2.10.13, if none of the boxes in boxes would overlap any of the + // boxes in output, and all of the boxes in boxes are entirely within the + // title area box. + let switched = false; + while (!box.within(titleAreaBox) || box.overlapsAny(outputBoxes)) { + // spec 7.2.10.14, check if we need to switch the direction. + if (isBoxOutsideTheRenderingArea()) { + // spec 7.2.10.17, if `switched` is true, remove all the boxes in + // `boxes`, which means we shouldn't apply any CSS boxes for this cue. + // Therefore, returns null box. + if (switched) { + return null; + } + // spec 7.2.10.18 ~ 7.2.10.20 + switched = true; + box = specifiedPosition.clone(); + step = -1 * step; + } + // spec 7.2.10.15, moving box along the specific direction. + box.move(movingDirection, step); + } + + if (isWritingDirectionHorizontal) { + styleBox.applyStyles({ + top: getPercentagePosition(box.top, fullDimension), + }); + } else { + styleBox.applyStyles({ + left: getPercentagePosition(box.left, fullDimension), + }); + } + } else { + LOG(`Adjust position when 'snap-to-lines' is false.`); + // (snap-to-lines if false) spec 7.2.10.1 ~ 7.2.10.2 + if (cue.lineAlign != "start") { + const isCenterAlign = cue.lineAlign == "center"; + const movingDirection = isWritingDirectionHorizontal ? "-y" : "-x"; + if (isWritingDirectionHorizontal) { + box.move(movingDirection, isCenterAlign ? box.height : box.height / 2); + } else { + box.move(movingDirection, isCenterAlign ? box.width : box.width / 2); + } + } + + // spec 7.2.10.3 + let bestPosition = {}, + specifiedPosition = box.clone(), + outsideAreaPercentage = 1; // Highest possible so the first thing we get is better. + let hasFoundBestPosition = false; + + // For the different writing directions, we should have different priority + // for the moving direction. For example, if the writing direction is + // horizontal, which means the cues will grow from the top to the bottom, + // then moving cues along the `y` axis should be more important than moving + // cues along the `x` axis, and vice versa for those cues growing from the + // left to right, or from the right to the left. We don't follow the exact + // way which the spec requires, see the reason in bug1575460. + function getAxis(writingDirection) { + if (writingDirection == "") { + return ["+y", "-y", "+x", "-x"]; + } + // Growing from left to right. + if (writingDirection == "lr") { + return ["+x", "-x", "+y", "-y"]; + } + // Growing from right to left. + return ["-x", "+x", "+y", "-y"]; + } + const axis = getAxis(cue.vertical); + + // This factor effects the granularity of the moving unit, when using the + // factor=1 often moves too much and results in too many redudant spaces + // between boxes. So we can increase the factor to slightly reduce the + // move we do every time, but still can preverse the reasonable spaces + // between boxes. + const factor = 4; + const toMove = styleBox.getFirstLineBoxSize() / factor; + for (let i = 0; i < axis.length && !hasFoundBestPosition; i++) { + while (!box.isOutsideTheAxisBoundary(containerBox, axis[i]) && + (!box.within(containerBox) || box.overlapsAny(outputBoxes))) { + box.move(axis[i], toMove); + } + // We found a spot where we aren't overlapping anything. This is our + // best position. + if (box.within(containerBox)) { + bestPosition = box.clone(); + hasFoundBestPosition = true; + break; + } + let p = box.intersectPercentage(containerBox); + // If we're outside the container box less then we were on our last try + // then remember this position as the best position. + if (outsideAreaPercentage > p) { + bestPosition = box.clone(); + outsideAreaPercentage = p; + } + // Reset the box position to the specified position. + box = specifiedPosition.clone(); + } + + // Can not find a place to place this box inside the rendering area. + if (!box.within(containerBox)) { + return null; + } + + styleBox.applyStyles({ + top: getPercentagePosition(box.top, containerBox.height), + left: getPercentagePosition(box.left, containerBox.width), + }); + } + + // In order to not be affected by CSS scale, so we use '%' to make sure the + // cue can stick in the right position. + function getPercentagePosition(position, fullDimension) { + return (position / fullDimension) * 100 + "%"; + } + + return box; +} + +export function WebVTT() { + this.isProcessingCues = false; + // Nothing +} + +// Helper to allow strings to be decoded instead of the default binary utf8 data. +WebVTT.StringDecoder = function() { + return { + decode: function(data) { + if (!data) { + return ""; + } + if (typeof data !== "string") { + throw new Error("Error - expected string data."); + } + return decodeURIComponent(encodeURIComponent(data)); + } + }; +}; + +WebVTT.convertCueToDOMTree = function(window, cuetext) { + if (!window) { + return null; + } + return parseContent(window, cuetext, PARSE_CONTENT_MODE.DOCUMENT_FRAGMENT); +}; + +function clearAllCuesDiv(overlay) { + while (overlay.firstChild) { + overlay.firstChild.remove(); + } +} + +// It's used to record how many cues we process in the last `processCues` run. +var lastDisplayedCueNums = 0; + +const DIV_COMPUTING_STATE = { + REUSE : 0, + REUSE_AND_CLEAR : 1, + COMPUTE_AND_CLEAR : 2 +}; + +// Runs the processing model over the cues and regions passed to it. +// Spec https://www.w3.org/TR/webvtt1/#processing-model +// @parem window : JS window +// @param cues : the VTT cues are going to be displayed. +// @param overlay : A block level element (usually a div) that the computed cues +// and regions will be placed into. +// @param controls : A Control bar element. Cues' position will be +// affected and repositioned according to it. +function processCuesInternal(window, cues, overlay, controls) { + LOG(`=== processCues ===`); + if (!cues) { + LOG(`clear display and abort processing because of no cue.`); + clearAllCuesDiv(overlay); + lastDisplayedCueNums = 0; + return; + } + + let controlBar, controlBarShown; + if (controls) { + // controls is a <div> that is the children of the UA Widget Shadow Root. + controlBar = controls.parentNode.getElementById("controlBar"); + controlBarShown = controlBar ? !controlBar.hidden : false; + } else { + // There is no controls element. This only happen to UA Widget because + // it is created lazily. + controlBarShown = false; + } + + /** + * This function is used to tell us if we have to recompute or reuse current + * cue's display state. Display state is a DIV element with corresponding + * CSS style to display cue on the screen. When the cue is being displayed + * first time, we will compute its display state. After that, we could reuse + * its state until following conditions happen. + * (1) control changes : it means the rendering area changes so we should + * recompute cues' position. + * (2) cue's `hasBeenReset` flag is true : it means cues' line or position + * property has been modified, we also need to recompute cues' position. + * (3) the amount of showing cues changes : it means some cue would disappear + * but other cues should stay at the same place without recomputing, so we + * can resume their display state. + */ + function getDIVComputingState(cues) { + if (overlay.lastControlBarShownStatus != controlBarShown) { + return DIV_COMPUTING_STATE.COMPUTE_AND_CLEAR; + } + + for (let i = 0; i < cues.length; i++) { + if (cues[i].hasBeenReset || !cues[i].displayState) { + return DIV_COMPUTING_STATE.COMPUTE_AND_CLEAR; + } + } + + if (lastDisplayedCueNums != cues.length) { + return DIV_COMPUTING_STATE.REUSE_AND_CLEAR; + } + return DIV_COMPUTING_STATE.REUSE; + } + + const divState = getDIVComputingState(cues); + overlay.lastControlBarShownStatus = controlBarShown; + + if (divState == DIV_COMPUTING_STATE.REUSE) { + LOG(`reuse current cue's display state and abort processing`); + return; + } + + clearAllCuesDiv(overlay); + let rootOfCues = window.document.createElement("div"); + rootOfCues.style.position = "absolute"; + rootOfCues.style.left = "0"; + rootOfCues.style.right = "0"; + rootOfCues.style.top = "0"; + rootOfCues.style.bottom = "0"; + overlay.appendChild(rootOfCues); + + if (divState == DIV_COMPUTING_STATE.REUSE_AND_CLEAR) { + LOG(`clear display but reuse cues' display state.`); + for (let cue of cues) { + rootOfCues.appendChild(cue.displayState); + } + } else if (divState == DIV_COMPUTING_STATE.COMPUTE_AND_CLEAR) { + LOG(`clear display and recompute cues' display state.`); + let boxPositions = [], + containerBox = new BoxPosition(rootOfCues); + + let styleBox, cue, controlBarBox; + if (controlBarShown) { + controlBarBox = new BoxPosition(controlBar); + // Add an empty output box that cover the same region as video control bar. + boxPositions.push(controlBarBox); + } + + // https://w3c.github.io/webvtt/#processing-model 6.1.12.1 + // Create regionNode + let regionNodeBoxes = {}; + let regionNodeBox; + + LOG(`lastDisplayedCueNums=${lastDisplayedCueNums}, currentCueNums=${cues.length}`); + lastDisplayedCueNums = cues.length; + for (let i = 0; i < cues.length; i++) { + cue = cues[i]; + if (cue.region != null) { + // 6.1.14.1 + styleBox = new RegionCueStyleBox(window, cue); + + if (!regionNodeBoxes[cue.region.id]) { + // create regionNode + // Adjust the container hieght to exclude the controlBar + let adjustContainerBox = new BoxPosition(rootOfCues); + if (controlBarShown) { + adjustContainerBox.height -= controlBarBox.height; + adjustContainerBox.bottom += controlBarBox.height; + } + regionNodeBox = new RegionNodeBox(window, cue.region, adjustContainerBox); + regionNodeBoxes[cue.region.id] = regionNodeBox; + } + // 6.1.14.3 + let currentRegionBox = regionNodeBoxes[cue.region.id]; + let currentRegionNodeDiv = currentRegionBox.div; + // 6.1.14.3.2 + // TODO: fix me, it looks like the we need to set/change "top" attribute at the styleBox.div + // to do the "scroll up", however, we do not implement it yet? + if (cue.region.scroll == "up" && currentRegionNodeDiv.childElementCount > 0) { + styleBox.div.style.transitionProperty = "top"; + styleBox.div.style.transitionDuration = "0.433s"; + } + + currentRegionNodeDiv.appendChild(styleBox.div); + rootOfCues.appendChild(currentRegionNodeDiv); + cue.displayState = styleBox.div; + boxPositions.push(new BoxPosition(currentRegionBox)); + } else { + // Compute the intial position and styles of the cue div. + styleBox = new CueStyleBox(window, cue, containerBox); + rootOfCues.appendChild(styleBox.div); + + // Move the cue to correct position, we might get the null box if the + // result of algorithm doesn't want us to show the cue when we don't + // have any room for this cue. + let cueBox = adjustBoxPosition(styleBox, containerBox, controlBarBox, boxPositions); + if (cueBox) { + styleBox.setBidiRule(); + // Remember the computed div so that we don't have to recompute it later + // if we don't have too. + cue.displayState = styleBox.div; + boxPositions.push(cueBox); + LOG(`cue ${i}, ` + cueBox.getBoxInfoInChars()); + } else { + LOG(`can not find a proper position to place cue ${i}`); + // Clear the display state and clear the reset flag in the cue as well, + // which controls whether the task for updating the cue display is + // dispatched. + cue.displayState = null; + rootOfCues.removeChild(styleBox.div); + } + } + } + } else { + LOG(`[ERROR] unknown div computing state`); + } +}; + +WebVTT.processCues = function(window, cues, overlay, controls) { + // When accessing `offsetXXX` attributes of element, it would trigger reflow + // and might result in a re-entry of this function. In order to avoid doing + // redundant computation, we would only do one processing at a time. + if (this.isProcessingCues) { + return; + } + this.isProcessingCues = true; + processCuesInternal(window, cues, overlay, controls); + this.isProcessingCues = false; +}; + +WebVTT.Parser = function(window, decoder) { + this.window = window; + this.state = "INITIAL"; + this.substate = ""; + this.substatebuffer = ""; + this.buffer = ""; + this.decoder = decoder || new TextDecoder("utf8"); + this.regionList = []; + this.isPrevLineBlank = false; +}; + +WebVTT.Parser.prototype = { + // If the error is a ParsingError then report it to the consumer if + // possible. If it's not a ParsingError then throw it like normal. + reportOrThrowError: function(e) { + if (e instanceof ParsingError) { + this.onparsingerror && this.onparsingerror(e); + } else { + throw e; + } + }, + parse: function (data) { + // If there is no data then we won't decode it, but will just try to parse + // whatever is in buffer already. This may occur in circumstances, for + // example when flush() is called. + if (data) { + // Try to decode the data that we received. + this.buffer += this.decoder.decode(data, {stream: true}); + } + + // This parser is line-based. Let's see if we have a line to parse. + while (/\r\n|\n|\r/.test(this.buffer)) { + let buffer = this.buffer; + let pos = 0; + while (buffer[pos] !== '\r' && buffer[pos] !== '\n') { + ++pos; + } + let line = buffer.substr(0, pos); + // Advance the buffer early in case we fail below. + if (buffer[pos] === '\r') { + ++pos; + } + if (buffer[pos] === '\n') { + ++pos; + } + this.buffer = buffer.substr(pos); + + // Spec defined replacement. + line = line.replace(/[\u0000]/g, "\uFFFD"); + + // Detect the comment. We parse line on the fly, so we only check if the + // comment block is preceded by a blank line and won't check if it's + // followed by another blank line. + // https://www.w3.org/TR/webvtt1/#introduction-comments + // TODO (1703895): according to the spec, the comment represents as a + // comment block, so we need to refactor the parser in order to better + // handle the comment block. + if (this.isPrevLineBlank && /^NOTE($|[ \t])/.test(line)) { + LOG("Ignore comment that starts with 'NOTE'"); + } else { + this.parseLine(line); + } + this.isPrevLineBlank = emptyOrOnlyContainsWhiteSpaces(line); + } + + return this; + }, + parseLine: function(line) { + let self = this; + + function createCueIfNeeded() { + if (!self.cue) { + self.cue = new self.window.VTTCue(0, 0, ""); + } + } + + // Parsing cue identifier and the identifier should be unique. + // Return true if the input is a cue identifier. + function parseCueIdentifier(input) { + if (maybeIsTimeStampFormat(input)) { + self.state = "CUE"; + return false; + } + + createCueIfNeeded(); + // TODO : ensure the cue identifier is unique among all cue identifiers. + self.cue.id = containsTimeDirectionSymbol(input) ? "" : input; + self.state = "CUE"; + return true; + } + + // Parsing the timestamp and cue settings. + // See spec, https://w3c.github.io/webvtt/#collect-webvtt-cue-timings-and-settings + function parseCueMayThrow(input) { + try { + createCueIfNeeded(); + parseCue(input, self.cue, self.regionList); + self.state = "CUETEXT"; + } catch (e) { + self.reportOrThrowError(e); + // In case of an error ignore rest of the cue. + self.cue = null; + self.state = "BADCUE"; + } + } + + // 3.4 WebVTT region and WebVTT region settings syntax + function parseRegion(input) { + let settings = new Settings(); + parseOptions(input, function (k, v) { + switch (k) { + case "id": + settings.set(k, v); + break; + case "width": + settings.percent(k, v); + break; + case "lines": + settings.digitsValue(k, v); + break; + case "regionanchor": + case "viewportanchor": { + let xy = v.split(','); + if (xy.length !== 2) { + break; + } + // We have to make sure both x and y parse, so use a temporary + // settings object here. + let anchor = new Settings(); + anchor.percent("x", xy[0]); + anchor.percent("y", xy[1]); + if (!anchor.has("x") || !anchor.has("y")) { + break; + } + settings.set(k + "X", anchor.get("x")); + settings.set(k + "Y", anchor.get("y")); + break; + } + case "scroll": + settings.alt(k, v, ["up"]); + break; + } + }, /:/, /\t|\n|\f|\r| /); // groupDelim is ASCII whitespace + // https://infra.spec.whatwg.org/#ascii-whitespace, U+0009 TAB, U+000A LF, U+000C FF, U+000D CR, U+0020 SPACE + + // Create the region, using default values for any values that were not + // specified. + if (settings.has("id")) { + try { + let region = new self.window.VTTRegion(); + region.id = settings.get("id", ""); + region.width = settings.get("width", 100); + region.lines = settings.get("lines", 3); + region.regionAnchorX = settings.get("regionanchorX", 0); + region.regionAnchorY = settings.get("regionanchorY", 100); + region.viewportAnchorX = settings.get("viewportanchorX", 0); + region.viewportAnchorY = settings.get("viewportanchorY", 100); + region.scroll = settings.get("scroll", ""); + // Register the region. + self.onregion && self.onregion(region); + // Remember the VTTRegion for later in case we parse any VTTCues that + // reference it. + self.regionList.push({ + id: settings.get("id"), + region: region + }); + } catch(e) { + dump("VTTRegion Error " + e + "\n"); + let regionPref = Services.prefs.getBoolPref("media.webvtt.regions.enabled"); + dump("regionPref " + regionPref + "\n"); + } + } + } + + // Parsing the WebVTT signature, it contains parsing algo step1 to step9. + // See spec, https://w3c.github.io/webvtt/#file-parsing + function parseSignatureMayThrow(signature) { + if (!/^WEBVTT([ \t].*)?$/.test(signature)) { + throw new ParsingError(ParsingError.Errors.BadSignature); + } else { + self.state = "HEADER"; + } + } + + function parseRegionOrStyle(input) { + switch (self.substate) { + case "REGION": + parseRegion(input); + break; + case "STYLE": + // TODO : not supported yet. + break; + } + } + // Parsing the region and style information. + // See spec, https://w3c.github.io/webvtt/#collect-a-webvtt-block + // + // There are sereval things would appear in header, + // 1. Region or Style setting + // 2. Garbage (meaningless string) + // 3. Empty line + // 4. Cue's timestamp + // The case 4 happens when there is no line interval between the header + // and the cue blocks. In this case, we should preserve the line for the + // next phase parsing, returning "true". + function parseHeader(line) { + if (!self.substate && /^REGION|^STYLE/.test(line)) { + self.substate = /^REGION/.test(line) ? "REGION" : "STYLE"; + return false; + } + + if (self.substate === "REGION" || self.substate === "STYLE") { + if (maybeIsTimeStampFormat(line) || + emptyOrOnlyContainsWhiteSpaces(line) || + containsTimeDirectionSymbol(line)) { + parseRegionOrStyle(self.substatebuffer); + self.substatebuffer = ""; + self.substate = null; + + // This is the end of the region or style state. + return parseHeader(line); + } + + if (/^REGION|^STYLE/.test(line)) { + // The line is another REGION/STYLE, parse and reset substatebuffer. + // Don't break the while loop to parse the next REGION/STYLE. + parseRegionOrStyle(self.substatebuffer); + self.substatebuffer = ""; + self.substate = /^REGION/.test(line) ? "REGION" : "STYLE"; + return false; + } + + // We weren't able to parse the line as a header. Accumulate and + // return. + self.substatebuffer += " " + line; + return false; + } + + if (emptyOrOnlyContainsWhiteSpaces(line)) { + // empty line, whitespaces, nothing to do. + return false; + } + + if (maybeIsTimeStampFormat(line)) { + self.state = "CUE"; + // We want to process the same line again. + return true; + } + + // string contains "-->" or an ID + self.state = "ID"; + return true; + } + + try { + LOG(`state=${self.state}, line=${line}`) + // 5.1 WebVTT file parsing. + if (self.state === "INITIAL") { + parseSignatureMayThrow(line); + return; + } + + if (self.state === "HEADER") { + // parseHeader returns false if the same line doesn't need to be + // parsed again. + if (!parseHeader(line)) { + return; + } + } + + if (self.state === "ID") { + // If there is no cue identifier, read the next line. + if (line == "") { + return; + } + + // If there is no cue identifier, parse the line again. + if (!parseCueIdentifier(line)) { + return self.parseLine(line); + } + return; + } + + if (self.state === "CUE") { + parseCueMayThrow(line); + return; + } + + if (self.state === "CUETEXT") { + // Report the cue when (1) get an empty line (2) get the "-->"" + if (emptyOrOnlyContainsWhiteSpaces(line) || + containsTimeDirectionSymbol(line)) { + // We are done parsing self cue. + self.oncue && self.oncue(self.cue); + self.cue = null; + self.state = "ID"; + + if (emptyOrOnlyContainsWhiteSpaces(line)) { + return; + } + + // Reuse the same line. + return self.parseLine(line); + } + if (self.cue.text) { + self.cue.text += "\n"; + } + self.cue.text += line; + return; + } + + if (self.state === "BADCUE") { + // 54-62 - Collect and discard the remaining cue. + self.state = "ID"; + return self.parseLine(line); + } + } catch (e) { + self.reportOrThrowError(e); + + // If we are currently parsing a cue, report what we have. + if (self.state === "CUETEXT" && self.cue && self.oncue) { + self.oncue(self.cue); + } + self.cue = null; + // Enter BADWEBVTT state if header was not parsed correctly otherwise + // another exception occurred so enter BADCUE state. + self.state = self.state === "INITIAL" ? "BADWEBVTT" : "BADCUE"; + } + return this; + }, + flush: function () { + let self = this; + try { + // Finish decoding the stream. + self.buffer += self.decoder.decode(); + self.buffer += "\n\n"; + self.parse(); + } catch(e) { + self.reportOrThrowError(e); + } + self.isPrevLineBlank = false; + self.onflush && self.onflush(); + return this; + } +}; |