summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java756
1 files changed, 756 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;
+ }
+ }
+}