summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java756
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlNode.java399
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlRegion.java69
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java151
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlStyle.java268
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java81
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/package-info.java19
7 files changed, 1743 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java
new file mode 100644
index 0000000000..502281c2de
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java
@@ -0,0 +1,756 @@
+/*
+ * 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.ttml;
+
+import android.text.Layout;
+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.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.ColorParser;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.XmlPullParserUtil;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.ArrayDeque;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+/**
+ * A {@link SimpleSubtitleDecoder} for TTML supporting the DFXP presentation profile. Features
+ * supported by this decoder are:
+ *
+ * <ul>
+ * <li>content
+ * <li>core
+ * <li>presentation
+ * <li>profile
+ * <li>structure
+ * <li>time-offset
+ * <li>timing
+ * <li>tickRate
+ * <li>time-clock-with-frames
+ * <li>time-clock
+ * <li>time-offset-with-frames
+ * <li>time-offset-with-ticks
+ * <li>cell-resolution
+ * </ul>
+ *
+ * @see <a href="http://www.w3.org/TR/ttaf1-dfxp/">TTML specification</a>
+ */
+public final class TtmlDecoder extends SimpleSubtitleDecoder {
+
+ private static final String TAG = "TtmlDecoder";
+
+ private static final String TTP = "http://www.w3.org/ns/ttml#parameter";
+
+ private static final String ATTR_BEGIN = "begin";
+ private static final String ATTR_DURATION = "dur";
+ private static final String ATTR_END = "end";
+ private static final String ATTR_STYLE = "style";
+ private static final String ATTR_REGION = "region";
+ private static final String ATTR_IMAGE = "backgroundImage";
+
+ private static final Pattern CLOCK_TIME =
+ Pattern.compile("^([0-9][0-9]+):([0-9][0-9]):([0-9][0-9])"
+ + "(?:(\\.[0-9]+)|:([0-9][0-9])(?:\\.([0-9]+))?)?$");
+ private static final Pattern OFFSET_TIME =
+ Pattern.compile("^([0-9]+(?:\\.[0-9]+)?)(h|m|s|ms|f|t)$");
+ private static final Pattern FONT_SIZE = Pattern.compile("^(([0-9]*.)?[0-9]+)(px|em|%)$");
+ private static final Pattern PERCENTAGE_COORDINATES =
+ Pattern.compile("^(\\d+\\.?\\d*?)% (\\d+\\.?\\d*?)%$");
+ private static final Pattern PIXEL_COORDINATES =
+ Pattern.compile("^(\\d+\\.?\\d*?)px (\\d+\\.?\\d*?)px$");
+ private static final Pattern CELL_RESOLUTION = Pattern.compile("^(\\d+) (\\d+)$");
+
+ private static final int DEFAULT_FRAME_RATE = 30;
+
+ private static final FrameAndTickRate DEFAULT_FRAME_AND_TICK_RATE =
+ new FrameAndTickRate(DEFAULT_FRAME_RATE, 1, 1);
+ private static final CellResolution DEFAULT_CELL_RESOLUTION =
+ new CellResolution(/* columns= */ 32, /* rows= */ 15);
+
+ private final XmlPullParserFactory xmlParserFactory;
+
+ public TtmlDecoder() {
+ super("TtmlDecoder");
+ try {
+ xmlParserFactory = XmlPullParserFactory.newInstance();
+ xmlParserFactory.setNamespaceAware(true);
+ } catch (XmlPullParserException e) {
+ throw new RuntimeException("Couldn't create XmlPullParserFactory instance", e);
+ }
+ }
+
+ @Override
+ protected Subtitle decode(byte[] bytes, int length, boolean reset)
+ throws SubtitleDecoderException {
+ try {
+ XmlPullParser xmlParser = xmlParserFactory.newPullParser();
+ Map<String, TtmlStyle> globalStyles = new HashMap<>();
+ Map<String, TtmlRegion> regionMap = new HashMap<>();
+ Map<String, String> imageMap = new HashMap<>();
+ regionMap.put(TtmlNode.ANONYMOUS_REGION_ID, new TtmlRegion(null));
+ ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes, 0, length);
+ xmlParser.setInput(inputStream, null);
+ TtmlSubtitle ttmlSubtitle = null;
+ ArrayDeque<TtmlNode> nodeStack = new ArrayDeque<>();
+ int unsupportedNodeDepth = 0;
+ int eventType = xmlParser.getEventType();
+ FrameAndTickRate frameAndTickRate = DEFAULT_FRAME_AND_TICK_RATE;
+ CellResolution cellResolution = DEFAULT_CELL_RESOLUTION;
+ TtsExtent ttsExtent = null;
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ TtmlNode parent = nodeStack.peek();
+ if (unsupportedNodeDepth == 0) {
+ String name = xmlParser.getName();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (TtmlNode.TAG_TT.equals(name)) {
+ frameAndTickRate = parseFrameAndTickRates(xmlParser);
+ cellResolution = parseCellResolution(xmlParser, DEFAULT_CELL_RESOLUTION);
+ ttsExtent = parseTtsExtent(xmlParser);
+ }
+ if (!isSupportedTag(name)) {
+ Log.i(TAG, "Ignoring unsupported tag: " + xmlParser.getName());
+ unsupportedNodeDepth++;
+ } else if (TtmlNode.TAG_HEAD.equals(name)) {
+ parseHeader(xmlParser, globalStyles, cellResolution, ttsExtent, regionMap, imageMap);
+ } else {
+ try {
+ TtmlNode node = parseNode(xmlParser, parent, regionMap, frameAndTickRate);
+ nodeStack.push(node);
+ if (parent != null) {
+ parent.addChild(node);
+ }
+ } catch (SubtitleDecoderException e) {
+ Log.w(TAG, "Suppressing parser error", e);
+ // Treat the node (and by extension, all of its children) as unsupported.
+ unsupportedNodeDepth++;
+ }
+ }
+ } else if (eventType == XmlPullParser.TEXT) {
+ parent.addChild(TtmlNode.buildTextNode(xmlParser.getText()));
+ } else if (eventType == XmlPullParser.END_TAG) {
+ if (xmlParser.getName().equals(TtmlNode.TAG_TT)) {
+ ttmlSubtitle = new TtmlSubtitle(nodeStack.peek(), globalStyles, regionMap, imageMap);
+ }
+ nodeStack.pop();
+ }
+ } else {
+ if (eventType == XmlPullParser.START_TAG) {
+ unsupportedNodeDepth++;
+ } else if (eventType == XmlPullParser.END_TAG) {
+ unsupportedNodeDepth--;
+ }
+ }
+ xmlParser.next();
+ eventType = xmlParser.getEventType();
+ }
+ return ttmlSubtitle;
+ } catch (XmlPullParserException xppe) {
+ throw new SubtitleDecoderException("Unable to decode source", xppe);
+ } catch (IOException e) {
+ throw new IllegalStateException("Unexpected error when reading input.", e);
+ }
+ }
+
+ private FrameAndTickRate parseFrameAndTickRates(XmlPullParser xmlParser)
+ throws SubtitleDecoderException {
+ int frameRate = DEFAULT_FRAME_RATE;
+ String frameRateString = xmlParser.getAttributeValue(TTP, "frameRate");
+ if (frameRateString != null) {
+ frameRate = Integer.parseInt(frameRateString);
+ }
+
+ float frameRateMultiplier = 1;
+ String frameRateMultiplierString = xmlParser.getAttributeValue(TTP, "frameRateMultiplier");
+ if (frameRateMultiplierString != null) {
+ String[] parts = Util.split(frameRateMultiplierString, " ");
+ if (parts.length != 2) {
+ throw new SubtitleDecoderException("frameRateMultiplier doesn't have 2 parts");
+ }
+ float numerator = Integer.parseInt(parts[0]);
+ float denominator = Integer.parseInt(parts[1]);
+ frameRateMultiplier = numerator / denominator;
+ }
+
+ int subFrameRate = DEFAULT_FRAME_AND_TICK_RATE.subFrameRate;
+ String subFrameRateString = xmlParser.getAttributeValue(TTP, "subFrameRate");
+ if (subFrameRateString != null) {
+ subFrameRate = Integer.parseInt(subFrameRateString);
+ }
+
+ int tickRate = DEFAULT_FRAME_AND_TICK_RATE.tickRate;
+ String tickRateString = xmlParser.getAttributeValue(TTP, "tickRate");
+ if (tickRateString != null) {
+ tickRate = Integer.parseInt(tickRateString);
+ }
+ return new FrameAndTickRate(frameRate * frameRateMultiplier, subFrameRate, tickRate);
+ }
+
+ private CellResolution parseCellResolution(XmlPullParser xmlParser, CellResolution defaultValue)
+ throws SubtitleDecoderException {
+ String cellResolution = xmlParser.getAttributeValue(TTP, "cellResolution");
+ if (cellResolution == null) {
+ return defaultValue;
+ }
+
+ Matcher cellResolutionMatcher = CELL_RESOLUTION.matcher(cellResolution);
+ if (!cellResolutionMatcher.matches()) {
+ Log.w(TAG, "Ignoring malformed cell resolution: " + cellResolution);
+ return defaultValue;
+ }
+ try {
+ int columns = Integer.parseInt(cellResolutionMatcher.group(1));
+ int rows = Integer.parseInt(cellResolutionMatcher.group(2));
+ if (columns == 0 || rows == 0) {
+ throw new SubtitleDecoderException("Invalid cell resolution " + columns + " " + rows);
+ }
+ return new CellResolution(columns, rows);
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Ignoring malformed cell resolution: " + cellResolution);
+ return defaultValue;
+ }
+ }
+
+ private TtsExtent parseTtsExtent(XmlPullParser xmlParser) {
+ String ttsExtent = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT);
+ if (ttsExtent == null) {
+ return null;
+ }
+
+ Matcher extentMatcher = PIXEL_COORDINATES.matcher(ttsExtent);
+ if (!extentMatcher.matches()) {
+ Log.w(TAG, "Ignoring non-pixel tts extent: " + ttsExtent);
+ return null;
+ }
+ try {
+ int width = Integer.parseInt(extentMatcher.group(1));
+ int height = Integer.parseInt(extentMatcher.group(2));
+ return new TtsExtent(width, height);
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Ignoring malformed tts extent: " + ttsExtent);
+ return null;
+ }
+ }
+
+ private Map<String, TtmlStyle> parseHeader(
+ XmlPullParser xmlParser,
+ Map<String, TtmlStyle> globalStyles,
+ CellResolution cellResolution,
+ TtsExtent ttsExtent,
+ Map<String, TtmlRegion> globalRegions,
+ Map<String, String> imageMap)
+ throws IOException, XmlPullParserException {
+ do {
+ xmlParser.next();
+ if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_STYLE)) {
+ String parentStyleId = XmlPullParserUtil.getAttributeValue(xmlParser, ATTR_STYLE);
+ TtmlStyle style = parseStyleAttributes(xmlParser, new TtmlStyle());
+ if (parentStyleId != null) {
+ for (String id : parseStyleIds(parentStyleId)) {
+ style.chain(globalStyles.get(id));
+ }
+ }
+ if (style.getId() != null) {
+ globalStyles.put(style.getId(), style);
+ }
+ } else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_REGION)) {
+ TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser, cellResolution, ttsExtent);
+ if (ttmlRegion != null) {
+ globalRegions.put(ttmlRegion.id, ttmlRegion);
+ }
+ } else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_METADATA)) {
+ parseMetadata(xmlParser, imageMap);
+ }
+ } while (!XmlPullParserUtil.isEndTag(xmlParser, TtmlNode.TAG_HEAD));
+ return globalStyles;
+ }
+
+ private void parseMetadata(XmlPullParser xmlParser, Map<String, String> imageMap)
+ throws IOException, XmlPullParserException {
+ do {
+ xmlParser.next();
+ if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_IMAGE)) {
+ String id = XmlPullParserUtil.getAttributeValue(xmlParser, "id");
+ if (id != null) {
+ String encodedBitmapData = xmlParser.nextText();
+ imageMap.put(id, encodedBitmapData);
+ }
+ }
+ } while (!XmlPullParserUtil.isEndTag(xmlParser, TtmlNode.TAG_METADATA));
+ }
+
+ /**
+ * Parses a region declaration.
+ *
+ * <p>Supports both percentage and pixel defined regions. In case of pixel defined regions the
+ * passed {@code ttsExtent} is used as a reference window to convert the pixel values to
+ * fractions. In case of missing tts:extent the pixel defined regions can't be parsed, and null is
+ * returned.
+ */
+ private TtmlRegion parseRegionAttributes(
+ XmlPullParser xmlParser, CellResolution cellResolution, TtsExtent ttsExtent) {
+ String regionId = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID);
+ if (regionId == null) {
+ return null;
+ }
+
+ float position;
+ float line;
+
+ String regionOrigin = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_ORIGIN);
+ if (regionOrigin != null) {
+ Matcher originPercentageMatcher = PERCENTAGE_COORDINATES.matcher(regionOrigin);
+ Matcher originPixelMatcher = PIXEL_COORDINATES.matcher(regionOrigin);
+ if (originPercentageMatcher.matches()) {
+ try {
+ position = Float.parseFloat(originPercentageMatcher.group(1)) / 100f;
+ line = Float.parseFloat(originPercentageMatcher.group(2)) / 100f;
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Ignoring region with malformed origin: " + regionOrigin);
+ return null;
+ }
+ } else if (originPixelMatcher.matches()) {
+ if (ttsExtent == null) {
+ Log.w(TAG, "Ignoring region with missing tts:extent: " + regionOrigin);
+ return null;
+ }
+ try {
+ int width = Integer.parseInt(originPixelMatcher.group(1));
+ int height = Integer.parseInt(originPixelMatcher.group(2));
+ // Convert pixel values to fractions.
+ position = width / (float) ttsExtent.width;
+ line = height / (float) ttsExtent.height;
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Ignoring region with malformed origin: " + regionOrigin);
+ return null;
+ }
+ } else {
+ Log.w(TAG, "Ignoring region with unsupported origin: " + regionOrigin);
+ return null;
+ }
+ } else {
+ Log.w(TAG, "Ignoring region without an origin");
+ return null;
+ // TODO: Should default to top left as below in this case, but need to fix
+ // https://github.com/google/ExoPlayer/issues/2953 first.
+ // Origin is omitted. Default to top left.
+ // position = 0;
+ // line = 0;
+ }
+
+ float width;
+ float height;
+ String regionExtent = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT);
+ if (regionExtent != null) {
+ Matcher extentPercentageMatcher = PERCENTAGE_COORDINATES.matcher(regionExtent);
+ Matcher extentPixelMatcher = PIXEL_COORDINATES.matcher(regionExtent);
+ if (extentPercentageMatcher.matches()) {
+ try {
+ width = Float.parseFloat(extentPercentageMatcher.group(1)) / 100f;
+ height = Float.parseFloat(extentPercentageMatcher.group(2)) / 100f;
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Ignoring region with malformed extent: " + regionOrigin);
+ return null;
+ }
+ } else if (extentPixelMatcher.matches()) {
+ if (ttsExtent == null) {
+ Log.w(TAG, "Ignoring region with missing tts:extent: " + regionOrigin);
+ return null;
+ }
+ try {
+ int extentWidth = Integer.parseInt(extentPixelMatcher.group(1));
+ int extentHeight = Integer.parseInt(extentPixelMatcher.group(2));
+ // Convert pixel values to fractions.
+ width = extentWidth / (float) ttsExtent.width;
+ height = extentHeight / (float) ttsExtent.height;
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Ignoring region with malformed extent: " + regionOrigin);
+ return null;
+ }
+ } else {
+ Log.w(TAG, "Ignoring region with unsupported extent: " + regionOrigin);
+ return null;
+ }
+ } else {
+ Log.w(TAG, "Ignoring region without an extent");
+ return null;
+ // TODO: Should default to extent of parent as below in this case, but need to fix
+ // https://github.com/google/ExoPlayer/issues/2953 first.
+ // Extent is omitted. Default to extent of parent.
+ // width = 1;
+ // height = 1;
+ }
+
+ @Cue.AnchorType int lineAnchor = Cue.ANCHOR_TYPE_START;
+ String displayAlign = XmlPullParserUtil.getAttributeValue(xmlParser,
+ TtmlNode.ATTR_TTS_DISPLAY_ALIGN);
+ if (displayAlign != null) {
+ switch (Util.toLowerInvariant(displayAlign)) {
+ case "center":
+ lineAnchor = Cue.ANCHOR_TYPE_MIDDLE;
+ line += height / 2;
+ break;
+ case "after":
+ lineAnchor = Cue.ANCHOR_TYPE_END;
+ line += height;
+ break;
+ default:
+ // Default "before" case. Do nothing.
+ break;
+ }
+ }
+
+ float regionTextHeight = 1.0f / cellResolution.rows;
+ return new TtmlRegion(
+ regionId,
+ position,
+ line,
+ /* lineType= */ Cue.LINE_TYPE_FRACTION,
+ lineAnchor,
+ width,
+ height,
+ /* textSizeType= */ Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING,
+ /* textSize= */ regionTextHeight);
+ }
+
+ private String[] parseStyleIds(String parentStyleIds) {
+ parentStyleIds = parentStyleIds.trim();
+ return parentStyleIds.isEmpty() ? new String[0] : Util.split(parentStyleIds, "\\s+");
+ }
+
+ private TtmlStyle parseStyleAttributes(XmlPullParser parser, TtmlStyle style) {
+ int attributeCount = parser.getAttributeCount();
+ for (int i = 0; i < attributeCount; i++) {
+ String attributeValue = parser.getAttributeValue(i);
+ switch (parser.getAttributeName(i)) {
+ case TtmlNode.ATTR_ID:
+ if (TtmlNode.TAG_STYLE.equals(parser.getName())) {
+ style = createIfNull(style).setId(attributeValue);
+ }
+ break;
+ case TtmlNode.ATTR_TTS_BACKGROUND_COLOR:
+ style = createIfNull(style);
+ try {
+ style.setBackgroundColor(ColorParser.parseTtmlColor(attributeValue));
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG, "Failed parsing background value: " + attributeValue);
+ }
+ break;
+ case TtmlNode.ATTR_TTS_COLOR:
+ style = createIfNull(style);
+ try {
+ style.setFontColor(ColorParser.parseTtmlColor(attributeValue));
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG, "Failed parsing color value: " + attributeValue);
+ }
+ break;
+ case TtmlNode.ATTR_TTS_FONT_FAMILY:
+ style = createIfNull(style).setFontFamily(attributeValue);
+ break;
+ case TtmlNode.ATTR_TTS_FONT_SIZE:
+ try {
+ style = createIfNull(style);
+ parseFontSize(attributeValue, style);
+ } catch (SubtitleDecoderException e) {
+ Log.w(TAG, "Failed parsing fontSize value: " + attributeValue);
+ }
+ break;
+ case TtmlNode.ATTR_TTS_FONT_WEIGHT:
+ style = createIfNull(style).setBold(
+ TtmlNode.BOLD.equalsIgnoreCase(attributeValue));
+ break;
+ case TtmlNode.ATTR_TTS_FONT_STYLE:
+ style = createIfNull(style).setItalic(
+ TtmlNode.ITALIC.equalsIgnoreCase(attributeValue));
+ break;
+ case TtmlNode.ATTR_TTS_TEXT_ALIGN:
+ switch (Util.toLowerInvariant(attributeValue)) {
+ case TtmlNode.LEFT:
+ style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_NORMAL);
+ break;
+ case TtmlNode.START:
+ style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_NORMAL);
+ break;
+ case TtmlNode.RIGHT:
+ style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_OPPOSITE);
+ break;
+ case TtmlNode.END:
+ style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_OPPOSITE);
+ break;
+ case TtmlNode.CENTER:
+ style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_CENTER);
+ break;
+ }
+ break;
+ case TtmlNode.ATTR_TTS_TEXT_DECORATION:
+ switch (Util.toLowerInvariant(attributeValue)) {
+ case TtmlNode.LINETHROUGH:
+ style = createIfNull(style).setLinethrough(true);
+ break;
+ case TtmlNode.NO_LINETHROUGH:
+ style = createIfNull(style).setLinethrough(false);
+ break;
+ case TtmlNode.UNDERLINE:
+ style = createIfNull(style).setUnderline(true);
+ break;
+ case TtmlNode.NO_UNDERLINE:
+ style = createIfNull(style).setUnderline(false);
+ break;
+ }
+ break;
+ default:
+ // ignore
+ break;
+ }
+ }
+ return style;
+ }
+
+ private TtmlStyle createIfNull(TtmlStyle style) {
+ return style == null ? new TtmlStyle() : style;
+ }
+
+ private TtmlNode parseNode(XmlPullParser parser, TtmlNode parent,
+ Map<String, TtmlRegion> regionMap, FrameAndTickRate frameAndTickRate)
+ throws SubtitleDecoderException {
+ long duration = C.TIME_UNSET;
+ long startTime = C.TIME_UNSET;
+ long endTime = C.TIME_UNSET;
+ String regionId = TtmlNode.ANONYMOUS_REGION_ID;
+ String imageId = null;
+ String[] styleIds = null;
+ int attributeCount = parser.getAttributeCount();
+ TtmlStyle style = parseStyleAttributes(parser, null);
+ for (int i = 0; i < attributeCount; i++) {
+ String attr = parser.getAttributeName(i);
+ String value = parser.getAttributeValue(i);
+ switch (attr) {
+ case ATTR_BEGIN:
+ startTime = parseTimeExpression(value, frameAndTickRate);
+ break;
+ case ATTR_END:
+ endTime = parseTimeExpression(value, frameAndTickRate);
+ break;
+ case ATTR_DURATION:
+ duration = parseTimeExpression(value, frameAndTickRate);
+ break;
+ case ATTR_STYLE:
+ // IDREFS: potentially multiple space delimited ids
+ String[] ids = parseStyleIds(value);
+ if (ids.length > 0) {
+ styleIds = ids;
+ }
+ break;
+ case ATTR_REGION:
+ if (regionMap.containsKey(value)) {
+ // If the region has not been correctly declared or does not define a position, we use
+ // the anonymous region.
+ regionId = value;
+ }
+ break;
+ case ATTR_IMAGE:
+ // Parse URI reference only if refers to an element in the same document (it must start
+ // with '#'). Resolving URIs from external sources is not supported.
+ if (value.startsWith("#")) {
+ imageId = value.substring(1);
+ }
+ break;
+ default:
+ // Do nothing.
+ break;
+ }
+ }
+ if (parent != null && parent.startTimeUs != C.TIME_UNSET) {
+ if (startTime != C.TIME_UNSET) {
+ startTime += parent.startTimeUs;
+ }
+ if (endTime != C.TIME_UNSET) {
+ endTime += parent.startTimeUs;
+ }
+ }
+ if (endTime == C.TIME_UNSET) {
+ if (duration != C.TIME_UNSET) {
+ // Infer the end time from the duration.
+ endTime = startTime + duration;
+ } else if (parent != null && parent.endTimeUs != C.TIME_UNSET) {
+ // If the end time remains unspecified, then it should be inherited from the parent.
+ endTime = parent.endTimeUs;
+ }
+ }
+ return TtmlNode.buildNode(
+ parser.getName(), startTime, endTime, style, styleIds, regionId, imageId);
+ }
+
+ private static boolean isSupportedTag(String tag) {
+ return tag.equals(TtmlNode.TAG_TT)
+ || tag.equals(TtmlNode.TAG_HEAD)
+ || tag.equals(TtmlNode.TAG_BODY)
+ || tag.equals(TtmlNode.TAG_DIV)
+ || tag.equals(TtmlNode.TAG_P)
+ || tag.equals(TtmlNode.TAG_SPAN)
+ || tag.equals(TtmlNode.TAG_BR)
+ || tag.equals(TtmlNode.TAG_STYLE)
+ || tag.equals(TtmlNode.TAG_STYLING)
+ || tag.equals(TtmlNode.TAG_LAYOUT)
+ || tag.equals(TtmlNode.TAG_REGION)
+ || tag.equals(TtmlNode.TAG_METADATA)
+ || tag.equals(TtmlNode.TAG_IMAGE)
+ || tag.equals(TtmlNode.TAG_DATA)
+ || tag.equals(TtmlNode.TAG_INFORMATION);
+ }
+
+ private static void parseFontSize(String expression, TtmlStyle out) throws
+ SubtitleDecoderException {
+ String[] expressions = Util.split(expression, "\\s+");
+ Matcher matcher;
+ if (expressions.length == 1) {
+ matcher = FONT_SIZE.matcher(expression);
+ } else if (expressions.length == 2){
+ matcher = FONT_SIZE.matcher(expressions[1]);
+ Log.w(TAG, "Multiple values in fontSize attribute. Picking the second value for vertical font"
+ + " size and ignoring the first.");
+ } else {
+ throw new SubtitleDecoderException("Invalid number of entries for fontSize: "
+ + expressions.length + ".");
+ }
+
+ if (matcher.matches()) {
+ String unit = matcher.group(3);
+ switch (unit) {
+ case "px":
+ out.setFontSizeUnit(TtmlStyle.FONT_SIZE_UNIT_PIXEL);
+ break;
+ case "em":
+ out.setFontSizeUnit(TtmlStyle.FONT_SIZE_UNIT_EM);
+ break;
+ case "%":
+ out.setFontSizeUnit(TtmlStyle.FONT_SIZE_UNIT_PERCENT);
+ break;
+ default:
+ throw new SubtitleDecoderException("Invalid unit for fontSize: '" + unit + "'.");
+ }
+ out.setFontSize(Float.valueOf(matcher.group(1)));
+ } else {
+ throw new SubtitleDecoderException("Invalid expression for fontSize: '" + expression + "'.");
+ }
+ }
+
+ /**
+ * Parses a time expression, returning the parsed timestamp.
+ * <p>
+ * For the format of a time expression, see:
+ * <a href="http://www.w3.org/TR/ttaf1-dfxp/#timing-value-timeExpression">timeExpression</a>
+ *
+ * @param time A string that includes the time expression.
+ * @param frameAndTickRate The effective frame and tick rates of the stream.
+ * @return The parsed timestamp in microseconds.
+ * @throws SubtitleDecoderException If the given string does not contain a valid time expression.
+ */
+ private static long parseTimeExpression(String time, FrameAndTickRate frameAndTickRate)
+ throws SubtitleDecoderException {
+ Matcher matcher = CLOCK_TIME.matcher(time);
+ if (matcher.matches()) {
+ String hours = matcher.group(1);
+ double durationSeconds = Long.parseLong(hours) * 3600;
+ String minutes = matcher.group(2);
+ durationSeconds += Long.parseLong(minutes) * 60;
+ String seconds = matcher.group(3);
+ durationSeconds += Long.parseLong(seconds);
+ String fraction = matcher.group(4);
+ durationSeconds += (fraction != null) ? Double.parseDouble(fraction) : 0;
+ String frames = matcher.group(5);
+ durationSeconds += (frames != null)
+ ? Long.parseLong(frames) / frameAndTickRate.effectiveFrameRate : 0;
+ String subframes = matcher.group(6);
+ durationSeconds += (subframes != null)
+ ? ((double) Long.parseLong(subframes)) / frameAndTickRate.subFrameRate
+ / frameAndTickRate.effectiveFrameRate
+ : 0;
+ return (long) (durationSeconds * C.MICROS_PER_SECOND);
+ }
+ matcher = OFFSET_TIME.matcher(time);
+ if (matcher.matches()) {
+ String timeValue = matcher.group(1);
+ double offsetSeconds = Double.parseDouble(timeValue);
+ String unit = matcher.group(2);
+ switch (unit) {
+ case "h":
+ offsetSeconds *= 3600;
+ break;
+ case "m":
+ offsetSeconds *= 60;
+ break;
+ case "s":
+ // Do nothing.
+ break;
+ case "ms":
+ offsetSeconds /= 1000;
+ break;
+ case "f":
+ offsetSeconds /= frameAndTickRate.effectiveFrameRate;
+ break;
+ case "t":
+ offsetSeconds /= frameAndTickRate.tickRate;
+ break;
+ }
+ return (long) (offsetSeconds * C.MICROS_PER_SECOND);
+ }
+ throw new SubtitleDecoderException("Malformed time expression: " + time);
+ }
+
+ private static final class FrameAndTickRate {
+ final float effectiveFrameRate;
+ final int subFrameRate;
+ final int tickRate;
+
+ FrameAndTickRate(float effectiveFrameRate, int subFrameRate, int tickRate) {
+ this.effectiveFrameRate = effectiveFrameRate;
+ this.subFrameRate = subFrameRate;
+ this.tickRate = tickRate;
+ }
+ }
+
+ /** Represents the cell resolution for a TTML file. */
+ private static final class CellResolution {
+ final int columns;
+ final int rows;
+
+ CellResolution(int columns, int rows) {
+ this.columns = columns;
+ this.rows = rows;
+ }
+ }
+
+ /** Represents the tts:extent for a TTML file. */
+ private static final class TtsExtent {
+ final int width;
+ final int height;
+
+ TtsExtent(int width, int height) {
+ this.width = width;
+ this.height = height;
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlNode.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlNode.java
new file mode 100644
index 0000000000..16d0f28f6b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlNode.java
@@ -0,0 +1,399 @@
+/*
+ * 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.ttml;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.text.SpannableStringBuilder;
+import android.util.Base64;
+import android.util.Pair;
+import androidx.annotation.Nullable;
+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.util.Assertions;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+/**
+ * A package internal representation of TTML node.
+ */
+/* package */ final class TtmlNode {
+
+ public static final String TAG_TT = "tt";
+ public static final String TAG_HEAD = "head";
+ public static final String TAG_BODY = "body";
+ public static final String TAG_DIV = "div";
+ public static final String TAG_P = "p";
+ public static final String TAG_SPAN = "span";
+ public static final String TAG_BR = "br";
+ public static final String TAG_STYLE = "style";
+ public static final String TAG_STYLING = "styling";
+ public static final String TAG_LAYOUT = "layout";
+ public static final String TAG_REGION = "region";
+ public static final String TAG_METADATA = "metadata";
+ public static final String TAG_IMAGE = "image";
+ public static final String TAG_DATA = "data";
+ public static final String TAG_INFORMATION = "information";
+
+ public static final String ANONYMOUS_REGION_ID = "";
+ public static final String ATTR_ID = "id";
+ public static final String ATTR_TTS_ORIGIN = "origin";
+ public static final String ATTR_TTS_EXTENT = "extent";
+ public static final String ATTR_TTS_DISPLAY_ALIGN = "displayAlign";
+ public static final String ATTR_TTS_BACKGROUND_COLOR = "backgroundColor";
+ public static final String ATTR_TTS_FONT_STYLE = "fontStyle";
+ public static final String ATTR_TTS_FONT_SIZE = "fontSize";
+ public static final String ATTR_TTS_FONT_FAMILY = "fontFamily";
+ public static final String ATTR_TTS_FONT_WEIGHT = "fontWeight";
+ public static final String ATTR_TTS_COLOR = "color";
+ public static final String ATTR_TTS_TEXT_DECORATION = "textDecoration";
+ public static final String ATTR_TTS_TEXT_ALIGN = "textAlign";
+
+ public static final String LINETHROUGH = "linethrough";
+ public static final String NO_LINETHROUGH = "nolinethrough";
+ public static final String UNDERLINE = "underline";
+ public static final String NO_UNDERLINE = "nounderline";
+ public static final String ITALIC = "italic";
+ public static final String BOLD = "bold";
+
+ public static final String LEFT = "left";
+ public static final String CENTER = "center";
+ public static final String RIGHT = "right";
+ public static final String START = "start";
+ public static final String END = "end";
+
+ @Nullable public final String tag;
+ @Nullable public final String text;
+ public final boolean isTextNode;
+ public final long startTimeUs;
+ public final long endTimeUs;
+ @Nullable public final TtmlStyle style;
+ @Nullable private final String[] styleIds;
+ public final String regionId;
+ @Nullable public final String imageId;
+
+ private final HashMap<String, Integer> nodeStartsByRegion;
+ private final HashMap<String, Integer> nodeEndsByRegion;
+
+ private List<TtmlNode> children;
+
+ public static TtmlNode buildTextNode(String text) {
+ return new TtmlNode(
+ /* tag= */ null,
+ TtmlRenderUtil.applyTextElementSpacePolicy(text),
+ /* startTimeUs= */ C.TIME_UNSET,
+ /* endTimeUs= */ C.TIME_UNSET,
+ /* style= */ null,
+ /* styleIds= */ null,
+ ANONYMOUS_REGION_ID,
+ /* imageId= */ null);
+ }
+
+ public static TtmlNode buildNode(
+ @Nullable String tag,
+ long startTimeUs,
+ long endTimeUs,
+ @Nullable TtmlStyle style,
+ @Nullable String[] styleIds,
+ String regionId,
+ @Nullable String imageId) {
+ return new TtmlNode(
+ tag, /* text= */ null, startTimeUs, endTimeUs, style, styleIds, regionId, imageId);
+ }
+
+ private TtmlNode(
+ @Nullable String tag,
+ @Nullable String text,
+ long startTimeUs,
+ long endTimeUs,
+ @Nullable TtmlStyle style,
+ @Nullable String[] styleIds,
+ String regionId,
+ @Nullable String imageId) {
+ this.tag = tag;
+ this.text = text;
+ this.imageId = imageId;
+ this.style = style;
+ this.styleIds = styleIds;
+ this.isTextNode = text != null;
+ this.startTimeUs = startTimeUs;
+ this.endTimeUs = endTimeUs;
+ this.regionId = Assertions.checkNotNull(regionId);
+ nodeStartsByRegion = new HashMap<>();
+ nodeEndsByRegion = new HashMap<>();
+ }
+
+ public boolean isActive(long timeUs) {
+ return (startTimeUs == C.TIME_UNSET && endTimeUs == C.TIME_UNSET)
+ || (startTimeUs <= timeUs && endTimeUs == C.TIME_UNSET)
+ || (startTimeUs == C.TIME_UNSET && timeUs < endTimeUs)
+ || (startTimeUs <= timeUs && timeUs < endTimeUs);
+ }
+
+ public void addChild(TtmlNode child) {
+ if (children == null) {
+ children = new ArrayList<>();
+ }
+ children.add(child);
+ }
+
+ public TtmlNode getChild(int index) {
+ if (children == null) {
+ throw new IndexOutOfBoundsException();
+ }
+ return children.get(index);
+ }
+
+ public int getChildCount() {
+ return children == null ? 0 : children.size();
+ }
+
+ public long[] getEventTimesUs() {
+ TreeSet<Long> eventTimeSet = new TreeSet<>();
+ getEventTimes(eventTimeSet, false);
+ long[] eventTimes = new long[eventTimeSet.size()];
+ int i = 0;
+ for (long eventTimeUs : eventTimeSet) {
+ eventTimes[i++] = eventTimeUs;
+ }
+ return eventTimes;
+ }
+
+ private void getEventTimes(TreeSet<Long> out, boolean descendsPNode) {
+ boolean isPNode = TAG_P.equals(tag);
+ boolean isDivNode = TAG_DIV.equals(tag);
+ if (descendsPNode || isPNode || (isDivNode && imageId != null)) {
+ if (startTimeUs != C.TIME_UNSET) {
+ out.add(startTimeUs);
+ }
+ if (endTimeUs != C.TIME_UNSET) {
+ out.add(endTimeUs);
+ }
+ }
+ if (children == null) {
+ return;
+ }
+ for (int i = 0; i < children.size(); i++) {
+ children.get(i).getEventTimes(out, descendsPNode || isPNode);
+ }
+ }
+
+ public String[] getStyleIds() {
+ return styleIds;
+ }
+
+ public List<Cue> getCues(
+ long timeUs,
+ Map<String, TtmlStyle> globalStyles,
+ Map<String, TtmlRegion> regionMap,
+ Map<String, String> imageMap) {
+
+ List<Pair<String, String>> regionImageOutputs = new ArrayList<>();
+ traverseForImage(timeUs, regionId, regionImageOutputs);
+
+ TreeMap<String, SpannableStringBuilder> regionTextOutputs = new TreeMap<>();
+ traverseForText(timeUs, false, regionId, regionTextOutputs);
+ traverseForStyle(timeUs, globalStyles, regionTextOutputs);
+
+ List<Cue> cues = new ArrayList<>();
+
+ // Create image based cues.
+ for (Pair<String, String> regionImagePair : regionImageOutputs) {
+ String encodedBitmapData = imageMap.get(regionImagePair.second);
+ if (encodedBitmapData == null) {
+ // Image reference points to an invalid image. Do nothing.
+ continue;
+ }
+
+ byte[] bitmapData = Base64.decode(encodedBitmapData, Base64.DEFAULT);
+ Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, /* offset= */ 0, bitmapData.length);
+ TtmlRegion region = regionMap.get(regionImagePair.first);
+
+ cues.add(
+ new Cue(
+ bitmap,
+ region.position,
+ Cue.ANCHOR_TYPE_START,
+ region.line,
+ region.lineAnchor,
+ region.width,
+ region.height));
+ }
+
+ // Create text based cues.
+ for (Entry<String, SpannableStringBuilder> entry : regionTextOutputs.entrySet()) {
+ TtmlRegion region = regionMap.get(entry.getKey());
+ cues.add(
+ new Cue(
+ cleanUpText(entry.getValue()),
+ /* textAlignment= */ null,
+ region.line,
+ region.lineType,
+ region.lineAnchor,
+ region.position,
+ /* positionAnchor= */ Cue.TYPE_UNSET,
+ region.width,
+ region.textSizeType,
+ region.textSize));
+ }
+
+ return cues;
+ }
+
+ private void traverseForImage(
+ long timeUs, String inheritedRegion, List<Pair<String, String>> regionImageList) {
+ String resolvedRegionId = ANONYMOUS_REGION_ID.equals(regionId) ? inheritedRegion : regionId;
+ if (isActive(timeUs) && TAG_DIV.equals(tag) && imageId != null) {
+ regionImageList.add(new Pair<>(resolvedRegionId, imageId));
+ return;
+ }
+ for (int i = 0; i < getChildCount(); ++i) {
+ getChild(i).traverseForImage(timeUs, resolvedRegionId, regionImageList);
+ }
+ }
+
+ private void traverseForText(
+ long timeUs,
+ boolean descendsPNode,
+ String inheritedRegion,
+ Map<String, SpannableStringBuilder> regionOutputs) {
+ nodeStartsByRegion.clear();
+ nodeEndsByRegion.clear();
+ if (TAG_METADATA.equals(tag)) {
+ // Ignore metadata tag.
+ return;
+ }
+
+ String resolvedRegionId = ANONYMOUS_REGION_ID.equals(regionId) ? inheritedRegion : regionId;
+
+ if (isTextNode && descendsPNode) {
+ getRegionOutput(resolvedRegionId, regionOutputs).append(text);
+ } else if (TAG_BR.equals(tag) && descendsPNode) {
+ getRegionOutput(resolvedRegionId, regionOutputs).append('\n');
+ } else if (isActive(timeUs)) {
+ // This is a container node, which can contain zero or more children.
+ for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {
+ nodeStartsByRegion.put(entry.getKey(), entry.getValue().length());
+ }
+
+ boolean isPNode = TAG_P.equals(tag);
+ for (int i = 0; i < getChildCount(); i++) {
+ getChild(i).traverseForText(timeUs, descendsPNode || isPNode, resolvedRegionId,
+ regionOutputs);
+ }
+ if (isPNode) {
+ TtmlRenderUtil.endParagraph(getRegionOutput(resolvedRegionId, regionOutputs));
+ }
+
+ for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {
+ nodeEndsByRegion.put(entry.getKey(), entry.getValue().length());
+ }
+ }
+ }
+
+ private static SpannableStringBuilder getRegionOutput(
+ String resolvedRegionId, Map<String, SpannableStringBuilder> regionOutputs) {
+ if (!regionOutputs.containsKey(resolvedRegionId)) {
+ regionOutputs.put(resolvedRegionId, new SpannableStringBuilder());
+ }
+ return regionOutputs.get(resolvedRegionId);
+ }
+
+ private void traverseForStyle(
+ long timeUs,
+ Map<String, TtmlStyle> globalStyles,
+ Map<String, SpannableStringBuilder> regionOutputs) {
+ if (!isActive(timeUs)) {
+ return;
+ }
+ for (Entry<String, Integer> entry : nodeEndsByRegion.entrySet()) {
+ String regionId = entry.getKey();
+ int start = nodeStartsByRegion.containsKey(regionId) ? nodeStartsByRegion.get(regionId) : 0;
+ int end = entry.getValue();
+ if (start != end) {
+ SpannableStringBuilder regionOutput = regionOutputs.get(regionId);
+ applyStyleToOutput(globalStyles, regionOutput, start, end);
+ }
+ }
+ for (int i = 0; i < getChildCount(); ++i) {
+ getChild(i).traverseForStyle(timeUs, globalStyles, regionOutputs);
+ }
+ }
+
+ private void applyStyleToOutput(
+ Map<String, TtmlStyle> globalStyles,
+ SpannableStringBuilder regionOutput,
+ int start,
+ int end) {
+ TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles);
+ if (resolvedStyle != null) {
+ TtmlRenderUtil.applyStylesToSpan(regionOutput, start, end, resolvedStyle);
+ }
+ }
+
+ private SpannableStringBuilder cleanUpText(SpannableStringBuilder builder) {
+ // Having joined the text elements, we need to do some final cleanup on the result.
+ // 1. Collapse multiple consecutive spaces into a single space.
+ int builderLength = builder.length();
+ for (int i = 0; i < builderLength; i++) {
+ if (builder.charAt(i) == ' ') {
+ int j = i + 1;
+ while (j < builder.length() && builder.charAt(j) == ' ') {
+ j++;
+ }
+ int spacesToDelete = j - (i + 1);
+ if (spacesToDelete > 0) {
+ builder.delete(i, i + spacesToDelete);
+ builderLength -= spacesToDelete;
+ }
+ }
+ }
+ // 2. Remove any spaces from the start of each line.
+ if (builderLength > 0 && builder.charAt(0) == ' ') {
+ builder.delete(0, 1);
+ builderLength--;
+ }
+ for (int i = 0; i < builderLength - 1; i++) {
+ if (builder.charAt(i) == '\n' && builder.charAt(i + 1) == ' ') {
+ builder.delete(i + 1, i + 2);
+ builderLength--;
+ }
+ }
+ // 3. Remove any spaces from the end of each line.
+ if (builderLength > 0 && builder.charAt(builderLength - 1) == ' ') {
+ builder.delete(builderLength - 1, builderLength);
+ builderLength--;
+ }
+ for (int i = 0; i < builderLength - 1; i++) {
+ if (builder.charAt(i) == ' ' && builder.charAt(i + 1) == '\n') {
+ builder.delete(i, i + 1);
+ builderLength--;
+ }
+ }
+ // 4. Trim a trailing newline, if there is one.
+ if (builderLength > 0 && builder.charAt(builderLength - 1) == '\n') {
+ builder.delete(builderLength - 1, builderLength);
+ /*builderLength--;*/
+ }
+ return builder;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlRegion.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlRegion.java
new file mode 100644
index 0000000000..d14e547d49
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlRegion.java
@@ -0,0 +1,69 @@
+/*
+ * 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.ttml;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue;
+
+/**
+ * Represents a TTML Region.
+ */
+/* package */ final class TtmlRegion {
+
+ public final String id;
+ public final float position;
+ public final float line;
+ public final @Cue.LineType int lineType;
+ public final @Cue.AnchorType int lineAnchor;
+ public final float width;
+ public final float height;
+ public final @Cue.TextSizeType int textSizeType;
+ public final float textSize;
+
+ public TtmlRegion(String id) {
+ this(
+ id,
+ /* position= */ Cue.DIMEN_UNSET,
+ /* line= */ Cue.DIMEN_UNSET,
+ /* lineType= */ Cue.TYPE_UNSET,
+ /* lineAnchor= */ Cue.TYPE_UNSET,
+ /* width= */ Cue.DIMEN_UNSET,
+ /* height= */ Cue.DIMEN_UNSET,
+ /* textSizeType= */ Cue.TYPE_UNSET,
+ /* textSize= */ Cue.DIMEN_UNSET);
+ }
+
+ public TtmlRegion(
+ String id,
+ float position,
+ float line,
+ @Cue.LineType int lineType,
+ @Cue.AnchorType int lineAnchor,
+ float width,
+ float height,
+ int textSizeType,
+ float textSize) {
+ this.id = id;
+ this.position = position;
+ this.line = line;
+ this.lineType = lineType;
+ this.lineAnchor = lineAnchor;
+ this.width = width;
+ this.height = height;
+ this.textSizeType = textSizeType;
+ this.textSize = textSize;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java
new file mode 100644
index 0000000000..f2387b6282
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java
@@ -0,0 +1,151 @@
+/*
+ * 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.ttml;
+
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+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 java.util.Map;
+
+/**
+ * Package internal utility class to render styled <code>TtmlNode</code>s.
+ */
+/* package */ final class TtmlRenderUtil {
+
+ public static TtmlStyle resolveStyle(TtmlStyle style, String[] styleIds,
+ Map<String, TtmlStyle> globalStyles) {
+ if (style == null && styleIds == null) {
+ // No styles at all.
+ return null;
+ } else if (style == null && styleIds.length == 1) {
+ // Only one single referential style present.
+ return globalStyles.get(styleIds[0]);
+ } else if (style == null && styleIds.length > 1) {
+ // Only multiple referential styles present.
+ TtmlStyle chainedStyle = new TtmlStyle();
+ for (String id : styleIds) {
+ chainedStyle.chain(globalStyles.get(id));
+ }
+ return chainedStyle;
+ } else if (style != null && styleIds != null && styleIds.length == 1) {
+ // Merge a single referential style into inline style.
+ return style.chain(globalStyles.get(styleIds[0]));
+ } else if (style != null && styleIds != null && styleIds.length > 1) {
+ // Merge multiple referential styles into inline style.
+ for (String id : styleIds) {
+ style.chain(globalStyles.get(id));
+ }
+ return style;
+ }
+ // Only inline styles available.
+ return style;
+ }
+
+ public static void applyStylesToSpan(SpannableStringBuilder builder,
+ int start, int end, TtmlStyle style) {
+
+ if (style.getStyle() != TtmlStyle.UNSPECIFIED) {
+ builder.setSpan(new StyleSpan(style.getStyle()), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.isLinethrough()) {
+ builder.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.isUnderline()) {
+ builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.hasFontColor()) {
+ builder.setSpan(new ForegroundColorSpan(style.getFontColor()), start, end,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.hasBackgroundColor()) {
+ builder.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.getFontFamily() != null) {
+ builder.setSpan(new TypefaceSpan(style.getFontFamily()), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.getTextAlign() != null) {
+ builder.setSpan(new AlignmentSpan.Standard(style.getTextAlign()), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ switch (style.getFontSizeUnit()) {
+ case TtmlStyle.FONT_SIZE_UNIT_PIXEL:
+ builder.setSpan(new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case TtmlStyle.FONT_SIZE_UNIT_EM:
+ builder.setSpan(new RelativeSizeSpan(style.getFontSize()), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case TtmlStyle.FONT_SIZE_UNIT_PERCENT:
+ builder.setSpan(new RelativeSizeSpan(style.getFontSize() / 100), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case TtmlStyle.UNSPECIFIED:
+ // Do nothing.
+ break;
+ }
+ }
+
+ /**
+ * Called when the end of a paragraph is encountered. Adds a newline if there are one or more
+ * non-space characters since the previous newline.
+ *
+ * @param builder The builder.
+ */
+ /* package */ static void endParagraph(SpannableStringBuilder builder) {
+ int position = builder.length() - 1;
+ while (position >= 0 && builder.charAt(position) == ' ') {
+ position--;
+ }
+ if (position >= 0 && builder.charAt(position) != '\n') {
+ builder.append('\n');
+ }
+ }
+
+ /**
+ * Applies the appropriate space policy to the given text element.
+ *
+ * @param in The text element to which the policy should be applied.
+ * @return The result of applying the policy to the text element.
+ */
+ /* package */ static String applyTextElementSpacePolicy(String in) {
+ // Removes carriage return followed by line feed. See: http://www.w3.org/TR/xml/#sec-line-ends
+ String out = in.replaceAll("\r\n", "\n");
+ // Apply suppress-at-line-break="auto" and
+ // white-space-treatment="ignore-if-surrounding-linefeed"
+ out = out.replaceAll(" *\n *", "\n");
+ // Apply linefeed-treatment="treat-as-space"
+ out = out.replaceAll("\n", " ");
+ // Apply white-space-collapse="true"
+ out = out.replaceAll("[ \t\\x0B\f\r]+", " ");
+ return out;
+ }
+
+ private TtmlRenderUtil() {}
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlStyle.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlStyle.java
new file mode 100644
index 0000000000..57faaecb69
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlStyle.java
@@ -0,0 +1,268 @@
+/*
+ * 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.ttml;
+
+import android.graphics.Typeface;
+import android.text.Layout;
+import androidx.annotation.IntDef;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Style object of a <code>TtmlNode</code>
+ */
+/* package */ final class TtmlStyle {
+
+ public static final int UNSPECIFIED = -1;
+
+ @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;
+
+ @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;
+
+ 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;
+ private String id;
+ private TtmlStyle inheritableStyle;
+ private Layout.Alignment textAlign;
+
+ public TtmlStyle() {
+ linethrough = UNSPECIFIED;
+ underline = UNSPECIFIED;
+ bold = UNSPECIFIED;
+ italic = UNSPECIFIED;
+ fontSizeUnit = UNSPECIFIED;
+ }
+
+ /**
+ * 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 TtmlStyle setLinethrough(boolean linethrough) {
+ Assertions.checkState(inheritableStyle == null);
+ this.linethrough = linethrough ? ON : OFF;
+ return this;
+ }
+
+ public boolean isUnderline() {
+ return underline == ON;
+ }
+
+ public TtmlStyle setUnderline(boolean underline) {
+ Assertions.checkState(inheritableStyle == null);
+ this.underline = underline ? ON : OFF;
+ return this;
+ }
+
+ public TtmlStyle setBold(boolean bold) {
+ Assertions.checkState(inheritableStyle == null);
+ this.bold = bold ? ON : OFF;
+ return this;
+ }
+
+ public TtmlStyle setItalic(boolean italic) {
+ Assertions.checkState(inheritableStyle == null);
+ this.italic = italic ? ON : OFF;
+ return this;
+ }
+
+ public String getFontFamily() {
+ return fontFamily;
+ }
+
+ public TtmlStyle setFontFamily(String fontFamily) {
+ Assertions.checkState(inheritableStyle == null);
+ this.fontFamily = fontFamily;
+ return this;
+ }
+
+ public int getFontColor() {
+ if (!hasFontColor) {
+ throw new IllegalStateException("Font color has not been defined.");
+ }
+ return fontColor;
+ }
+
+ public TtmlStyle setFontColor(int fontColor) {
+ Assertions.checkState(inheritableStyle == null);
+ this.fontColor = fontColor;
+ hasFontColor = true;
+ return this;
+ }
+
+ public boolean hasFontColor() {
+ return hasFontColor;
+ }
+
+ public int getBackgroundColor() {
+ if (!hasBackgroundColor) {
+ throw new IllegalStateException("Background color has not been defined.");
+ }
+ return backgroundColor;
+ }
+
+ public TtmlStyle setBackgroundColor(int backgroundColor) {
+ this.backgroundColor = backgroundColor;
+ hasBackgroundColor = true;
+ return this;
+ }
+
+ public boolean hasBackgroundColor() {
+ return hasBackgroundColor;
+ }
+
+ /**
+ * Inherits from an ancestor style. Properties like <i>tts:backgroundColor</i> which
+ * are not inheritable are not inherited as well as properties which are already set locally
+ * are never overridden.
+ *
+ * @param ancestor the ancestor style to inherit from
+ */
+ public TtmlStyle inherit(TtmlStyle ancestor) {
+ return inherit(ancestor, false);
+ }
+
+ /**
+ * Chains this style to referential style. Local properties which are already set
+ * are never overridden.
+ *
+ * @param ancestor the referential style to inherit from
+ */
+ public TtmlStyle chain(TtmlStyle ancestor) {
+ return inherit(ancestor, true);
+ }
+
+ private TtmlStyle inherit(TtmlStyle ancestor, boolean chaining) {
+ if (ancestor != null) {
+ if (!hasFontColor && ancestor.hasFontColor) {
+ setFontColor(ancestor.fontColor);
+ }
+ if (bold == UNSPECIFIED) {
+ bold = ancestor.bold;
+ }
+ if (italic == UNSPECIFIED) {
+ italic = ancestor.italic;
+ }
+ if (fontFamily == null) {
+ fontFamily = ancestor.fontFamily;
+ }
+ if (linethrough == UNSPECIFIED) {
+ linethrough = ancestor.linethrough;
+ }
+ if (underline == UNSPECIFIED) {
+ underline = ancestor.underline;
+ }
+ if (textAlign == null) {
+ textAlign = ancestor.textAlign;
+ }
+ if (fontSizeUnit == UNSPECIFIED) {
+ fontSizeUnit = ancestor.fontSizeUnit;
+ fontSize = ancestor.fontSize;
+ }
+ // attributes not inherited as of http://www.w3.org/TR/ttml1/
+ if (chaining && !hasBackgroundColor && ancestor.hasBackgroundColor) {
+ setBackgroundColor(ancestor.backgroundColor);
+ }
+ }
+ return this;
+ }
+
+ public TtmlStyle setId(String id) {
+ this.id = id;
+ return this;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public Layout.Alignment getTextAlign() {
+ return textAlign;
+ }
+
+ public TtmlStyle setTextAlign(Layout.Alignment textAlign) {
+ this.textAlign = textAlign;
+ return this;
+ }
+
+ public TtmlStyle setFontSize(float fontSize) {
+ this.fontSize = fontSize;
+ return this;
+ }
+
+ public TtmlStyle setFontSizeUnit(int fontSizeUnit) {
+ this.fontSizeUnit = fontSizeUnit;
+ return this;
+ }
+
+ @FontSizeUnit public int getFontSizeUnit() {
+ return fontSizeUnit;
+ }
+
+ public float getFontSize() {
+ return fontSize;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java
new file mode 100644
index 0000000000..52bd389818
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java
@@ -0,0 +1,81 @@
+/*
+ * 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.ttml;
+
+import androidx.annotation.VisibleForTesting;
+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.Util;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A representation of a TTML subtitle.
+ */
+/* package */ final class TtmlSubtitle implements Subtitle {
+
+ private final TtmlNode root;
+ private final long[] eventTimesUs;
+ private final Map<String, TtmlStyle> globalStyles;
+ private final Map<String, TtmlRegion> regionMap;
+ private final Map<String, String> imageMap;
+
+ public TtmlSubtitle(
+ TtmlNode root,
+ Map<String, TtmlStyle> globalStyles,
+ Map<String, TtmlRegion> regionMap,
+ Map<String, String> imageMap) {
+ this.root = root;
+ this.regionMap = regionMap;
+ this.imageMap = imageMap;
+ this.globalStyles =
+ globalStyles != null ? Collections.unmodifiableMap(globalStyles) : Collections.emptyMap();
+ this.eventTimesUs = root.getEventTimesUs();
+ }
+
+ @Override
+ public int getNextEventTimeIndex(long timeUs) {
+ int index = Util.binarySearchCeil(eventTimesUs, timeUs, false, false);
+ return index < eventTimesUs.length ? index : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getEventTimeCount() {
+ return eventTimesUs.length;
+ }
+
+ @Override
+ public long getEventTime(int index) {
+ return eventTimesUs[index];
+ }
+
+ @VisibleForTesting
+ /* package */ TtmlNode getRoot() {
+ return root;
+ }
+
+ @Override
+ public List<Cue> getCues(long timeUs) {
+ return root.getCues(timeUs, globalStyles, regionMap, imageMap);
+ }
+
+ @VisibleForTesting
+ /* package */ Map<String, TtmlStyle> getGlobalStyles() {
+ return globalStyles;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/package-info.java
new file mode 100644
index 0000000000..e6e7a5a8e3
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.ttml;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi;