summaryrefslogtreecommitdiffstats
path: root/dom/media/webvtt
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /dom/media/webvtt
parentInitial commit. (diff)
downloadfirefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz
firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/media/webvtt')
-rw-r--r--dom/media/webvtt/TextTrack.cpp385
-rw-r--r--dom/media/webvtt/TextTrack.h148
-rw-r--r--dom/media/webvtt/TextTrackCue.cpp260
-rw-r--r--dom/media/webvtt/TextTrackCue.h342
-rw-r--r--dom/media/webvtt/TextTrackCueList.cpp125
-rw-r--r--dom/media/webvtt/TextTrackCueList.h73
-rw-r--r--dom/media/webvtt/TextTrackList.cpp192
-rw-r--r--dom/media/webvtt/TextTrackList.h79
-rw-r--r--dom/media/webvtt/TextTrackRegion.cpp58
-rw-r--r--dom/media/webvtt/TextTrackRegion.h138
-rw-r--r--dom/media/webvtt/WebVTTListener.cpp212
-rw-r--r--dom/media/webvtt/WebVTTListener.h69
-rw-r--r--dom/media/webvtt/WebVTTParserWrapper.sys.mjs56
-rw-r--r--dom/media/webvtt/components.conf14
-rw-r--r--dom/media/webvtt/moz.build52
-rw-r--r--dom/media/webvtt/nsIWebVTTListener.idl37
-rw-r--r--dom/media/webvtt/nsIWebVTTParserWrapper.idl94
-rw-r--r--dom/media/webvtt/package.json6
-rw-r--r--dom/media/webvtt/test/crashtests/1304948.html33
-rw-r--r--dom/media/webvtt/test/crashtests/1319486.html27
-rw-r--r--dom/media/webvtt/test/crashtests/1533909.html17
-rw-r--r--dom/media/webvtt/test/crashtests/882549.html13
-rw-r--r--dom/media/webvtt/test/crashtests/894104.html20
-rw-r--r--dom/media/webvtt/test/crashtests/crashtests.list5
-rw-r--r--dom/media/webvtt/test/mochitest/bad-signature.vtt1
-rw-r--r--dom/media/webvtt/test/mochitest/basic.vtt29
-rw-r--r--dom/media/webvtt/test/mochitest/bug883173.vtt16
-rw-r--r--dom/media/webvtt/test/mochitest/long.vtt8001
-rw-r--r--dom/media/webvtt/test/mochitest/manifest.js27
-rw-r--r--dom/media/webvtt/test/mochitest/mochitest.ini50
-rw-r--r--dom/media/webvtt/test/mochitest/parser.vtt6
-rw-r--r--dom/media/webvtt/test/mochitest/region.vtt6
-rw-r--r--dom/media/webvtt/test/mochitest/sequential.vtt10
-rw-r--r--dom/media/webvtt/test/mochitest/test_bug1018933.html50
-rw-r--r--dom/media/webvtt/test/mochitest/test_bug1242594.html46
-rw-r--r--dom/media/webvtt/test/mochitest/test_bug883173.html39
-rw-r--r--dom/media/webvtt/test/mochitest/test_bug895091.html60
-rw-r--r--dom/media/webvtt/test/mochitest/test_bug957847.html30
-rw-r--r--dom/media/webvtt/test/mochitest/test_testtrack_cors_no_response.html41
-rw-r--r--dom/media/webvtt/test/mochitest/test_texttrack.html158
-rw-r--r--dom/media/webvtt/test/mochitest/test_texttrack_cors_preload_none.html40
-rw-r--r--dom/media/webvtt/test/mochitest/test_texttrack_mode_change_during_loading.html75
-rw-r--r--dom/media/webvtt/test/mochitest/test_texttrack_moz.html60
-rw-r--r--dom/media/webvtt/test/mochitest/test_texttrackcue.html298
-rw-r--r--dom/media/webvtt/test/mochitest/test_texttrackcue_moz.html34
-rw-r--r--dom/media/webvtt/test/mochitest/test_texttrackevents_video.html91
-rw-r--r--dom/media/webvtt/test/mochitest/test_texttracklist.html51
-rw-r--r--dom/media/webvtt/test/mochitest/test_texttracklist_moz.html34
-rw-r--r--dom/media/webvtt/test/mochitest/test_texttrackregion.html57
-rw-r--r--dom/media/webvtt/test/mochitest/test_trackelementevent.html77
-rw-r--r--dom/media/webvtt/test/mochitest/test_trackelementsrc.html53
-rw-r--r--dom/media/webvtt/test/mochitest/test_trackevent.html69
-rw-r--r--dom/media/webvtt/test/mochitest/test_vttparser.html44
-rw-r--r--dom/media/webvtt/test/mochitest/test_webvtt_empty_displaystate.html98
-rw-r--r--dom/media/webvtt/test/mochitest/test_webvtt_event_same_time.html63
-rw-r--r--dom/media/webvtt/test/mochitest/test_webvtt_infinite_processing_loop.html49
-rw-r--r--dom/media/webvtt/test/mochitest/test_webvtt_overlapping_time.html100
-rw-r--r--dom/media/webvtt/test/mochitest/test_webvtt_positionalign.html113
-rw-r--r--dom/media/webvtt/test/mochitest/test_webvtt_seeking.html110
-rw-r--r--dom/media/webvtt/test/mochitest/test_webvtt_update_display_after_adding_or_removing_cue.html93
-rw-r--r--dom/media/webvtt/test/mochitest/vttPositionAlign.vtt86
-rw-r--r--dom/media/webvtt/test/reftest/black.mp4bin0 -> 15036 bytes
-rw-r--r--dom/media/webvtt/test/reftest/cues_time_overlapping.webvtt7
-rw-r--r--dom/media/webvtt/test/reftest/reftest.list3
-rw-r--r--dom/media/webvtt/test/reftest/vtt_overlapping_time-ref.html29
-rw-r--r--dom/media/webvtt/test/reftest/vtt_overlapping_time.html30
-rw-r--r--dom/media/webvtt/test/reftest/vtt_reflow_display-ref.html28
-rw-r--r--dom/media/webvtt/test/reftest/vtt_reflow_display.css33
-rw-r--r--dom/media/webvtt/test/reftest/vtt_reflow_display.html37
-rw-r--r--dom/media/webvtt/test/reftest/vtt_update_display_after_removed_cue.html36
-rw-r--r--dom/media/webvtt/test/reftest/vtt_update_display_after_removed_cue_ref.html6
-rw-r--r--dom/media/webvtt/test/reftest/white.webmbin0 -> 10880 bytes
-rw-r--r--dom/media/webvtt/test/xpcshell/test_parser.js158
-rw-r--r--dom/media/webvtt/test/xpcshell/xpcshell.ini3
-rw-r--r--dom/media/webvtt/update-webvtt.js61
-rw-r--r--dom/media/webvtt/vtt.sys.mjs1663
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
new file mode 100644
index 0000000000..24eb3be139
--- /dev/null
+++ b/dom/media/webvtt/test/reftest/black.mp4
Binary files differ
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
new file mode 100644
index 0000000000..bbacad7ffd
--- /dev/null
+++ b/dom/media/webvtt/test/reftest/white.webm
Binary files differ
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 = {
+ "&amp;": "&",
+ "&lt;": "<",
+ "&gt;": ">",
+ "&lrm;": "\u200e",
+ "&rlm;": "\u200f",
+ "&nbsp;": "\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;
+ }
+};