summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/CssParser.java347
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java101
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/Mp4WebvttSubtitle.java56
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java329
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCue.java319
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java550
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java125
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java119
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java115
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/package-info.java20
10 files changed, 2081 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/CssParser.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/CssParser.java
new file mode 100644
index 0000000000..3337cc3481
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/CssParser.java
@@ -0,0 +1,347 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.text.webvtt;
+
+import android.text.TextUtils;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ColorParser;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Provides a CSS parser for STYLE blocks in Webvtt files. Supports only a subset of the CSS
+ * features.
+ */
+/* package */ final class CssParser {
+
+ private static final String PROPERTY_BGCOLOR = "background-color";
+ private static final String PROPERTY_FONT_FAMILY = "font-family";
+ private static final String PROPERTY_FONT_WEIGHT = "font-weight";
+ private static final String PROPERTY_TEXT_DECORATION = "text-decoration";
+ private static final String VALUE_BOLD = "bold";
+ private static final String VALUE_UNDERLINE = "underline";
+ private static final String RULE_START = "{";
+ private static final String RULE_END = "}";
+ private static final String PROPERTY_FONT_STYLE = "font-style";
+ private static final String VALUE_ITALIC = "italic";
+
+ private static final Pattern VOICE_NAME_PATTERN = Pattern.compile("\\[voice=\"([^\"]*)\"\\]");
+
+ // Temporary utility data structures.
+ private final ParsableByteArray styleInput;
+ private final StringBuilder stringBuilder;
+
+ public CssParser() {
+ styleInput = new ParsableByteArray();
+ stringBuilder = new StringBuilder();
+ }
+
+ /**
+ * Takes a CSS style block and consumes up to the first empty line. Attempts to parse the contents
+ * of the style block and returns a list of {@link WebvttCssStyle} instances if successful. If
+ * parsing fails, it returns a list including only the styles which have been successfully parsed
+ * up to the style rule which was malformed.
+ *
+ * @param input The input from which the style block should be read.
+ * @return A list of {@link WebvttCssStyle}s that represents the parsed block, or a list
+ * containing the styles up to the parsing failure.
+ */
+ public List<WebvttCssStyle> parseBlock(ParsableByteArray input) {
+ stringBuilder.setLength(0);
+ int initialInputPosition = input.getPosition();
+ skipStyleBlock(input);
+ styleInput.reset(input.data, input.getPosition());
+ styleInput.setPosition(initialInputPosition);
+
+ List<WebvttCssStyle> styles = new ArrayList<>();
+ String selector;
+ while ((selector = parseSelector(styleInput, stringBuilder)) != null) {
+ if (!RULE_START.equals(parseNextToken(styleInput, stringBuilder))) {
+ return styles;
+ }
+ WebvttCssStyle style = new WebvttCssStyle();
+ applySelectorToStyle(style, selector);
+ String token = null;
+ boolean blockEndFound = false;
+ while (!blockEndFound) {
+ int position = styleInput.getPosition();
+ token = parseNextToken(styleInput, stringBuilder);
+ blockEndFound = token == null || RULE_END.equals(token);
+ if (!blockEndFound) {
+ styleInput.setPosition(position);
+ parseStyleDeclaration(styleInput, style, stringBuilder);
+ }
+ }
+ // Check that the style rule ended correctly.
+ if (RULE_END.equals(token)) {
+ styles.add(style);
+ }
+ }
+ return styles;
+ }
+
+ /**
+ * Returns a string containing the selector. The input is expected to have the form {@code
+ * ::cue(tag#id.class1.class2[voice="someone"]}, where every element is optional.
+ *
+ * @param input From which the selector is obtained.
+ * @return A string containing the target, empty string if the selector is universal (targets all
+ * cues) or null if an error was encountered.
+ */
+ @Nullable
+ private static String parseSelector(ParsableByteArray input, StringBuilder stringBuilder) {
+ skipWhitespaceAndComments(input);
+ if (input.bytesLeft() < 5) {
+ return null;
+ }
+ String cueSelector = input.readString(5);
+ if (!"::cue".equals(cueSelector)) {
+ return null;
+ }
+ int position = input.getPosition();
+ String token = parseNextToken(input, stringBuilder);
+ if (token == null) {
+ return null;
+ }
+ if (RULE_START.equals(token)) {
+ input.setPosition(position);
+ return "";
+ }
+ String target = null;
+ if ("(".equals(token)) {
+ target = readCueTarget(input);
+ }
+ token = parseNextToken(input, stringBuilder);
+ if (!")".equals(token)) {
+ return null;
+ }
+ return target;
+ }
+
+ /**
+ * Reads the contents of ::cue() and returns it as a string.
+ */
+ private static String readCueTarget(ParsableByteArray input) {
+ int position = input.getPosition();
+ int limit = input.limit();
+ boolean cueTargetEndFound = false;
+ while (position < limit && !cueTargetEndFound) {
+ char c = (char) input.data[position++];
+ cueTargetEndFound = c == ')';
+ }
+ return input.readString(--position - input.getPosition()).trim();
+ // --offset to return ')' to the input.
+ }
+
+ private static void parseStyleDeclaration(ParsableByteArray input, WebvttCssStyle style,
+ StringBuilder stringBuilder) {
+ skipWhitespaceAndComments(input);
+ String property = parseIdentifier(input, stringBuilder);
+ if ("".equals(property)) {
+ return;
+ }
+ if (!":".equals(parseNextToken(input, stringBuilder))) {
+ return;
+ }
+ skipWhitespaceAndComments(input);
+ String value = parsePropertyValue(input, stringBuilder);
+ if (value == null || "".equals(value)) {
+ return;
+ }
+ int position = input.getPosition();
+ String token = parseNextToken(input, stringBuilder);
+ if (";".equals(token)) {
+ // The style declaration is well formed.
+ } else if (RULE_END.equals(token)) {
+ // The style declaration is well formed and we can go on, but the closing bracket had to be
+ // fed back.
+ input.setPosition(position);
+ } else {
+ // The style declaration is not well formed.
+ return;
+ }
+ // At this point we have a presumably valid declaration, we need to parse it and fill the style.
+ if ("color".equals(property)) {
+ style.setFontColor(ColorParser.parseCssColor(value));
+ } else if (PROPERTY_BGCOLOR.equals(property)) {
+ style.setBackgroundColor(ColorParser.parseCssColor(value));
+ } else if (PROPERTY_TEXT_DECORATION.equals(property)) {
+ if (VALUE_UNDERLINE.equals(value)) {
+ style.setUnderline(true);
+ }
+ } else if (PROPERTY_FONT_FAMILY.equals(property)) {
+ style.setFontFamily(value);
+ } else if (PROPERTY_FONT_WEIGHT.equals(property)) {
+ if (VALUE_BOLD.equals(value)) {
+ style.setBold(true);
+ }
+ } else if (PROPERTY_FONT_STYLE.equals(property)) {
+ if (VALUE_ITALIC.equals(value)) {
+ style.setItalic(true);
+ }
+ }
+ // TODO: Fill remaining supported styles.
+ }
+
+ // Visible for testing.
+ /* package */ static void skipWhitespaceAndComments(ParsableByteArray input) {
+ boolean skipping = true;
+ while (input.bytesLeft() > 0 && skipping) {
+ skipping = maybeSkipWhitespace(input) || maybeSkipComment(input);
+ }
+ }
+
+ // Visible for testing.
+ @Nullable
+ /* package */ static String parseNextToken(ParsableByteArray input, StringBuilder stringBuilder) {
+ skipWhitespaceAndComments(input);
+ if (input.bytesLeft() == 0) {
+ return null;
+ }
+ String identifier = parseIdentifier(input, stringBuilder);
+ if (!"".equals(identifier)) {
+ return identifier;
+ }
+ // We found a delimiter.
+ return "" + (char) input.readUnsignedByte();
+ }
+
+ private static boolean maybeSkipWhitespace(ParsableByteArray input) {
+ switch(peekCharAtPosition(input, input.getPosition())) {
+ case '\t':
+ case '\r':
+ case '\n':
+ case '\f':
+ case ' ':
+ input.skipBytes(1);
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ // Visible for testing.
+ /* package */ static void skipStyleBlock(ParsableByteArray input) {
+ // The style block cannot contain empty lines, so we assume the input ends when a empty line
+ // is found.
+ String line;
+ do {
+ line = input.readLine();
+ } while (!TextUtils.isEmpty(line));
+ }
+
+ private static char peekCharAtPosition(ParsableByteArray input, int position) {
+ return (char) input.data[position];
+ }
+
+ @Nullable
+ private static String parsePropertyValue(ParsableByteArray input, StringBuilder stringBuilder) {
+ StringBuilder expressionBuilder = new StringBuilder();
+ String token;
+ int position;
+ boolean expressionEndFound = false;
+ // TODO: Add support for "Strings in quotes with spaces".
+ while (!expressionEndFound) {
+ position = input.getPosition();
+ token = parseNextToken(input, stringBuilder);
+ if (token == null) {
+ // Syntax error.
+ return null;
+ }
+ if (RULE_END.equals(token) || ";".equals(token)) {
+ input.setPosition(position);
+ expressionEndFound = true;
+ } else {
+ expressionBuilder.append(token);
+ }
+ }
+ return expressionBuilder.toString();
+ }
+
+ private static boolean maybeSkipComment(ParsableByteArray input) {
+ int position = input.getPosition();
+ int limit = input.limit();
+ byte[] data = input.data;
+ if (position + 2 <= limit && data[position++] == '/' && data[position++] == '*') {
+ while (position + 1 < limit) {
+ char skippedChar = (char) data[position++];
+ if (skippedChar == '*') {
+ if (((char) data[position]) == '/') {
+ position++;
+ limit = position;
+ }
+ }
+ }
+ input.skipBytes(limit - input.getPosition());
+ return true;
+ }
+ return false;
+ }
+
+ private static String parseIdentifier(ParsableByteArray input, StringBuilder stringBuilder) {
+ stringBuilder.setLength(0);
+ int position = input.getPosition();
+ int limit = input.limit();
+ boolean identifierEndFound = false;
+ while (position < limit && !identifierEndFound) {
+ char c = (char) input.data[position];
+ if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '#'
+ || c == '-' || c == '.' || c == '_') {
+ position++;
+ stringBuilder.append(c);
+ } else {
+ identifierEndFound = true;
+ }
+ }
+ input.skipBytes(position - input.getPosition());
+ return stringBuilder.toString();
+ }
+
+ /**
+ * Sets the target of a {@link WebvttCssStyle} by splitting a selector of the form
+ * {@code ::cue(tag#id.class1.class2[voice="someone"]}, where every element is optional.
+ */
+ private void applySelectorToStyle(WebvttCssStyle style, String selector) {
+ if ("".equals(selector)) {
+ return; // Universal selector.
+ }
+ int voiceStartIndex = selector.indexOf('[');
+ if (voiceStartIndex != -1) {
+ Matcher matcher = VOICE_NAME_PATTERN.matcher(selector.substring(voiceStartIndex));
+ if (matcher.matches()) {
+ style.setTargetVoice(matcher.group(1));
+ }
+ selector = selector.substring(0, voiceStartIndex);
+ }
+ String[] classDivision = Util.split(selector, "\\.");
+ String tagAndIdDivision = classDivision[0];
+ int idPrefixIndex = tagAndIdDivision.indexOf('#');
+ if (idPrefixIndex != -1) {
+ style.setTargetTagName(tagAndIdDivision.substring(0, idPrefixIndex));
+ style.setTargetId(tagAndIdDivision.substring(idPrefixIndex + 1)); // We discard the '#'.
+ } else {
+ style.setTargetTagName(tagAndIdDivision);
+ }
+ if (classDivision.length > 1) {
+ style.setTargetClasses(Util.nullSafeArrayCopyOfRange(classDivision, 1, classDivision.length));
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java
new file mode 100644
index 0000000000..3df35c789b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.text.webvtt;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Subtitle;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.SubtitleDecoderException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** A {@link SimpleSubtitleDecoder} for Webvtt embedded in a Mp4 container file. */
+@SuppressWarnings("ConstantField")
+public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder {
+
+ private static final int BOX_HEADER_SIZE = 8;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ private static final int TYPE_payl = 0x7061796c;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ private static final int TYPE_sttg = 0x73747467;
+
+ @SuppressWarnings("ConstantCaseForConstants")
+ private static final int TYPE_vttc = 0x76747463;
+
+ private final ParsableByteArray sampleData;
+ private final WebvttCue.Builder builder;
+
+ public Mp4WebvttDecoder() {
+ super("Mp4WebvttDecoder");
+ sampleData = new ParsableByteArray();
+ builder = new WebvttCue.Builder();
+ }
+
+ @Override
+ protected Subtitle decode(byte[] bytes, int length, boolean reset)
+ throws SubtitleDecoderException {
+ // Webvtt in Mp4 samples have boxes inside of them, so we have to do a traditional box parsing:
+ // first 4 bytes size and then 4 bytes type.
+ sampleData.reset(bytes, length);
+ List<Cue> resultingCueList = new ArrayList<>();
+ while (sampleData.bytesLeft() > 0) {
+ if (sampleData.bytesLeft() < BOX_HEADER_SIZE) {
+ throw new SubtitleDecoderException("Incomplete Mp4Webvtt Top Level box header found.");
+ }
+ int boxSize = sampleData.readInt();
+ int boxType = sampleData.readInt();
+ if (boxType == TYPE_vttc) {
+ resultingCueList.add(parseVttCueBox(sampleData, builder, boxSize - BOX_HEADER_SIZE));
+ } else {
+ // Peers of the VTTCueBox are still not supported and are skipped.
+ sampleData.skipBytes(boxSize - BOX_HEADER_SIZE);
+ }
+ }
+ return new Mp4WebvttSubtitle(resultingCueList);
+ }
+
+ private static Cue parseVttCueBox(ParsableByteArray sampleData, WebvttCue.Builder builder,
+ int remainingCueBoxBytes) throws SubtitleDecoderException {
+ builder.reset();
+ while (remainingCueBoxBytes > 0) {
+ if (remainingCueBoxBytes < BOX_HEADER_SIZE) {
+ throw new SubtitleDecoderException("Incomplete vtt cue box header found.");
+ }
+ int boxSize = sampleData.readInt();
+ int boxType = sampleData.readInt();
+ remainingCueBoxBytes -= BOX_HEADER_SIZE;
+ int payloadLength = boxSize - BOX_HEADER_SIZE;
+ String boxPayload =
+ Util.fromUtf8Bytes(sampleData.data, sampleData.getPosition(), payloadLength);
+ sampleData.skipBytes(payloadLength);
+ remainingCueBoxBytes -= payloadLength;
+ if (boxType == TYPE_sttg) {
+ WebvttCueParser.parseCueSettingsList(boxPayload, builder);
+ } else if (boxType == TYPE_payl) {
+ WebvttCueParser.parseCueText(null, boxPayload.trim(), builder, Collections.emptyList());
+ } else {
+ // Other VTTCueBox children are still not supported and are ignored.
+ }
+ }
+ return builder.build();
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/Mp4WebvttSubtitle.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/Mp4WebvttSubtitle.java
new file mode 100644
index 0000000000..545e8b2511
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/Mp4WebvttSubtitle.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.text.webvtt;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Subtitle;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Representation of a Webvtt subtitle embedded in a MP4 container file.
+ */
+/* package */ final class Mp4WebvttSubtitle implements Subtitle {
+
+ private final List<Cue> cues;
+
+ public Mp4WebvttSubtitle(List<Cue> cueList) {
+ cues = Collections.unmodifiableList(cueList);
+ }
+
+ @Override
+ public int getNextEventTimeIndex(long timeUs) {
+ return timeUs < 0 ? 0 : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getEventTimeCount() {
+ return 1;
+ }
+
+ @Override
+ public long getEventTime(int index) {
+ Assertions.checkArgument(index == 0);
+ return 0;
+ }
+
+ @Override
+ public List<Cue> getCues(long timeUs) {
+ return timeUs >= 0 ? cues : Collections.emptyList();
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java
new file mode 100644
index 0000000000..da37cfbdf3
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.text.webvtt;
+
+import android.graphics.Typeface;
+import android.text.Layout;
+import android.text.TextUtils;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
+
+/**
+ * Style object of a Css style block in a Webvtt file.
+ *
+ * @see <a href="https://w3c.github.io/webvtt/#applying-css-properties">W3C specification - Apply
+ * CSS properties</a>
+ */
+public final class WebvttCssStyle {
+
+ public static final int UNSPECIFIED = -1;
+
+ /**
+ * Style flag enum. Possible flag values are {@link #UNSPECIFIED}, {@link #STYLE_NORMAL}, {@link
+ * #STYLE_BOLD}, {@link #STYLE_ITALIC} and {@link #STYLE_BOLD_ITALIC}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {UNSPECIFIED, STYLE_NORMAL, STYLE_BOLD, STYLE_ITALIC, STYLE_BOLD_ITALIC})
+ public @interface StyleFlags {}
+
+ public static final int STYLE_NORMAL = Typeface.NORMAL;
+ public static final int STYLE_BOLD = Typeface.BOLD;
+ public static final int STYLE_ITALIC = Typeface.ITALIC;
+ public static final int STYLE_BOLD_ITALIC = Typeface.BOLD_ITALIC;
+
+ /**
+ * Font size unit enum. One of {@link #UNSPECIFIED}, {@link #FONT_SIZE_UNIT_PIXEL}, {@link
+ * #FONT_SIZE_UNIT_EM} or {@link #FONT_SIZE_UNIT_PERCENT}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({UNSPECIFIED, FONT_SIZE_UNIT_PIXEL, FONT_SIZE_UNIT_EM, FONT_SIZE_UNIT_PERCENT})
+ public @interface FontSizeUnit {}
+
+ public static final int FONT_SIZE_UNIT_PIXEL = 1;
+ public static final int FONT_SIZE_UNIT_EM = 2;
+ public static final int FONT_SIZE_UNIT_PERCENT = 3;
+
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({UNSPECIFIED, OFF, ON})
+ private @interface OptionalBoolean {}
+
+ private static final int OFF = 0;
+ private static final int ON = 1;
+
+ // Selector properties.
+ private String targetId;
+ private String targetTag;
+ private List<String> targetClasses;
+ private String targetVoice;
+
+ // Style properties.
+ @Nullable private String fontFamily;
+ private int fontColor;
+ private boolean hasFontColor;
+ private int backgroundColor;
+ private boolean hasBackgroundColor;
+ @OptionalBoolean private int linethrough;
+ @OptionalBoolean private int underline;
+ @OptionalBoolean private int bold;
+ @OptionalBoolean private int italic;
+ @FontSizeUnit private int fontSizeUnit;
+ private float fontSize;
+ @Nullable private Layout.Alignment textAlign;
+
+ // Calling reset() is forbidden because `this` isn't initialized. This can be safely suppressed
+ // because reset() only assigns fields, it doesn't read any.
+ @SuppressWarnings("nullness:method.invocation.invalid")
+ public WebvttCssStyle() {
+ reset();
+ }
+
+ @EnsuresNonNull({"targetId", "targetTag", "targetClasses", "targetVoice"})
+ public void reset() {
+ targetId = "";
+ targetTag = "";
+ targetClasses = Collections.emptyList();
+ targetVoice = "";
+ fontFamily = null;
+ hasFontColor = false;
+ hasBackgroundColor = false;
+ linethrough = UNSPECIFIED;
+ underline = UNSPECIFIED;
+ bold = UNSPECIFIED;
+ italic = UNSPECIFIED;
+ fontSizeUnit = UNSPECIFIED;
+ textAlign = null;
+ }
+
+ public void setTargetId(String targetId) {
+ this.targetId = targetId;
+ }
+
+ public void setTargetTagName(String targetTag) {
+ this.targetTag = targetTag;
+ }
+
+ public void setTargetClasses(String[] targetClasses) {
+ this.targetClasses = Arrays.asList(targetClasses);
+ }
+
+ public void setTargetVoice(String targetVoice) {
+ this.targetVoice = targetVoice;
+ }
+
+ /**
+ * Returns a value in a score system compliant with the CSS Specificity rules.
+ *
+ * @see <a href="https://www.w3.org/TR/CSS2/cascade.html">CSS Cascading</a>
+ * <p>The score works as follows:
+ * <ul>
+ * <li>Id match adds 0x40000000 to the score.
+ * <li>Each class and voice match adds 4 to the score.
+ * <li>Tag matching adds 2 to the score.
+ * <li>Universal selector matching scores 1.
+ * </ul>
+ *
+ * @param id The id of the cue if present, {@code null} otherwise.
+ * @param tag Name of the tag, {@code null} if it refers to the entire cue.
+ * @param classes An array containing the classes the tag belongs to. Must not be null.
+ * @param voice Annotated voice if present, {@code null} otherwise.
+ * @return The score of the match, zero if there is no match.
+ */
+ public int getSpecificityScore(
+ @Nullable String id, @Nullable String tag, String[] classes, @Nullable String voice) {
+ if (targetId.isEmpty() && targetTag.isEmpty() && targetClasses.isEmpty()
+ && targetVoice.isEmpty()) {
+ // The selector is universal. It matches with the minimum score if and only if the given
+ // element is a whole cue.
+ return TextUtils.isEmpty(tag) ? 1 : 0;
+ }
+ int score = 0;
+ score = updateScoreForMatch(score, targetId, id, 0x40000000);
+ score = updateScoreForMatch(score, targetTag, tag, 2);
+ score = updateScoreForMatch(score, targetVoice, voice, 4);
+ if (score == -1 || !Arrays.asList(classes).containsAll(targetClasses)) {
+ return 0;
+ } else {
+ score += targetClasses.size() * 4;
+ }
+ return score;
+ }
+
+ /**
+ * Returns the style or {@link #UNSPECIFIED} when no style information is given.
+ *
+ * @return {@link #UNSPECIFIED}, {@link #STYLE_NORMAL}, {@link #STYLE_BOLD}, {@link #STYLE_BOLD}
+ * or {@link #STYLE_BOLD_ITALIC}.
+ */
+ @StyleFlags public int getStyle() {
+ if (bold == UNSPECIFIED && italic == UNSPECIFIED) {
+ return UNSPECIFIED;
+ }
+ return (bold == ON ? STYLE_BOLD : STYLE_NORMAL)
+ | (italic == ON ? STYLE_ITALIC : STYLE_NORMAL);
+ }
+
+ public boolean isLinethrough() {
+ return linethrough == ON;
+ }
+
+ public WebvttCssStyle setLinethrough(boolean linethrough) {
+ this.linethrough = linethrough ? ON : OFF;
+ return this;
+ }
+
+ public boolean isUnderline() {
+ return underline == ON;
+ }
+
+ public WebvttCssStyle setUnderline(boolean underline) {
+ this.underline = underline ? ON : OFF;
+ return this;
+ }
+ public WebvttCssStyle setBold(boolean bold) {
+ this.bold = bold ? ON : OFF;
+ return this;
+ }
+
+ public WebvttCssStyle setItalic(boolean italic) {
+ this.italic = italic ? ON : OFF;
+ return this;
+ }
+
+ @Nullable
+ public String getFontFamily() {
+ return fontFamily;
+ }
+
+ public WebvttCssStyle setFontFamily(@Nullable String fontFamily) {
+ this.fontFamily = Util.toLowerInvariant(fontFamily);
+ return this;
+ }
+
+ public int getFontColor() {
+ if (!hasFontColor) {
+ throw new IllegalStateException("Font color not defined");
+ }
+ return fontColor;
+ }
+
+ public WebvttCssStyle setFontColor(int color) {
+ this.fontColor = color;
+ hasFontColor = true;
+ return this;
+ }
+
+ public boolean hasFontColor() {
+ return hasFontColor;
+ }
+
+ public int getBackgroundColor() {
+ if (!hasBackgroundColor) {
+ throw new IllegalStateException("Background color not defined.");
+ }
+ return backgroundColor;
+ }
+
+ public WebvttCssStyle setBackgroundColor(int backgroundColor) {
+ this.backgroundColor = backgroundColor;
+ hasBackgroundColor = true;
+ return this;
+ }
+
+ public boolean hasBackgroundColor() {
+ return hasBackgroundColor;
+ }
+
+ @Nullable
+ public Layout.Alignment getTextAlign() {
+ return textAlign;
+ }
+
+ public WebvttCssStyle setTextAlign(@Nullable Layout.Alignment textAlign) {
+ this.textAlign = textAlign;
+ return this;
+ }
+
+ public WebvttCssStyle setFontSize(float fontSize) {
+ this.fontSize = fontSize;
+ return this;
+ }
+
+ public WebvttCssStyle setFontSizeUnit(short unit) {
+ this.fontSizeUnit = unit;
+ return this;
+ }
+
+ @FontSizeUnit public int getFontSizeUnit() {
+ return fontSizeUnit;
+ }
+
+ public float getFontSize() {
+ return fontSize;
+ }
+
+ public void cascadeFrom(WebvttCssStyle style) {
+ if (style.hasFontColor) {
+ setFontColor(style.fontColor);
+ }
+ if (style.bold != UNSPECIFIED) {
+ bold = style.bold;
+ }
+ if (style.italic != UNSPECIFIED) {
+ italic = style.italic;
+ }
+ if (style.fontFamily != null) {
+ fontFamily = style.fontFamily;
+ }
+ if (linethrough == UNSPECIFIED) {
+ linethrough = style.linethrough;
+ }
+ if (underline == UNSPECIFIED) {
+ underline = style.underline;
+ }
+ if (textAlign == null) {
+ textAlign = style.textAlign;
+ }
+ if (fontSizeUnit == UNSPECIFIED) {
+ fontSizeUnit = style.fontSizeUnit;
+ fontSize = style.fontSize;
+ }
+ if (style.hasBackgroundColor) {
+ setBackgroundColor(style.backgroundColor);
+ }
+ }
+
+ private static int updateScoreForMatch(
+ int currentScore, String target, @Nullable String actual, int score) {
+ if (target.isEmpty() || currentScore == -1) {
+ return currentScore;
+ }
+ return target.equals(actual) ? currentScore + score : -1;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCue.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCue.java
new file mode 100644
index 0000000000..af701d8f54
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCue.java
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.text.webvtt;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.text.Layout.Alignment;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+
+/** A representation of a WebVTT cue. */
+public final class WebvttCue extends Cue {
+
+ private static final float DEFAULT_POSITION = 0.5f;
+
+ public final long startTime;
+ public final long endTime;
+
+ private WebvttCue(
+ long startTime,
+ long endTime,
+ CharSequence text,
+ @Nullable Alignment textAlignment,
+ float line,
+ @Cue.LineType int lineType,
+ @Cue.AnchorType int lineAnchor,
+ float position,
+ @Cue.AnchorType int positionAnchor,
+ float width) {
+ super(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, width);
+ this.startTime = startTime;
+ this.endTime = endTime;
+ }
+
+ /**
+ * Returns whether or not this cue should be placed in the default position and rolled-up with
+ * the other "normal" cues.
+ *
+ * @return Whether this cue should be placed in the default position.
+ */
+ public boolean isNormalCue() {
+ return (line == DIMEN_UNSET && position == DEFAULT_POSITION);
+ }
+
+ /** Builder for WebVTT cues. */
+ @SuppressWarnings("hiding")
+ public static class Builder {
+
+ /**
+ * Valid values for {@link #setTextAlignment(int)}.
+ *
+ * <p>We use a custom list (and not {@link Alignment} directly) in order to include both {@code
+ * START}/{@code LEFT} and {@code END}/{@code RIGHT}. The distinction is important for {@link
+ * #derivePosition(int)}.
+ *
+ * <p>These correspond to the valid values for the 'align' cue setting in the <a
+ * href="https://www.w3.org/TR/webvtt1/#webvtt-cue-text-alignment">WebVTT spec</a>.
+ */
+ @Documented
+ @Retention(SOURCE)
+ @IntDef({
+ TEXT_ALIGNMENT_START,
+ TEXT_ALIGNMENT_CENTER,
+ TEXT_ALIGNMENT_END,
+ TEXT_ALIGNMENT_LEFT,
+ TEXT_ALIGNMENT_RIGHT
+ })
+ public @interface TextAlignment {}
+ /**
+ * See WebVTT's <a
+ * href="https://www.w3.org/TR/webvtt1/#webvtt-cue-start-alignment">align:start</a>.
+ */
+ public static final int TEXT_ALIGNMENT_START = 1;
+
+ /**
+ * See WebVTT's <a
+ * href="https://www.w3.org/TR/webvtt1/#webvtt-cue-center-alignment">align:center</a>.
+ */
+ public static final int TEXT_ALIGNMENT_CENTER = 2;
+
+ /**
+ * See WebVTT's <a href="https://www.w3.org/TR/webvtt1/#webvtt-cue-end-alignment">align:end</a>.
+ */
+ public static final int TEXT_ALIGNMENT_END = 3;
+
+ /**
+ * See WebVTT's <a href="https://www.w3.org/TR/webvtt1/#webvtt-cue-left-alignment">align:left</a>.
+ */
+ public static final int TEXT_ALIGNMENT_LEFT = 4;
+
+ /**
+ * See WebVTT's <a
+ * href="https://www.w3.org/TR/webvtt1/#webvtt-cue-right-alignment">align:right</a>.
+ */
+ public static final int TEXT_ALIGNMENT_RIGHT = 5;
+
+ private static final String TAG = "WebvttCueBuilder";
+
+ private long startTime;
+ private long endTime;
+ @Nullable private CharSequence text;
+ @TextAlignment private int textAlignment;
+ private float line;
+ // Equivalent to WebVTT's snap-to-lines flag:
+ // https://www.w3.org/TR/webvtt1/#webvtt-cue-snap-to-lines-flag
+ @LineType private int lineType;
+ @AnchorType private int lineAnchor;
+ private float position;
+ @AnchorType private int positionAnchor;
+ private float width;
+
+ // Initialization methods
+
+ // Calling reset() is forbidden because `this` isn't initialized. This can be safely
+ // suppressed because reset() only assigns fields, it doesn't read any.
+ @SuppressWarnings("nullness:method.invocation.invalid")
+ public Builder() {
+ reset();
+ }
+
+ public void reset() {
+ startTime = 0;
+ endTime = 0;
+ text = null;
+ // Default: https://www.w3.org/TR/webvtt1/#webvtt-cue-text-alignment
+ textAlignment = TEXT_ALIGNMENT_CENTER;
+ line = Cue.DIMEN_UNSET;
+ // Defaults to NUMBER (true): https://www.w3.org/TR/webvtt1/#webvtt-cue-snap-to-lines-flag
+ lineType = Cue.LINE_TYPE_NUMBER;
+ // Default: https://www.w3.org/TR/webvtt1/#webvtt-cue-line-alignment
+ lineAnchor = Cue.ANCHOR_TYPE_START;
+ position = Cue.DIMEN_UNSET;
+ positionAnchor = Cue.TYPE_UNSET;
+ // Default: https://www.w3.org/TR/webvtt1/#webvtt-cue-size
+ width = 1.0f;
+ }
+
+ // Construction methods.
+
+ public WebvttCue build() {
+ line = computeLine(line, lineType);
+
+ if (position == Cue.DIMEN_UNSET) {
+ position = derivePosition(textAlignment);
+ }
+
+ if (positionAnchor == Cue.TYPE_UNSET) {
+ positionAnchor = derivePositionAnchor(textAlignment);
+ }
+
+ width = Math.min(width, deriveMaxSize(positionAnchor, position));
+
+ return new WebvttCue(
+ startTime,
+ endTime,
+ Assertions.checkNotNull(text),
+ convertTextAlignment(textAlignment),
+ line,
+ lineType,
+ lineAnchor,
+ position,
+ positionAnchor,
+ width);
+ }
+
+ public Builder setStartTime(long time) {
+ startTime = time;
+ return this;
+ }
+
+ public Builder setEndTime(long time) {
+ endTime = time;
+ return this;
+ }
+
+ public Builder setText(CharSequence text) {
+ this.text = text;
+ return this;
+ }
+
+ public Builder setTextAlignment(@TextAlignment int textAlignment) {
+ this.textAlignment = textAlignment;
+ return this;
+ }
+
+ public Builder setLine(float line) {
+ this.line = line;
+ return this;
+ }
+
+ public Builder setLineType(@LineType int lineType) {
+ this.lineType = lineType;
+ return this;
+ }
+
+ public Builder setLineAnchor(@AnchorType int lineAnchor) {
+ this.lineAnchor = lineAnchor;
+ return this;
+ }
+
+ public Builder setPosition(float position) {
+ this.position = position;
+ return this;
+ }
+
+ public Builder setPositionAnchor(@AnchorType int positionAnchor) {
+ this.positionAnchor = positionAnchor;
+ return this;
+ }
+
+ public Builder setWidth(float width) {
+ this.width = width;
+ return this;
+ }
+
+ // https://www.w3.org/TR/webvtt1/#webvtt-cue-line
+ private static float computeLine(float line, @LineType int lineType) {
+ if (line != Cue.DIMEN_UNSET
+ && lineType == Cue.LINE_TYPE_FRACTION
+ && (line < 0.0f || line > 1.0f)) {
+ return 1.0f; // Step 1
+ } else if (line != Cue.DIMEN_UNSET) {
+ // Step 2: Do nothing, line is already correct.
+ return line;
+ } else if (lineType == Cue.LINE_TYPE_FRACTION) {
+ return 1.0f; // Step 3
+ } else {
+ // Steps 4 - 10 (stacking multiple simultaneous cues) are handled by WebvttSubtitle#getCues
+ // and WebvttCue#isNormalCue.
+ return DIMEN_UNSET;
+ }
+ }
+
+ // https://www.w3.org/TR/webvtt1/#webvtt-cue-position
+ private static float derivePosition(@TextAlignment int textAlignment) {
+ switch (textAlignment) {
+ case TEXT_ALIGNMENT_LEFT:
+ return 0.0f;
+ case TEXT_ALIGNMENT_RIGHT:
+ return 1.0f;
+ case TEXT_ALIGNMENT_START:
+ case TEXT_ALIGNMENT_CENTER:
+ case TEXT_ALIGNMENT_END:
+ default:
+ return DEFAULT_POSITION;
+ }
+ }
+
+ // https://www.w3.org/TR/webvtt1/#webvtt-cue-position-alignment
+ @AnchorType
+ private static int derivePositionAnchor(@TextAlignment int textAlignment) {
+ switch (textAlignment) {
+ case TEXT_ALIGNMENT_LEFT:
+ case TEXT_ALIGNMENT_START:
+ return Cue.ANCHOR_TYPE_START;
+ case TEXT_ALIGNMENT_RIGHT:
+ case TEXT_ALIGNMENT_END:
+ return Cue.ANCHOR_TYPE_END;
+ case TEXT_ALIGNMENT_CENTER:
+ default:
+ return Cue.ANCHOR_TYPE_MIDDLE;
+ }
+ }
+
+ @Nullable
+ private static Alignment convertTextAlignment(@TextAlignment int textAlignment) {
+ switch (textAlignment) {
+ case TEXT_ALIGNMENT_START:
+ case TEXT_ALIGNMENT_LEFT:
+ return Alignment.ALIGN_NORMAL;
+ case TEXT_ALIGNMENT_CENTER:
+ return Alignment.ALIGN_CENTER;
+ case TEXT_ALIGNMENT_END:
+ case TEXT_ALIGNMENT_RIGHT:
+ return Alignment.ALIGN_OPPOSITE;
+ default:
+ Log.w(TAG, "Unknown textAlignment: " + textAlignment);
+ return null;
+ }
+ }
+
+ // Step 2 here: https://www.w3.org/TR/webvtt1/#processing-cue-settings
+ private static float deriveMaxSize(@AnchorType int positionAnchor, float position) {
+ switch (positionAnchor) {
+ case Cue.ANCHOR_TYPE_START:
+ return 1.0f - position;
+ case Cue.ANCHOR_TYPE_END:
+ return position;
+ case Cue.ANCHOR_TYPE_MIDDLE:
+ if (position <= 0.5f) {
+ return position * 2;
+ } else {
+ return (1.0f - position) * 2;
+ }
+ case Cue.TYPE_UNSET:
+ default:
+ throw new IllegalStateException(String.valueOf(positionAnchor));
+ }
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java
new file mode 100644
index 0000000000..b370e67792
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java
@@ -0,0 +1,550 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.text.webvtt;
+
+import android.graphics.Typeface;
+import android.text.Layout;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.style.AbsoluteSizeSpan;
+import android.text.style.AlignmentSpan;
+import android.text.style.BackgroundColorSpan;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.RelativeSizeSpan;
+import android.text.style.StrikethroughSpan;
+import android.text.style.StyleSpan;
+import android.text.style.TypefaceSpan;
+import android.text.style.UnderlineSpan;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** Parser for WebVTT cues. (https://w3c.github.io/webvtt/#cues) */
+public final class WebvttCueParser {
+
+ public static final Pattern CUE_HEADER_PATTERN = Pattern
+ .compile("^(\\S+)\\s+-->\\s+(\\S+)(.*)?$");
+
+ private static final Pattern CUE_SETTING_PATTERN = Pattern.compile("(\\S+?):(\\S+)");
+
+ private static final char CHAR_LESS_THAN = '<';
+ private static final char CHAR_GREATER_THAN = '>';
+ private static final char CHAR_SLASH = '/';
+ private static final char CHAR_AMPERSAND = '&';
+ private static final char CHAR_SEMI_COLON = ';';
+ private static final char CHAR_SPACE = ' ';
+
+ private static final String ENTITY_LESS_THAN = "lt";
+ private static final String ENTITY_GREATER_THAN = "gt";
+ private static final String ENTITY_AMPERSAND = "amp";
+ private static final String ENTITY_NON_BREAK_SPACE = "nbsp";
+
+ private static final String TAG_BOLD = "b";
+ private static final String TAG_ITALIC = "i";
+ private static final String TAG_UNDERLINE = "u";
+ private static final String TAG_CLASS = "c";
+ private static final String TAG_VOICE = "v";
+ private static final String TAG_LANG = "lang";
+
+ private static final int STYLE_BOLD = Typeface.BOLD;
+ private static final int STYLE_ITALIC = Typeface.ITALIC;
+
+ private static final String TAG = "WebvttCueParser";
+
+ private final StringBuilder textBuilder;
+
+ public WebvttCueParser() {
+ textBuilder = new StringBuilder();
+ }
+
+ /**
+ * Parses the next valid WebVTT cue in a parsable array, including timestamps, settings and text.
+ *
+ * @param webvttData Parsable WebVTT file data.
+ * @param builder Builder for WebVTT Cues (output parameter).
+ * @param styles List of styles defined by the CSS style blocks preceding the cues.
+ * @return Whether a valid Cue was found.
+ */
+ public boolean parseCue(
+ ParsableByteArray webvttData, WebvttCue.Builder builder, List<WebvttCssStyle> styles) {
+ @Nullable String firstLine = webvttData.readLine();
+ if (firstLine == null) {
+ return false;
+ }
+ Matcher cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(firstLine);
+ if (cueHeaderMatcher.matches()) {
+ // We have found the timestamps in the first line. No id present.
+ return parseCue(null, cueHeaderMatcher, webvttData, builder, textBuilder, styles);
+ }
+ // The first line is not the timestamps, but could be the cue id.
+ @Nullable String secondLine = webvttData.readLine();
+ if (secondLine == null) {
+ return false;
+ }
+ cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(secondLine);
+ if (cueHeaderMatcher.matches()) {
+ // We can do the rest of the parsing, including the id.
+ return parseCue(firstLine.trim(), cueHeaderMatcher, webvttData, builder, textBuilder,
+ styles);
+ }
+ return false;
+ }
+
+ /**
+ * Parses a string containing a list of cue settings.
+ *
+ * @param cueSettingsList String containing the settings for a given cue.
+ * @param builder The {@link WebvttCue.Builder} where incremental construction takes place.
+ */
+ /* package */ static void parseCueSettingsList(String cueSettingsList,
+ WebvttCue.Builder builder) {
+ // Parse the cue settings list.
+ Matcher cueSettingMatcher = CUE_SETTING_PATTERN.matcher(cueSettingsList);
+ while (cueSettingMatcher.find()) {
+ String name = cueSettingMatcher.group(1);
+ String value = cueSettingMatcher.group(2);
+ try {
+ if ("line".equals(name)) {
+ parseLineAttribute(value, builder);
+ } else if ("align".equals(name)) {
+ builder.setTextAlignment(parseTextAlignment(value));
+ } else if ("position".equals(name)) {
+ parsePositionAttribute(value, builder);
+ } else if ("size".equals(name)) {
+ builder.setWidth(WebvttParserUtil.parsePercentage(value));
+ } else {
+ Log.w(TAG, "Unknown cue setting " + name + ":" + value);
+ }
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Skipping bad cue setting: " + cueSettingMatcher.group());
+ }
+ }
+ }
+
+ /**
+ * Parses the text payload of a WebVTT Cue and applies modifications on {@link WebvttCue.Builder}.
+ *
+ * @param id Id of the cue, {@code null} if it is not present.
+ * @param markup The markup text to be parsed.
+ * @param styles List of styles defined by the CSS style blocks preceding the cues.
+ * @param builder Output builder.
+ */
+ /* package */ static void parseCueText(
+ @Nullable String id, String markup, WebvttCue.Builder builder, List<WebvttCssStyle> styles) {
+ SpannableStringBuilder spannedText = new SpannableStringBuilder();
+ ArrayDeque<StartTag> startTagStack = new ArrayDeque<>();
+ List<StyleMatch> scratchStyleMatches = new ArrayList<>();
+ int pos = 0;
+ while (pos < markup.length()) {
+ char curr = markup.charAt(pos);
+ switch (curr) {
+ case CHAR_LESS_THAN:
+ if (pos + 1 >= markup.length()) {
+ pos++;
+ break; // avoid ArrayOutOfBoundsException
+ }
+ int ltPos = pos;
+ boolean isClosingTag = markup.charAt(ltPos + 1) == CHAR_SLASH;
+ pos = findEndOfTag(markup, ltPos + 1);
+ boolean isVoidTag = markup.charAt(pos - 2) == CHAR_SLASH;
+ String fullTagExpression = markup.substring(ltPos + (isClosingTag ? 2 : 1),
+ isVoidTag ? pos - 2 : pos - 1);
+ if (fullTagExpression.trim().isEmpty()) {
+ continue;
+ }
+ String tagName = getTagName(fullTagExpression);
+ if (!isSupportedTag(tagName)) {
+ continue;
+ }
+ if (isClosingTag) {
+ StartTag startTag;
+ do {
+ if (startTagStack.isEmpty()) {
+ break;
+ }
+ startTag = startTagStack.pop();
+ applySpansForTag(id, startTag, spannedText, styles, scratchStyleMatches);
+ } while(!startTag.name.equals(tagName));
+ } else if (!isVoidTag) {
+ startTagStack.push(StartTag.buildStartTag(fullTagExpression, spannedText.length()));
+ }
+ break;
+ case CHAR_AMPERSAND:
+ int semiColonEndIndex = markup.indexOf(CHAR_SEMI_COLON, pos + 1);
+ int spaceEndIndex = markup.indexOf(CHAR_SPACE, pos + 1);
+ int entityEndIndex = semiColonEndIndex == -1 ? spaceEndIndex
+ : (spaceEndIndex == -1 ? semiColonEndIndex
+ : Math.min(semiColonEndIndex, spaceEndIndex));
+ if (entityEndIndex != -1) {
+ applyEntity(markup.substring(pos + 1, entityEndIndex), spannedText);
+ if (entityEndIndex == spaceEndIndex) {
+ spannedText.append(" ");
+ }
+ pos = entityEndIndex + 1;
+ } else {
+ spannedText.append(curr);
+ pos++;
+ }
+ break;
+ default:
+ spannedText.append(curr);
+ pos++;
+ break;
+ }
+ }
+ // apply unclosed tags
+ while (!startTagStack.isEmpty()) {
+ applySpansForTag(id, startTagStack.pop(), spannedText, styles, scratchStyleMatches);
+ }
+ applySpansForTag(id, StartTag.buildWholeCueVirtualTag(), spannedText, styles,
+ scratchStyleMatches);
+ builder.setText(spannedText);
+ }
+
+ private static boolean parseCue(
+ @Nullable String id,
+ Matcher cueHeaderMatcher,
+ ParsableByteArray webvttData,
+ WebvttCue.Builder builder,
+ StringBuilder textBuilder,
+ List<WebvttCssStyle> styles) {
+ try {
+ // Parse the cue start and end times.
+ builder.setStartTime(WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1)))
+ .setEndTime(WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(2)));
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Skipping cue with bad header: " + cueHeaderMatcher.group());
+ return false;
+ }
+
+ parseCueSettingsList(cueHeaderMatcher.group(3), builder);
+
+ // Parse the cue text.
+ textBuilder.setLength(0);
+ for (String line = webvttData.readLine();
+ !TextUtils.isEmpty(line);
+ line = webvttData.readLine()) {
+ if (textBuilder.length() > 0) {
+ textBuilder.append("\n");
+ }
+ textBuilder.append(line.trim());
+ }
+ parseCueText(id, textBuilder.toString(), builder, styles);
+ return true;
+ }
+
+ // Internal methods
+
+ private static void parseLineAttribute(String s, WebvttCue.Builder builder) {
+ int commaIndex = s.indexOf(',');
+ if (commaIndex != -1) {
+ builder.setLineAnchor(parsePositionAnchor(s.substring(commaIndex + 1)));
+ s = s.substring(0, commaIndex);
+ }
+ if (s.endsWith("%")) {
+ builder.setLine(WebvttParserUtil.parsePercentage(s)).setLineType(Cue.LINE_TYPE_FRACTION);
+ } else {
+ int lineNumber = Integer.parseInt(s);
+ if (lineNumber < 0) {
+ // WebVTT defines line -1 as last visible row when lineAnchor is ANCHOR_TYPE_START, where-as
+ // Cue defines it to be the first row that's not visible.
+ lineNumber--;
+ }
+ builder.setLine(lineNumber).setLineType(Cue.LINE_TYPE_NUMBER);
+ }
+ }
+
+ private static void parsePositionAttribute(String s, WebvttCue.Builder builder) {
+ int commaIndex = s.indexOf(',');
+ if (commaIndex != -1) {
+ builder.setPositionAnchor(parsePositionAnchor(s.substring(commaIndex + 1)));
+ s = s.substring(0, commaIndex);
+ }
+ builder.setPosition(WebvttParserUtil.parsePercentage(s));
+ }
+
+ @Cue.AnchorType
+ private static int parsePositionAnchor(String s) {
+ switch (s) {
+ case "start":
+ return Cue.ANCHOR_TYPE_START;
+ case "center":
+ case "middle":
+ return Cue.ANCHOR_TYPE_MIDDLE;
+ case "end":
+ return Cue.ANCHOR_TYPE_END;
+ default:
+ Log.w(TAG, "Invalid anchor value: " + s);
+ return Cue.TYPE_UNSET;
+ }
+ }
+
+ @WebvttCue.Builder.TextAlignment
+ private static int parseTextAlignment(String s) {
+ switch (s) {
+ case "start":
+ return WebvttCue.Builder.TEXT_ALIGNMENT_START;
+ case "left":
+ return WebvttCue.Builder.TEXT_ALIGNMENT_LEFT;
+ case "center":
+ case "middle":
+ return WebvttCue.Builder.TEXT_ALIGNMENT_CENTER;
+ case "end":
+ return WebvttCue.Builder.TEXT_ALIGNMENT_END;
+ case "right":
+ return WebvttCue.Builder.TEXT_ALIGNMENT_RIGHT;
+ default:
+ Log.w(TAG, "Invalid alignment value: " + s);
+ // Default value: https://www.w3.org/TR/webvtt1/#webvtt-cue-text-alignment
+ return WebvttCue.Builder.TEXT_ALIGNMENT_CENTER;
+ }
+ }
+
+ /**
+ * Find end of tag (&gt;). The position returned is the position of the &gt; plus one (exclusive).
+ *
+ * @param markup The WebVTT cue markup to be parsed.
+ * @param startPos The position from where to start searching for the end of tag.
+ * @return The position of the end of tag plus 1 (one).
+ */
+ private static int findEndOfTag(String markup, int startPos) {
+ int index = markup.indexOf(CHAR_GREATER_THAN, startPos);
+ return index == -1 ? markup.length() : index + 1;
+ }
+
+ private static void applyEntity(String entity, SpannableStringBuilder spannedText) {
+ switch (entity) {
+ case ENTITY_LESS_THAN:
+ spannedText.append('<');
+ break;
+ case ENTITY_GREATER_THAN:
+ spannedText.append('>');
+ break;
+ case ENTITY_NON_BREAK_SPACE:
+ spannedText.append(' ');
+ break;
+ case ENTITY_AMPERSAND:
+ spannedText.append('&');
+ break;
+ default:
+ Log.w(TAG, "ignoring unsupported entity: '&" + entity + ";'");
+ break;
+ }
+ }
+
+ private static boolean isSupportedTag(String tagName) {
+ switch (tagName) {
+ case TAG_BOLD:
+ case TAG_CLASS:
+ case TAG_ITALIC:
+ case TAG_LANG:
+ case TAG_UNDERLINE:
+ case TAG_VOICE:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private static void applySpansForTag(
+ @Nullable String cueId,
+ StartTag startTag,
+ SpannableStringBuilder text,
+ List<WebvttCssStyle> styles,
+ List<StyleMatch> scratchStyleMatches) {
+ int start = startTag.position;
+ int end = text.length();
+ switch(startTag.name) {
+ case TAG_BOLD:
+ text.setSpan(new StyleSpan(STYLE_BOLD), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case TAG_ITALIC:
+ text.setSpan(new StyleSpan(STYLE_ITALIC), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case TAG_UNDERLINE:
+ text.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case TAG_CLASS:
+ case TAG_LANG:
+ case TAG_VOICE:
+ case "": // Case of the "whole cue" virtual tag.
+ break;
+ default:
+ return;
+ }
+ scratchStyleMatches.clear();
+ getApplicableStyles(styles, cueId, startTag, scratchStyleMatches);
+ int styleMatchesCount = scratchStyleMatches.size();
+ for (int i = 0; i < styleMatchesCount; i++) {
+ applyStyleToText(text, scratchStyleMatches.get(i).style, start, end);
+ }
+ }
+
+ private static void applyStyleToText(SpannableStringBuilder spannedText, WebvttCssStyle style,
+ int start, int end) {
+ if (style == null) {
+ return;
+ }
+ if (style.getStyle() != WebvttCssStyle.UNSPECIFIED) {
+ spannedText.setSpan(new StyleSpan(style.getStyle()), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.isLinethrough()) {
+ spannedText.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.isUnderline()) {
+ spannedText.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.hasFontColor()) {
+ spannedText.setSpan(new ForegroundColorSpan(style.getFontColor()), start, end,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.hasBackgroundColor()) {
+ spannedText.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.getFontFamily() != null) {
+ spannedText.setSpan(new TypefaceSpan(style.getFontFamily()), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ Layout.Alignment textAlign = style.getTextAlign();
+ if (textAlign != null) {
+ spannedText.setSpan(
+ new AlignmentSpan.Standard(textAlign), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ switch (style.getFontSizeUnit()) {
+ case WebvttCssStyle.FONT_SIZE_UNIT_PIXEL:
+ spannedText.setSpan(new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case WebvttCssStyle.FONT_SIZE_UNIT_EM:
+ spannedText.setSpan(new RelativeSizeSpan(style.getFontSize()), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case WebvttCssStyle.FONT_SIZE_UNIT_PERCENT:
+ spannedText.setSpan(new RelativeSizeSpan(style.getFontSize() / 100), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case WebvttCssStyle.UNSPECIFIED:
+ // Do nothing.
+ break;
+ }
+ }
+
+ /**
+ * Returns the tag name for the given tag contents.
+ *
+ * @param tagExpression Characters between &amp;lt: and &amp;gt; of a start or end tag.
+ * @return The name of tag.
+ */
+ private static String getTagName(String tagExpression) {
+ tagExpression = tagExpression.trim();
+ Assertions.checkArgument(!tagExpression.isEmpty());
+ return Util.splitAtFirst(tagExpression, "[ \\.]")[0];
+ }
+
+ private static void getApplicableStyles(
+ List<WebvttCssStyle> declaredStyles,
+ @Nullable String id,
+ StartTag tag,
+ List<StyleMatch> output) {
+ int styleCount = declaredStyles.size();
+ for (int i = 0; i < styleCount; i++) {
+ WebvttCssStyle style = declaredStyles.get(i);
+ int score = style.getSpecificityScore(id, tag.name, tag.classes, tag.voice);
+ if (score > 0) {
+ output.add(new StyleMatch(score, style));
+ }
+ }
+ Collections.sort(output);
+ }
+
+ private static final class StyleMatch implements Comparable<StyleMatch> {
+
+ public final int score;
+ public final WebvttCssStyle style;
+
+ public StyleMatch(int score, WebvttCssStyle style) {
+ this.score = score;
+ this.style = style;
+ }
+
+ @Override
+ public int compareTo(@NonNull StyleMatch another) {
+ return this.score - another.score;
+ }
+
+ }
+
+ private static final class StartTag {
+
+ private static final String[] NO_CLASSES = new String[0];
+
+ public final String name;
+ public final int position;
+ public final String voice;
+ public final String[] classes;
+
+ private StartTag(String name, int position, String voice, String[] classes) {
+ this.position = position;
+ this.name = name;
+ this.voice = voice;
+ this.classes = classes;
+ }
+
+ public static StartTag buildStartTag(String fullTagExpression, int position) {
+ fullTagExpression = fullTagExpression.trim();
+ Assertions.checkArgument(!fullTagExpression.isEmpty());
+ int voiceStartIndex = fullTagExpression.indexOf(" ");
+ String voice;
+ if (voiceStartIndex == -1) {
+ voice = "";
+ } else {
+ voice = fullTagExpression.substring(voiceStartIndex).trim();
+ fullTagExpression = fullTagExpression.substring(0, voiceStartIndex);
+ }
+ String[] nameAndClasses = Util.split(fullTagExpression, "\\.");
+ String name = nameAndClasses[0];
+ String[] classes;
+ if (nameAndClasses.length > 1) {
+ classes = Util.nullSafeArrayCopyOfRange(nameAndClasses, 1, nameAndClasses.length);
+ } else {
+ classes = NO_CLASSES;
+ }
+ return new StartTag(name, position, voice, classes);
+ }
+
+ public static StartTag buildWholeCueVirtualTag() {
+ return new StartTag("", 0, "", new String[0]);
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java
new file mode 100644
index 0000000000..a70a49e82e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.text.webvtt;
+
+import android.text.TextUtils;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Subtitle;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.SubtitleDecoderException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A {@link SimpleSubtitleDecoder} for WebVTT.
+ * <p>
+ * @see <a href="http://dev.w3.org/html5/webvtt">WebVTT specification</a>
+ */
+public final class WebvttDecoder extends SimpleSubtitleDecoder {
+
+ private static final int EVENT_NONE = -1;
+ private static final int EVENT_END_OF_FILE = 0;
+ private static final int EVENT_COMMENT = 1;
+ private static final int EVENT_STYLE_BLOCK = 2;
+ private static final int EVENT_CUE = 3;
+
+ private static final String COMMENT_START = "NOTE";
+ private static final String STYLE_START = "STYLE";
+
+ private final WebvttCueParser cueParser;
+ private final ParsableByteArray parsableWebvttData;
+ private final WebvttCue.Builder webvttCueBuilder;
+ private final CssParser cssParser;
+ private final List<WebvttCssStyle> definedStyles;
+
+ public WebvttDecoder() {
+ super("WebvttDecoder");
+ cueParser = new WebvttCueParser();
+ parsableWebvttData = new ParsableByteArray();
+ webvttCueBuilder = new WebvttCue.Builder();
+ cssParser = new CssParser();
+ definedStyles = new ArrayList<>();
+ }
+
+ @Override
+ protected Subtitle decode(byte[] bytes, int length, boolean reset)
+ throws SubtitleDecoderException {
+ parsableWebvttData.reset(bytes, length);
+ // Initialization for consistent starting state.
+ webvttCueBuilder.reset();
+ definedStyles.clear();
+
+ // Validate the first line of the header, and skip the remainder.
+ try {
+ WebvttParserUtil.validateWebvttHeaderLine(parsableWebvttData);
+ } catch (ParserException e) {
+ throw new SubtitleDecoderException(e);
+ }
+ while (!TextUtils.isEmpty(parsableWebvttData.readLine())) {}
+
+ int event;
+ ArrayList<WebvttCue> subtitles = new ArrayList<>();
+ while ((event = getNextEvent(parsableWebvttData)) != EVENT_END_OF_FILE) {
+ if (event == EVENT_COMMENT) {
+ skipComment(parsableWebvttData);
+ } else if (event == EVENT_STYLE_BLOCK) {
+ if (!subtitles.isEmpty()) {
+ throw new SubtitleDecoderException("A style block was found after the first cue.");
+ }
+ parsableWebvttData.readLine(); // Consume the "STYLE" header.
+ definedStyles.addAll(cssParser.parseBlock(parsableWebvttData));
+ } else if (event == EVENT_CUE) {
+ if (cueParser.parseCue(parsableWebvttData, webvttCueBuilder, definedStyles)) {
+ subtitles.add(webvttCueBuilder.build());
+ webvttCueBuilder.reset();
+ }
+ }
+ }
+ return new WebvttSubtitle(subtitles);
+ }
+
+ /**
+ * Positions the input right before the next event, and returns the kind of event found. Does not
+ * consume any data from such event, if any.
+ *
+ * @return The kind of event found.
+ */
+ private static int getNextEvent(ParsableByteArray parsableWebvttData) {
+ int foundEvent = EVENT_NONE;
+ int currentInputPosition = 0;
+ while (foundEvent == EVENT_NONE) {
+ currentInputPosition = parsableWebvttData.getPosition();
+ String line = parsableWebvttData.readLine();
+ if (line == null) {
+ foundEvent = EVENT_END_OF_FILE;
+ } else if (STYLE_START.equals(line)) {
+ foundEvent = EVENT_STYLE_BLOCK;
+ } else if (line.startsWith(COMMENT_START)) {
+ foundEvent = EVENT_COMMENT;
+ } else {
+ foundEvent = EVENT_CUE;
+ }
+ }
+ parsableWebvttData.setPosition(currentInputPosition);
+ return foundEvent;
+ }
+
+ private static void skipComment(ParsableByteArray parsableWebvttData) {
+ while (!TextUtils.isEmpty(parsableWebvttData.readLine())) {}
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java
new file mode 100644
index 0000000000..b87d014de0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.text.webvtt;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utility methods for parsing WebVTT data.
+ */
+public final class WebvttParserUtil {
+
+ private static final Pattern COMMENT = Pattern.compile("^NOTE([ \t].*)?$");
+ private static final String WEBVTT_HEADER = "WEBVTT";
+
+ private WebvttParserUtil() {}
+
+ /**
+ * Reads and validates the first line of a WebVTT file.
+ *
+ * @param input The input from which the line should be read.
+ * @throws ParserException If the line isn't the start of a valid WebVTT file.
+ */
+ public static void validateWebvttHeaderLine(ParsableByteArray input) throws ParserException {
+ int startPosition = input.getPosition();
+ if (!isWebvttHeaderLine(input)) {
+ input.setPosition(startPosition);
+ throw new ParserException("Expected WEBVTT. Got " + input.readLine());
+ }
+ }
+
+ /**
+ * Returns whether the given input is the first line of a WebVTT file.
+ *
+ * @param input The input from which the line should be read.
+ */
+ public static boolean isWebvttHeaderLine(ParsableByteArray input) {
+ @Nullable String line = input.readLine();
+ return line != null && line.startsWith(WEBVTT_HEADER);
+ }
+
+ /**
+ * Parses a WebVTT timestamp.
+ *
+ * @param timestamp The timestamp string.
+ * @return The parsed timestamp in microseconds.
+ * @throws NumberFormatException If the timestamp could not be parsed.
+ */
+ public static long parseTimestampUs(String timestamp) throws NumberFormatException {
+ long value = 0;
+ String[] parts = Util.splitAtFirst(timestamp, "\\.");
+ String[] subparts = Util.split(parts[0], ":");
+ for (String subpart : subparts) {
+ value = (value * 60) + Long.parseLong(subpart);
+ }
+ value *= 1000;
+ if (parts.length == 2) {
+ value += Long.parseLong(parts[1]);
+ }
+ return value * 1000;
+ }
+
+ /**
+ * Parses a percentage string.
+ *
+ * @param s The percentage string.
+ * @return The parsed value, where 1.0 represents 100%.
+ * @throws NumberFormatException If the percentage could not be parsed.
+ */
+ public static float parsePercentage(String s) throws NumberFormatException {
+ if (!s.endsWith("%")) {
+ throw new NumberFormatException("Percentages must end with %");
+ }
+ return Float.parseFloat(s.substring(0, s.length() - 1)) / 100;
+ }
+
+ /**
+ * Reads lines up to and including the next WebVTT cue header.
+ *
+ * @param input The input from which lines should be read.
+ * @return A {@link Matcher} for the WebVTT cue header, or null if the end of the input was
+ * reached without a cue header being found. In the case that a cue header is found, groups 1,
+ * 2 and 3 of the returned matcher contain the start time, end time and settings list.
+ */
+ @Nullable
+ public static Matcher findNextCueHeader(ParsableByteArray input) {
+ @Nullable String line;
+ while ((line = input.readLine()) != null) {
+ if (COMMENT.matcher(line).matches()) {
+ // Skip until the end of the comment block.
+ while ((line = input.readLine()) != null && !line.isEmpty()) {}
+ } else {
+ Matcher cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(line);
+ if (cueHeaderMatcher.matches()) {
+ return cueHeaderMatcher;
+ }
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java
new file mode 100644
index 0000000000..558c699eba
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.text.webvtt;
+
+import android.text.SpannableStringBuilder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Subtitle;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A representation of a WebVTT subtitle.
+ */
+/* package */ final class WebvttSubtitle implements Subtitle {
+
+ private final List<WebvttCue> cues;
+ private final int numCues;
+ private final long[] cueTimesUs;
+ private final long[] sortedCueTimesUs;
+
+ /**
+ * @param cues A list of the cues in this subtitle.
+ */
+ public WebvttSubtitle(List<WebvttCue> cues) {
+ this.cues = cues;
+ numCues = cues.size();
+ cueTimesUs = new long[2 * numCues];
+ for (int cueIndex = 0; cueIndex < numCues; cueIndex++) {
+ WebvttCue cue = cues.get(cueIndex);
+ int arrayIndex = cueIndex * 2;
+ cueTimesUs[arrayIndex] = cue.startTime;
+ cueTimesUs[arrayIndex + 1] = cue.endTime;
+ }
+ sortedCueTimesUs = Arrays.copyOf(cueTimesUs, cueTimesUs.length);
+ Arrays.sort(sortedCueTimesUs);
+ }
+
+ @Override
+ public int getNextEventTimeIndex(long timeUs) {
+ int index = Util.binarySearchCeil(sortedCueTimesUs, timeUs, false, false);
+ return index < sortedCueTimesUs.length ? index : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getEventTimeCount() {
+ return sortedCueTimesUs.length;
+ }
+
+ @Override
+ public long getEventTime(int index) {
+ Assertions.checkArgument(index >= 0);
+ Assertions.checkArgument(index < sortedCueTimesUs.length);
+ return sortedCueTimesUs[index];
+ }
+
+ @Override
+ public List<Cue> getCues(long timeUs) {
+ List<Cue> list = new ArrayList<>();
+ WebvttCue firstNormalCue = null;
+ SpannableStringBuilder normalCueTextBuilder = null;
+
+ for (int i = 0; i < numCues; i++) {
+ if ((cueTimesUs[i * 2] <= timeUs) && (timeUs < cueTimesUs[i * 2 + 1])) {
+ WebvttCue cue = cues.get(i);
+ // TODO(ibaker): Replace this with a closer implementation of the WebVTT spec (keeping
+ // individual cues, but tweaking their `line` value):
+ // https://www.w3.org/TR/webvtt1/#cue-computed-line
+ if (cue.isNormalCue()) {
+ // we want to merge all of the normal cues into a single cue to ensure they are drawn
+ // correctly (i.e. don't overlap) and to emulate roll-up, but only if there are multiple
+ // normal cues, otherwise we can just append the single normal cue
+ if (firstNormalCue == null) {
+ firstNormalCue = cue;
+ } else if (normalCueTextBuilder == null) {
+ normalCueTextBuilder = new SpannableStringBuilder();
+ normalCueTextBuilder
+ .append(Assertions.checkNotNull(firstNormalCue.text))
+ .append("\n")
+ .append(Assertions.checkNotNull(cue.text));
+ } else {
+ normalCueTextBuilder.append("\n").append(Assertions.checkNotNull(cue.text));
+ }
+ } else {
+ list.add(cue);
+ }
+ }
+ }
+ if (normalCueTextBuilder != null) {
+ // there were multiple normal cues, so create a new cue with all of the text
+ list.add(new WebvttCue.Builder().setText(normalCueTextBuilder).build());
+ } else if (firstNormalCue != null) {
+ // there was only a single normal cue, so just add it to the list
+ list.add(firstNormalCue);
+ }
+ return list;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/package-info.java
new file mode 100644
index 0000000000..e2c014d539
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/webvtt/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * 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.
+ *
+ */
+@NonNullApi
+package org.mozilla.thirdparty.com.google.android.exoplayer2.text.webvtt;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi;