diff --git a/library/src/androidTest/assets/ttml/chain_multiple_styles.xml b/library/src/androidTest/assets/ttml/chain_multiple_styles.xml
new file mode 100644
index 0000000000..d4de3c8ef9
--- /dev/null
+++ b/library/src/androidTest/assets/ttml/chain_multiple_styles.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/library/src/androidTest/assets/ttml/inherit_and_override_style.xml b/library/src/androidTest/assets/ttml/inherit_and_override_style.xml
new file mode 100644
index 0000000000..599e22ef1b
--- /dev/null
+++ b/library/src/androidTest/assets/ttml/inherit_and_override_style.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/library/src/androidTest/assets/ttml/inherit_global_and_parent.xml b/library/src/androidTest/assets/ttml/inherit_global_and_parent.xml
new file mode 100644
index 0000000000..126bfcdba2
--- /dev/null
+++ b/library/src/androidTest/assets/ttml/inherit_global_and_parent.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/library/src/androidTest/assets/ttml/inherit_multiple_styles.xml b/library/src/androidTest/assets/ttml/inherit_multiple_styles.xml
new file mode 100644
index 0000000000..3320db70b2
--- /dev/null
+++ b/library/src/androidTest/assets/ttml/inherit_multiple_styles.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/library/src/androidTest/assets/ttml/inherit_style.xml b/library/src/androidTest/assets/ttml/inherit_style.xml
new file mode 100644
index 0000000000..808087960c
--- /dev/null
+++ b/library/src/androidTest/assets/ttml/inherit_style.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/library/src/androidTest/assets/ttml/inline_style_attributes.xml b/library/src/androidTest/assets/ttml/inline_style_attributes.xml
new file mode 100644
index 0000000000..3d7f4a7d77
--- /dev/null
+++ b/library/src/androidTest/assets/ttml/inline_style_attributes.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
diff --git a/library/src/androidTest/assets/ttml/instance_creation.xml b/library/src/androidTest/assets/ttml/instance_creation.xml
new file mode 100644
index 0000000000..0916002e52
--- /dev/null
+++ b/library/src/androidTest/assets/ttml/instance_creation.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/library/src/androidTest/assets/ttml/no_underline_linethrough.xml b/library/src/androidTest/assets/ttml/no_underline_linethrough.xml
new file mode 100644
index 0000000000..d7092a9415
--- /dev/null
+++ b/library/src/androidTest/assets/ttml/no_underline_linethrough.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/library/src/androidTest/assets/ttml/non_inheritable_properties.xml b/library/src/androidTest/assets/ttml/non_inheritable_properties.xml
new file mode 100644
index 0000000000..b16e63e674
--- /dev/null
+++ b/library/src/androidTest/assets/ttml/non_inheritable_properties.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
diff --git a/library/src/androidTest/java/com/google/android/exoplayer/text/ttml/TtmlParserTest.java b/library/src/androidTest/java/com/google/android/exoplayer/text/ttml/TtmlParserTest.java
new file mode 100644
index 0000000000..6236bda5c0
--- /dev/null
+++ b/library/src/androidTest/java/com/google/android/exoplayer/text/ttml/TtmlParserTest.java
@@ -0,0 +1,353 @@
+/*
+ * Copyright (C) 2014 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 com.google.android.exoplayer.text.ttml;
+
+import android.graphics.Color;
+import android.test.InstrumentationTestCase;
+import android.text.Layout;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Unit test for {@link TtmlParser}.
+ */
+public final class TtmlParserTest extends InstrumentationTestCase {
+
+ private static final String INLINE_ATTRIBUTES_TTML_FILE =
+ "ttml/inline_style_attributes.xml";
+ private static final String INHERIT_STYLE_TTML_FILE =
+ "ttml/inherit_style.xml";
+ private static final String INHERIT_STYLE_OVERRIDE_TTML_FILE =
+ "ttml/inherit_and_override_style.xml";
+ private static final String INHERIT_GLOBAL_AND_PARENT_TTML_FILE =
+ "ttml/inherit_global_and_parent.xml";
+ private static final String NON_INHERTABLE_PROPERTIES_TTML_FILE =
+ "ttml/non_inheritable_properties.xml";
+ private static final String INHERIT_MULTIPLE_STYLES_TTML_FILE =
+ "ttml/inherit_multiple_styles.xml";
+ private static final String CHAIN_MULTIPLE_STYLES_TTML_FILE =
+ "ttml/chain_multiple_styles.xml";
+ private static final String NO_UNDERLINE_LINETHROUGH_TTML_FILE =
+ "ttml/no_underline_linethrough.xml";
+ private static final String INSTANCE_CREATION_TTML_FILE =
+ "ttml/instance_creation.xml";
+
+ public void testInlineAttributes() throws IOException {
+ TtmlSubtitle subtitle = getSubtitle(INLINE_ATTRIBUTES_TTML_FILE);
+ assertEquals(4, subtitle.getEventTimeCount());
+
+ TtmlNode root = subtitle.getRoot();
+
+ TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);
+ TtmlNode firstDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 0);
+ TtmlStyle firstPStyle = queryChildrenForTag(firstDiv, TtmlNode.TAG_P, 0).style;
+ assertEquals(Color.parseColor("yellow"), firstPStyle.getColor());
+ assertEquals(Color.parseColor("blue"), firstPStyle.getBackgroundColor());
+ assertEquals("serif", firstPStyle.getFontFamily());
+ assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, firstPStyle.getStyle());
+ assertTrue(firstPStyle.isUnderline());
+ }
+
+ public void testInheritInlineAttributes() throws IOException {
+
+ TtmlSubtitle subtitle = getSubtitle(INLINE_ATTRIBUTES_TTML_FILE);
+ assertEquals(4, subtitle.getEventTimeCount());
+
+ TtmlNode root = subtitle.getRoot();
+ // inherite inline attributes
+ TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);
+ TtmlNode secondDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 1);
+ TtmlStyle secondPStyle = queryChildrenForTag(secondDiv, TtmlNode.TAG_P, 0).style;
+ assertEquals(Color.parseColor("lime"), secondPStyle.getColor());
+ assertFalse(secondPStyle.hasBackgroundColorSpecified());
+ assertEquals(0, secondPStyle.getBackgroundColor());
+ assertEquals("sansSerif", secondPStyle.getFontFamily());
+ assertEquals(TtmlStyle.STYLE_ITALIC, secondPStyle.getStyle());
+ assertTrue(secondPStyle.isLinethrough());
+ }
+
+ public void testInheritGlobalStyle() throws IOException {
+ TtmlSubtitle subtitle = getSubtitle(INHERIT_STYLE_TTML_FILE);
+ assertEquals(2, subtitle.getEventTimeCount());
+
+ TtmlNode root = subtitle.getRoot();
+
+ TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);
+ TtmlNode firstDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 0);
+ TtmlStyle firstPStyle = queryChildrenForTag(firstDiv, TtmlNode.TAG_P, 0).style;
+ assertEquals(Color.parseColor("yellow"), firstPStyle.getColor());
+ assertEquals(Color.parseColor("blue"), firstPStyle.getBackgroundColor());
+ assertEquals("serif", firstPStyle.getFontFamily());
+ assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, firstPStyle.getStyle());
+ assertTrue(firstPStyle.isUnderline());
+ }
+
+ public void testInheritGlobalStyleOverriddenByInlineAttributes() throws IOException {
+ TtmlSubtitle subtitle = getSubtitle(INHERIT_STYLE_OVERRIDE_TTML_FILE);
+ assertEquals(4, subtitle.getEventTimeCount());
+
+ TtmlNode root = subtitle.getRoot();
+
+ // first pNode inherits global style
+ TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);
+ TtmlNode firstDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 0);
+ TtmlStyle firstPStyle = queryChildrenForTag(firstDiv, TtmlNode.TAG_P, 0).style;
+ assertEquals(Color.parseColor("yellow"), firstPStyle.getColor());
+ assertEquals(Color.parseColor("blue"), firstPStyle.getBackgroundColor());
+ assertEquals("serif", firstPStyle.getFontFamily());
+ assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, firstPStyle.getStyle());
+ assertTrue(firstPStyle.isUnderline());
+
+ // second pNode inherits global style and overrides with attribute
+ TtmlNode secondDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 1);
+ TtmlStyle secondPStyle = queryChildrenForTag(secondDiv, TtmlNode.TAG_P, 0).style;
+ assertEquals(Color.parseColor("yellow"), secondPStyle.getColor());
+ assertEquals(Color.parseColor("red"), secondPStyle.getBackgroundColor());
+ assertEquals("sansSerif", secondPStyle.getFontFamily());
+ assertEquals(TtmlStyle.STYLE_ITALIC, secondPStyle.getStyle());
+ assertTrue(secondPStyle.isUnderline());
+ }
+
+ public void testInheritGlobalAndParent() throws IOException {
+ TtmlSubtitle subtitle = getSubtitle(INHERIT_GLOBAL_AND_PARENT_TTML_FILE);
+ assertEquals(4, subtitle.getEventTimeCount());
+
+ TtmlNode root = subtitle.getRoot();
+
+ // first pNode inherits parent style
+ TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);
+ TtmlNode firstDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 0);
+ TtmlStyle firstPStyle = queryChildrenForTag(firstDiv, TtmlNode.TAG_P, 0).style;
+
+ assertFalse(firstPStyle.hasBackgroundColorSpecified());
+ assertEquals(0, firstPStyle.getBackgroundColor());
+
+ assertEquals(Color.parseColor("lime"), firstPStyle.getColor());
+ assertEquals("sansSerif", firstPStyle.getFontFamily());
+ assertEquals(TtmlStyle.STYLE_NORMAL, firstPStyle.getStyle());
+ assertTrue(firstPStyle.isLinethrough());
+
+ // second pNode inherits parent style and overrides with global style
+ TtmlNode secondDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 1);
+ TtmlStyle secondPStyle = queryChildrenForTag(secondDiv, TtmlNode.TAG_P, 0).style;
+ // attributes overridden by global style
+ assertEquals(Color.parseColor("blue"), secondPStyle.getBackgroundColor());
+ assertEquals(Color.parseColor("yellow"), secondPStyle.getColor());
+ assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, secondPStyle.getStyle());
+ assertEquals("serif", secondPStyle.getFontFamily());
+ assertTrue(secondPStyle.isUnderline());
+ assertEquals(Layout.Alignment.ALIGN_CENTER, secondPStyle.getTextAlign());
+ }
+
+ public void testNonInheritablePropertiesAreNotInherited() throws IOException {
+ TtmlSubtitle subtitle = getSubtitle(NON_INHERTABLE_PROPERTIES_TTML_FILE);
+ assertEquals(2, subtitle.getEventTimeCount());
+
+ TtmlNode root = subtitle.getRoot();
+
+ TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);
+ TtmlNode firstDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 0);
+ TtmlNode firstPStyle = queryChildrenForTag(firstDiv, TtmlNode.TAG_P, 0);
+ TtmlStyle spanStyle = queryChildrenForTag(firstPStyle, TtmlNode.TAG_SPAN, 0).style;
+
+ assertFalse("background color must not be inherited from a context node",
+ spanStyle.hasBackgroundColorSpecified());
+ }
+
+ public void testInheritMultipleStyles() throws IOException {
+ TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);
+ assertEquals(12, subtitle.getEventTimeCount());
+
+ TtmlNode root = subtitle.getRoot();
+
+ TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);
+ TtmlNode firstDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 0);
+ TtmlStyle firstPStyle = queryChildrenForTag(firstDiv, TtmlNode.TAG_P, 0).style;
+
+ assertEquals(Color.parseColor("blue"), firstPStyle.getBackgroundColor());
+ assertEquals(Color.parseColor("yellow"), firstPStyle.getColor());
+ assertEquals("sansSerif", firstPStyle.getFontFamily());
+ assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, firstPStyle.getStyle());
+ assertTrue(firstPStyle.isLinethrough());
+ }
+
+ public void testInheritMultipleStylesWithoutLocalAttributes() throws IOException {
+ TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);
+ assertEquals(12, subtitle.getEventTimeCount());
+
+ TtmlNode root = subtitle.getRoot();
+
+ TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);
+ TtmlNode secondDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 1);
+ TtmlStyle firstPStyle = queryChildrenForTag(secondDiv, TtmlNode.TAG_P, 0).style;
+
+ assertEquals(Color.parseColor("blue"), firstPStyle.getBackgroundColor());
+ assertEquals(Color.parseColor("black"), firstPStyle.getColor());
+ assertEquals("sansSerif", firstPStyle.getFontFamily());
+ assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, firstPStyle.getStyle());
+ assertTrue(firstPStyle.isLinethrough());
+
+ }
+
+ public void testMergeMultipleStylesWithParentStyle() throws IOException {
+ TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);
+ assertEquals(12, subtitle.getEventTimeCount());
+
+ TtmlNode root = subtitle.getRoot();
+
+ TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);
+ TtmlNode thirdDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 2);
+ TtmlStyle firstPStyle = queryChildrenForTag(thirdDiv, TtmlNode.TAG_P, 0).style;
+
+ // inherit from first global style
+ assertEquals(Color.parseColor("red"), firstPStyle.getBackgroundColor());
+ // inherit from second global style
+ assertTrue(firstPStyle.isLinethrough());
+ // inherited from parent node
+ assertEquals("sansSerifInline", firstPStyle.getFontFamily());
+ assertEquals(TtmlStyle.STYLE_ITALIC, firstPStyle.getStyle());
+ assertTrue(firstPStyle.isUnderline());
+ assertEquals(Color.parseColor("yellow"), firstPStyle.getColor());
+ }
+
+ public void testEmptyStyleAttribute() throws IOException {
+ TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);
+ assertEquals(12, subtitle.getEventTimeCount());
+
+ TtmlNode root = subtitle.getRoot();
+ TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);
+ TtmlNode fourthDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 3);
+
+ // no styles specified
+ assertNull(queryChildrenForTag(fourthDiv, TtmlNode.TAG_P, 0).style);
+ }
+
+ public void testNonexistingStyleId() throws IOException {
+ TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);
+ assertEquals(12, subtitle.getEventTimeCount());
+
+ TtmlNode root = subtitle.getRoot();
+ TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);
+ TtmlNode fifthDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 4);
+
+ // no styles specified
+ assertNull(queryChildrenForTag(fifthDiv, TtmlNode.TAG_P, 0).style);
+ }
+
+ public void testNonExistingAndExistingStyleId() throws IOException {
+ TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);
+ assertEquals(12, subtitle.getEventTimeCount());
+
+ TtmlNode root = subtitle.getRoot();
+ TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);
+ TtmlNode sixthDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 5);
+
+ // no styles specified
+ TtmlStyle style = queryChildrenForTag(sixthDiv, TtmlNode.TAG_P, 0).style;
+ assertNotNull(style);
+ assertEquals(Color.RED, style.getBackgroundColor());
+ }
+
+ public void testMultipleChaining() throws IOException {
+ TtmlSubtitle subtitle = getSubtitle(CHAIN_MULTIPLE_STYLES_TTML_FILE);
+ assertEquals(2, subtitle.getEventTimeCount());
+
+ TtmlNode root = subtitle.getRoot();
+ TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);
+ TtmlNode div = queryChildrenForTag(body, TtmlNode.TAG_DIV, 0);
+
+ // no styles specified
+ TtmlStyle style = queryChildrenForTag(div, TtmlNode.TAG_P, 0).style;
+ assertEquals("serif", style.getFontFamily());
+ assertEquals(Color.RED, style.getBackgroundColor());
+ assertEquals(Color.BLACK, style.getColor());
+ assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, style.getStyle());
+ assertTrue(style.isLinethrough());
+
+ }
+
+ public void testNoUnderline() throws IOException {
+ TtmlSubtitle subtitle = getSubtitle(NO_UNDERLINE_LINETHROUGH_TTML_FILE);
+ assertEquals(4, subtitle.getEventTimeCount());
+
+ TtmlNode root = subtitle.getRoot();
+ TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);
+ TtmlNode div = queryChildrenForTag(body, TtmlNode.TAG_DIV, 0);
+
+ TtmlStyle style = queryChildrenForTag(div, TtmlNode.TAG_P, 0).style;
+ assertFalse("noUnderline from inline attribute expected", style.isUnderline());
+ }
+
+ public void testNoLinethrough() throws IOException {
+ TtmlSubtitle subtitle = getSubtitle(NO_UNDERLINE_LINETHROUGH_TTML_FILE);
+ assertEquals(4, subtitle.getEventTimeCount());
+
+ TtmlNode root = subtitle.getRoot();
+ TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);
+ TtmlNode div = queryChildrenForTag(body, TtmlNode.TAG_DIV, 1);
+
+ TtmlStyle style = queryChildrenForTag(div, TtmlNode.TAG_P, 0).style;
+ assertFalse("noLineThrough from inline attribute expected in second pNode",
+ style.isLinethrough());
+ }
+
+
+ public void testOnlySingleInstance() throws IOException {
+ TtmlSubtitle subtitle = getSubtitle(INSTANCE_CREATION_TTML_FILE);
+ assertEquals(4, subtitle.getEventTimeCount());
+
+ TtmlNode root = subtitle.getRoot();
+ TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0);
+ TtmlNode firstDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 0);
+ TtmlNode secondDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 1);
+ TtmlNode thirdDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 2);
+
+ TtmlNode firstP = queryChildrenForTag(firstDiv, TtmlNode.TAG_P, 0);
+ TtmlNode secondP = queryChildrenForTag(secondDiv, TtmlNode.TAG_P, 0);
+ TtmlNode secondSpan = queryChildrenForTag(secondP, TtmlNode.TAG_SPAN, 0);
+ TtmlNode thirdP = queryChildrenForTag(thirdDiv, TtmlNode.TAG_P, 0);
+ TtmlNode thirdSpan = queryChildrenForTag(secondP, TtmlNode.TAG_SPAN, 0);
+
+ // inherit the same instance down the tree if possible
+ assertSame(body.style, firstP.style);
+ assertSame(firstP.style, secondP.style);
+ assertSame(secondP.style, secondSpan.style);
+
+ // if a backgroundColor is involved it does not help
+ assertNotSame(thirdP.style.getInheritableStyle(), thirdSpan.style);
+ }
+
+ private TtmlNode queryChildrenForTag(TtmlNode node, String tag, int pos) {
+ int count = 0;
+ for (int i = 0; i < node.getChildCount(); i++) {
+ if (tag.equals(node.getChild(i).tag)) {
+ if (pos == count++) {
+ return node.getChild(i);
+ }
+ }
+ }
+ return null;
+ }
+
+ private TtmlSubtitle getSubtitle(String file) throws IOException {
+ TtmlParser ttmlParser = new TtmlParser(false);
+ InputStream inputStream = getInstrumentation().getContext()
+ .getResources().getAssets().open(file);
+
+ return (TtmlSubtitle) ttmlParser.parse(inputStream);
+ }
+}
diff --git a/library/src/androidTest/java/com/google/android/exoplayer/text/ttml/TtmlStyleTest.java b/library/src/androidTest/java/com/google/android/exoplayer/text/ttml/TtmlStyleTest.java
new file mode 100644
index 0000000000..c452f68327
--- /dev/null
+++ b/library/src/androidTest/java/com/google/android/exoplayer/text/ttml/TtmlStyleTest.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2014 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 com.google.android.exoplayer.text.ttml;
+
+import android.graphics.Color;
+import android.test.InstrumentationTestCase;
+
+/**
+ * Unit test for {@link TtmlStyle}.
+ */
+public final class TtmlStyleTest extends InstrumentationTestCase {
+
+ private static final String FONT_FAMILY = "serif";
+ private static final String ID = "id";
+ public static final int FOREGROUND_COLOR = Color.WHITE;
+ public static final int BACKGROUND_COLOR = Color.BLACK;
+ private TtmlStyle style;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ style = new TtmlStyle();
+ }
+
+ public void testInheritStyle() {
+ style.inherit(createAncestorStyle());
+ assertNull("id must not be inherited", style.getId());
+ assertTrue(style.isUnderline());
+ assertTrue(style.isLinethrough());
+ assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, style.getStyle());
+ assertEquals(FONT_FAMILY, style.getFontFamily());
+ assertEquals(Color.WHITE, style.getColor());
+ assertFalse("do not inherit backgroundColor", style.hasBackgroundColorSpecified());
+ }
+
+ public void testChainStyle() {
+ style.chain(createAncestorStyle());
+ assertNull("id must not be inherited", style.getId());
+ assertTrue(style.isUnderline());
+ assertTrue(style.isLinethrough());
+ assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, style.getStyle());
+ assertEquals(FONT_FAMILY, style.getFontFamily());
+ assertEquals(FOREGROUND_COLOR, style.getColor());
+ // do inherit backgroundColor when chaining
+ assertEquals("do not inherit backgroundColor when chaining",
+ BACKGROUND_COLOR, style.getBackgroundColor());
+ }
+
+ public void testGetInheritableStyle() {
+ // same instance as long as everything can be inherited
+ assertSame(style, style.getInheritableStyle());
+ style.inherit(createAncestorStyle());
+ assertSame(style, style.getInheritableStyle());
+ // after setting a property which is not inheritable
+ // we expect the inheritable style to be another instance
+ style.setBackgroundColor(0);
+ TtmlStyle inheritableStyle = style.getInheritableStyle();
+ assertNotSame(style, inheritableStyle);
+ // and subsequent call give always the same instance
+ assertSame(inheritableStyle, style.getInheritableStyle());
+
+ boolean exceptionThrown = false;
+ try {
+ // setting properties after calling getInheritableStyle gives an exception
+ style.setItalic(true);
+ } catch (IllegalStateException e) {
+ exceptionThrown = true;
+ }
+ assertTrue(exceptionThrown);
+ }
+
+ private TtmlStyle createAncestorStyle() {
+ TtmlStyle ancestor = new TtmlStyle();
+ ancestor.setId(ID);
+ ancestor.setItalic(true);
+ ancestor.setBold(true);
+ ancestor.setBackgroundColor(BACKGROUND_COLOR);
+ ancestor.setColor(FOREGROUND_COLOR);
+ ancestor.setLinethrough(true);
+ ancestor.setUnderline(true);
+ ancestor.setFontFamily(FONT_FAMILY);
+ return ancestor;
+ }
+
+ public void testStyle() {
+ assertEquals(TtmlStyle.UNSPECIFIED, style.getStyle());
+ style.setItalic(true);
+ assertEquals(TtmlStyle.STYLE_ITALIC, style.getStyle());
+ style.setBold(true);
+ assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, style.getStyle());
+ style.setItalic(false);
+ assertEquals(TtmlStyle.STYLE_BOLD, style.getStyle());
+ style.setBold(false);
+ assertEquals(TtmlStyle.STYLE_NORMAL, style.getStyle());
+ }
+
+ public void testLinethrough() {
+ assertFalse(style.isLinethrough());
+ style.setLinethrough(true);
+ assertTrue(style.isLinethrough());
+ style.setLinethrough(false);
+ assertFalse(style.isLinethrough());
+ }
+
+ public void testUnderline() {
+ assertFalse(style.isUnderline());
+ style.setUnderline(true);
+ assertTrue(style.isUnderline());
+ style.setUnderline(false);
+ assertFalse(style.isUnderline());
+ }
+
+ public void testFontFamily() {
+ assertNull(style.getFontFamily());
+ style.setFontFamily(FONT_FAMILY);
+ assertEquals(FONT_FAMILY, style.getFontFamily());
+ style.setFontFamily(null);
+ assertNull(style.getFontFamily());
+ }
+
+ public void testColor() {
+ assertFalse(style.hasColorSpecified());
+ style.setColor(Color.BLACK);
+ assertEquals(Color.BLACK, style.getColor());
+ assertTrue(style.hasColorSpecified());
+ }
+
+ public void testBackgroundColor() {
+ assertFalse(style.hasBackgroundColorSpecified());
+ style.setBackgroundColor(Color.BLACK);
+ assertEquals(Color.BLACK, style.getBackgroundColor());
+ assertTrue(style.hasBackgroundColorSpecified());
+ }
+
+ public void testId() {
+ assertNull(style.getId());
+ style.setId(ID);
+ assertEquals(ID, style.getId());
+ style.setId(null);
+ assertNull(style.getId());
+ }
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java
index 64c387e7ed..7059b32863 100644
--- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java
+++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java
@@ -25,6 +25,7 @@ import com.google.android.exoplayer.extractor.mp4.PsshAtomUtil;
import com.google.android.exoplayer.upstream.UriLoadable;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.MimeTypes;
+import com.google.android.exoplayer.util.ParserUtil;
import com.google.android.exoplayer.util.UriUtil;
import com.google.android.exoplayer.util.Util;
@@ -120,16 +121,16 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
boolean seenFirstBaseUrl = false;
do {
xpp.next();
- if (isStartTag(xpp, "BaseURL")) {
+ if (ParserUtil.isStartTag(xpp, "BaseURL")) {
if (!seenFirstBaseUrl) {
baseUrl = parseBaseUrl(xpp, baseUrl);
seenFirstBaseUrl = true;
}
- } else if (isStartTag(xpp, "UTCTiming")) {
+ } else if (ParserUtil.isStartTag(xpp, "UTCTiming")) {
utcTiming = parseUtcTiming(xpp);
- } else if (isStartTag(xpp, "Location")) {
+ } else if (ParserUtil.isStartTag(xpp, "Location")) {
location = xpp.nextText();
- } else if (isStartTag(xpp, "Period") && !seenEarlyAccessPeriod) {
+ } else if (ParserUtil.isStartTag(xpp, "Period") && !seenEarlyAccessPeriod) {
Pair periodWithDurationMs = parsePeriod(xpp, baseUrl, nextPeriodStartMs);
Period period = periodWithDurationMs.first;
if (period.startMs == -1) {
@@ -146,7 +147,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
periods.add(period);
}
}
- } while (!isEndTag(xpp, "MPD"));
+ } while (!ParserUtil.isEndTag(xpp, "MPD"));
if (!dynamic && durationMs == -1) {
// The manifest is static and doesn't define a duration. This is unexpected.
@@ -190,21 +191,21 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
boolean seenFirstBaseUrl = false;
do {
xpp.next();
- if (isStartTag(xpp, "BaseURL")) {
+ if (ParserUtil.isStartTag(xpp, "BaseURL")) {
if (!seenFirstBaseUrl) {
baseUrl = parseBaseUrl(xpp, baseUrl);
seenFirstBaseUrl = true;
}
- } else if (isStartTag(xpp, "AdaptationSet")) {
+ } else if (ParserUtil.isStartTag(xpp, "AdaptationSet")) {
adaptationSets.add(parseAdaptationSet(xpp, baseUrl, segmentBase));
- } else if (isStartTag(xpp, "SegmentBase")) {
+ } else if (ParserUtil.isStartTag(xpp, "SegmentBase")) {
segmentBase = parseSegmentBase(xpp, baseUrl, null);
- } else if (isStartTag(xpp, "SegmentList")) {
+ } else if (ParserUtil.isStartTag(xpp, "SegmentList")) {
segmentBase = parseSegmentList(xpp, baseUrl, null);
- } else if (isStartTag(xpp, "SegmentTemplate")) {
+ } else if (ParserUtil.isStartTag(xpp, "SegmentTemplate")) {
segmentBase = parseSegmentTemplate(xpp, baseUrl, null);
}
- } while (!isEndTag(xpp, "Period"));
+ } while (!ParserUtil.isEndTag(xpp, "Period"));
return Pair.create(buildPeriod(id, startMs, adaptationSets), durationMs);
}
@@ -234,35 +235,35 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
boolean seenFirstBaseUrl = false;
do {
xpp.next();
- if (isStartTag(xpp, "BaseURL")) {
+ if (ParserUtil.isStartTag(xpp, "BaseURL")) {
if (!seenFirstBaseUrl) {
baseUrl = parseBaseUrl(xpp, baseUrl);
seenFirstBaseUrl = true;
}
- } else if (isStartTag(xpp, "ContentProtection")) {
+ } else if (ParserUtil.isStartTag(xpp, "ContentProtection")) {
contentProtectionsBuilder.addAdaptationSetProtection(parseContentProtection(xpp));
- } else if (isStartTag(xpp, "ContentComponent")) {
+ } else if (ParserUtil.isStartTag(xpp, "ContentComponent")) {
language = checkLanguageConsistency(language, xpp.getAttributeValue(null, "lang"));
contentType = checkContentTypeConsistency(contentType, parseContentType(xpp));
- } else if (isStartTag(xpp, "Representation")) {
+ } else if (ParserUtil.isStartTag(xpp, "Representation")) {
Representation representation = parseRepresentation(xpp, baseUrl, mimeType, codecs, width,
height, frameRate, audioChannels, audioSamplingRate, language, segmentBase,
contentProtectionsBuilder);
contentProtectionsBuilder.endRepresentation();
contentType = checkContentTypeConsistency(contentType, getContentType(representation));
representations.add(representation);
- } else if (isStartTag(xpp, "AudioChannelConfiguration")) {
+ } else if (ParserUtil.isStartTag(xpp, "AudioChannelConfiguration")) {
audioChannels = parseAudioChannelConfiguration(xpp);
- } else if (isStartTag(xpp, "SegmentBase")) {
+ } else if (ParserUtil.isStartTag(xpp, "SegmentBase")) {
segmentBase = parseSegmentBase(xpp, baseUrl, (SingleSegmentBase) segmentBase);
- } else if (isStartTag(xpp, "SegmentList")) {
+ } else if (ParserUtil.isStartTag(xpp, "SegmentList")) {
segmentBase = parseSegmentList(xpp, baseUrl, (SegmentList) segmentBase);
- } else if (isStartTag(xpp, "SegmentTemplate")) {
+ } else if (ParserUtil.isStartTag(xpp, "SegmentTemplate")) {
segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase);
- } else if (isStartTag(xpp)) {
+ } else if (ParserUtil.isStartTag(xpp)) {
parseAdaptationSetChild(xpp);
}
- } while (!isEndTag(xpp, "AdaptationSet"));
+ } while (!ParserUtil.isEndTag(xpp, "AdaptationSet"));
return buildAdaptationSet(id, contentType, representations, contentProtectionsBuilder.build());
}
@@ -316,14 +317,14 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
do {
xpp.next();
// The cenc:pssh element is defined in 23001-7:2015
- if (isStartTag(xpp, "cenc:pssh") && xpp.next() == XmlPullParser.TEXT) {
+ if (ParserUtil.isStartTag(xpp, "cenc:pssh") && xpp.next() == XmlPullParser.TEXT) {
psshAtom = Base64.decode(xpp.getText(), Base64.DEFAULT);
uuid = PsshAtomUtil.parseUuid(psshAtom);
if (uuid == null) {
throw new ParserException("Invalid pssh atom in cenc:pssh element");
}
}
- } while (!isEndTag(xpp, "ContentProtection"));
+ } while (!ParserUtil.isEndTag(xpp, "ContentProtection"));
return buildContentProtection(schemeIdUri, uuid, psshAtom);
}
@@ -367,23 +368,23 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
boolean seenFirstBaseUrl = false;
do {
xpp.next();
- if (isStartTag(xpp, "BaseURL")) {
+ if (ParserUtil.isStartTag(xpp, "BaseURL")) {
if (!seenFirstBaseUrl) {
baseUrl = parseBaseUrl(xpp, baseUrl);
seenFirstBaseUrl = true;
}
- } else if (isStartTag(xpp, "AudioChannelConfiguration")) {
+ } else if (ParserUtil.isStartTag(xpp, "AudioChannelConfiguration")) {
audioChannels = parseAudioChannelConfiguration(xpp);
- } else if (isStartTag(xpp, "SegmentBase")) {
+ } else if (ParserUtil.isStartTag(xpp, "SegmentBase")) {
segmentBase = parseSegmentBase(xpp, baseUrl, (SingleSegmentBase) segmentBase);
- } else if (isStartTag(xpp, "SegmentList")) {
+ } else if (ParserUtil.isStartTag(xpp, "SegmentList")) {
segmentBase = parseSegmentList(xpp, baseUrl, (SegmentList) segmentBase);
- } else if (isStartTag(xpp, "SegmentTemplate")) {
+ } else if (ParserUtil.isStartTag(xpp, "SegmentTemplate")) {
segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase);
- } else if (isStartTag(xpp, "ContentProtection")) {
+ } else if (ParserUtil.isStartTag(xpp, "ContentProtection")) {
contentProtectionsBuilder.addRepresentationProtection(parseContentProtection(xpp));
}
- } while (!isEndTag(xpp, "Representation"));
+ } while (!ParserUtil.isEndTag(xpp, "Representation"));
Format format = buildFormat(id, mimeType, width, height, frameRate, audioChannels,
audioSamplingRate, bandwidth, language, codecs);
@@ -423,10 +424,10 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
RangedUri initialization = parent != null ? parent.initialization : null;
do {
xpp.next();
- if (isStartTag(xpp, "Initialization")) {
+ if (ParserUtil.isStartTag(xpp, "Initialization")) {
initialization = parseInitialization(xpp, baseUrl);
}
- } while (!isEndTag(xpp, "SegmentBase"));
+ } while (!ParserUtil.isEndTag(xpp, "SegmentBase"));
return buildSingleSegmentBase(initialization, timescale, presentationTimeOffset, baseUrl,
indexStart, indexLength);
@@ -453,17 +454,17 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
do {
xpp.next();
- if (isStartTag(xpp, "Initialization")) {
+ if (ParserUtil.isStartTag(xpp, "Initialization")) {
initialization = parseInitialization(xpp, baseUrl);
- } else if (isStartTag(xpp, "SegmentTimeline")) {
+ } else if (ParserUtil.isStartTag(xpp, "SegmentTimeline")) {
timeline = parseSegmentTimeline(xpp);
- } else if (isStartTag(xpp, "SegmentURL")) {
+ } else if (ParserUtil.isStartTag(xpp, "SegmentURL")) {
if (segments == null) {
segments = new ArrayList<>();
}
segments.add(parseSegmentUrl(xpp, baseUrl));
}
- } while (!isEndTag(xpp, "SegmentList"));
+ } while (!ParserUtil.isEndTag(xpp, "SegmentList"));
if (parent != null) {
initialization = initialization != null ? initialization : parent.initialization;
@@ -500,12 +501,12 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
do {
xpp.next();
- if (isStartTag(xpp, "Initialization")) {
+ if (ParserUtil.isStartTag(xpp, "Initialization")) {
initialization = parseInitialization(xpp, baseUrl);
- } else if (isStartTag(xpp, "SegmentTimeline")) {
+ } else if (ParserUtil.isStartTag(xpp, "SegmentTimeline")) {
timeline = parseSegmentTimeline(xpp);
}
- } while (!isEndTag(xpp, "SegmentTemplate"));
+ } while (!ParserUtil.isEndTag(xpp, "SegmentTemplate"));
if (parent != null) {
initialization = initialization != null ? initialization : parent.initialization;
@@ -530,7 +531,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
long elapsedTime = 0;
do {
xpp.next();
- if (isStartTag(xpp, "S")) {
+ if (ParserUtil.isStartTag(xpp, "S")) {
elapsedTime = parseLong(xpp, "t", elapsedTime);
long duration = parseLong(xpp, "d");
int count = 1 + parseInt(xpp, "r", 0);
@@ -539,7 +540,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
elapsedTime += duration;
}
}
- } while (!isEndTag(xpp, "SegmentTimeline"));
+ } while (!ParserUtil.isEndTag(xpp, "SegmentTimeline"));
return segmentTimeline;
}
@@ -592,7 +593,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
int audioChannels = parseInt(xpp, "value");
do {
xpp.next();
- } while (!isEndTag(xpp, "AudioChannelConfiguration"));
+ } while (!ParserUtil.isEndTag(xpp, "AudioChannelConfiguration"));
return audioChannels;
}
@@ -641,19 +642,6 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
}
}
- protected static boolean isEndTag(XmlPullParser xpp, String name) throws XmlPullParserException {
- return xpp.getEventType() == XmlPullParser.END_TAG && name.equals(xpp.getName());
- }
-
- protected static boolean isStartTag(XmlPullParser xpp, String name)
- throws XmlPullParserException {
- return xpp.getEventType() == XmlPullParser.START_TAG && name.equals(xpp.getName());
- }
-
- protected static boolean isStartTag(XmlPullParser xpp) throws XmlPullParserException {
- return xpp.getEventType() == XmlPullParser.START_TAG;
- }
-
protected static float parseFrameRate(XmlPullParser xpp, float defaultValue) {
float frameRate = defaultValue;
String frameRateAttribute = xpp.getAttributeValue(null, "frameRate");
diff --git a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlNode.java b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlNode.java
index f476dde013..0eb282db5c 100644
--- a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlNode.java
+++ b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlNode.java
@@ -15,7 +15,16 @@
*/
package com.google.android.exoplayer.text.ttml;
+import android.text.Spannable;
import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.AlignmentSpan;
+import android.text.style.BackgroundColorSpan;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StrikethroughSpan;
+import android.text.style.StyleSpan;
+import android.text.style.TypefaceSpan;
+import android.text.style.UnderlineSpan;
import java.util.ArrayList;
import java.util.Iterator;
@@ -28,7 +37,6 @@ import java.util.TreeSet;
/* package */ final class TtmlNode {
public static final long UNDEFINED_TIME = -1;
-
public static final String TAG_TT = "tt";
public static final String TAG_HEAD = "head";
public static final String TAG_BODY = "body";
@@ -45,25 +53,51 @@ import java.util.TreeSet;
public static final String TAG_SMPTE_DATA = "smpte:data";
public static final String TAG_SMPTE_INFORMATION = "smpte:information";
+ public static final String ATTR_ID = "id";
+ 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";
+
public final String tag;
public final String text;
public final boolean isTextNode;
public final long startTimeUs;
public final long endTimeUs;
+ public final TtmlStyle style;
private List children;
- public static TtmlNode buildTextNode(String text) {
- return new TtmlNode(null, applyTextElementSpacePolicy(text), UNDEFINED_TIME, UNDEFINED_TIME);
+ public static TtmlNode buildTextNode(String text, TtmlStyle style) {
+ return new TtmlNode(null, applyTextElementSpacePolicy(text), UNDEFINED_TIME,
+ UNDEFINED_TIME, style);
}
- public static TtmlNode buildNode(String tag, long startTimeUs, long endTimeUs) {
- return new TtmlNode(tag, null, startTimeUs, endTimeUs);
+ public static TtmlNode buildNode(String tag, long startTimeUs, long endTimeUs, TtmlStyle style) {
+ return new TtmlNode(tag, null, startTimeUs, endTimeUs, style);
}
- private TtmlNode(String tag, String text, long startTimeUs, long endTimeUs) {
+ private TtmlNode(String tag, String text, long startTimeUs, long endTimeUs, TtmlStyle style) {
this.tag = tag;
this.text = text;
+ this.style = style;
this.isTextNode = text != null;
this.startTimeUs = startTimeUs;
this.endTimeUs = endTimeUs;
@@ -168,15 +202,18 @@ import java.util.TreeSet;
// 4. Trim a trailing newline, if there is one.
if (builderLength > 0 && builder.charAt(builderLength - 1) == '\n') {
builder.delete(builderLength - 1, builderLength);
- builderLength--;
+ /*builderLength--;*/
}
- return builder.subSequence(0, builderLength);
+
+ return builder;
}
private SpannableStringBuilder getText(long timeUs, SpannableStringBuilder builder,
boolean descendsPNode) {
if (isTextNode && descendsPNode) {
+ int start = builder.length();
builder.append(text);
+ applyStylesToSpan(builder, start, builder.length(), style);
} else if (TAG_BR.equals(tag) && descendsPNode) {
builder.append('\n');
} else if (TAG_METADATA.equals(tag)) {
@@ -193,6 +230,37 @@ import java.util.TreeSet;
return builder;
}
+ private 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.hasColorSpecified()) {
+ builder.setSpan(new ForegroundColorSpan(style.getColor()), start, end,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.hasBackgroundColorSpecified()) {
+ 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);
+ }
+ }
+
/**
* Invoked when the end of a paragraph is encountered. Adds a newline if there are one or more
* non-space characters since the previous newline.
diff --git a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java
index b41c3704b5..6b3f27df21 100644
--- a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java
+++ b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java
@@ -20,7 +20,10 @@ import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.text.Subtitle;
import com.google.android.exoplayer.text.SubtitleParser;
import com.google.android.exoplayer.util.MimeTypes;
+import com.google.android.exoplayer.util.ParserUtil;
+import android.graphics.Color;
+import android.text.Layout;
import android.util.Log;
import org.xmlpull.v1.XmlPullParser;
@@ -29,7 +32,9 @@ import org.xmlpull.v1.XmlPullParserFactory;
import java.io.IOException;
import java.io.InputStream;
+import java.util.HashMap;
import java.util.LinkedList;
+import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -61,6 +66,7 @@ public final class TtmlParser implements SubtitleParser {
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 Pattern CLOCK_TIME =
Pattern.compile("^([0-9][0-9]+):([0-9][0-9]):([0-9][0-9])"
@@ -102,6 +108,7 @@ public final class TtmlParser implements SubtitleParser {
public Subtitle parse(InputStream inputStream) throws IOException {
try {
XmlPullParser xmlParser = xmlParserFactory.newPullParser();
+ Map globalStyles = new HashMap<>();
xmlParser.setInput(inputStream, null);
TtmlSubtitle ttmlSubtitle = null;
LinkedList nodeStack = new LinkedList<>();
@@ -115,9 +122,11 @@ public final class TtmlParser implements SubtitleParser {
if (!isSupportedTag(name)) {
Log.i(TAG, "Ignoring unsupported tag: " + xmlParser.getName());
unsupportedNodeDepth++;
+ } else if (TtmlNode.TAG_HEAD.equals(name)) {
+ parseHeader(xmlParser, globalStyles);
} else {
try {
- TtmlNode node = parseNode(xmlParser, parent);
+ TtmlNode node = parseNode(xmlParser, parent, globalStyles);
nodeStack.addLast(node);
if (parent != null) {
parent.addChild(node);
@@ -133,7 +142,7 @@ public final class TtmlParser implements SubtitleParser {
}
}
} else if (eventType == XmlPullParser.TEXT) {
- parent.addChild(TtmlNode.buildTextNode(xmlParser.getText()));
+ parent.addChild(TtmlNode.buildTextNode(xmlParser.getText(), parent.style));
} else if (eventType == XmlPullParser.END_TAG) {
if (xmlParser.getName().equals(TtmlNode.TAG_TT)) {
ttmlSubtitle = new TtmlSubtitle(nodeStack.getLast());
@@ -156,19 +165,141 @@ public final class TtmlParser implements SubtitleParser {
}
}
+ private Map parseHeader(XmlPullParser xmlParser,
+ Map globalStyles)
+ throws IOException, XmlPullParserException {
+
+ do {
+ xmlParser.next();
+ if (ParserUtil.isStartTag(xmlParser, TtmlNode.TAG_STYLE)) {
+ String parentStyleId = xmlParser.getAttributeValue(null, ATTR_STYLE);
+ TtmlStyle style = parseStyleAttributes(xmlParser, new TtmlStyle());
+ if (parentStyleId != null) {
+ String[] ids = parentStyleId.split(" ");
+ for (int i = 0; i < ids.length; i++) {
+ style.chain(globalStyles.get(ids[i]));
+ }
+ }
+ if (style.getId() != null) {
+ globalStyles.put(style.getId(), style);
+ }
+ }
+ } while (!ParserUtil.isEndTag(xmlParser, TtmlNode.TAG_HEAD));
+ return globalStyles;
+ }
+
+ private TtmlStyle parseStyleAttributes(XmlPullParser parser, TtmlStyle style) {
+ int attributeCount = parser.getAttributeCount();
+ for (int i = 0; i < attributeCount; i++) {
+ String attributeName = parser.getAttributeName(i);
+ String attributeValue = parser.getAttributeValue(i);
+ // TODO: check if it is safe to remove the namespace prefix
+ switch (ParserUtil.removeNamespacePrefix(attributeName)) {
+ 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(Color.parseColor(attributeValue));
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG, "failed parsing background value: '" + attributeValue + "'");
+ }
+ break;
+ case TtmlNode.ATTR_TTS_COLOR:
+ style = createIfNull(style);
+ try {
+ style.setColor(Color.parseColor(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:
+ // TODO: handle size
+ break;
+ case TtmlNode.ATTR_TTS_FONT_WEIGHT:
+ style = createIfNull(style).setBold(
+ TtmlNode.BOLD.equals(attributeValue.toLowerCase()));
+ break;
+ case TtmlNode.ATTR_TTS_FONT_STYLE:
+ style = createIfNull(style).setItalic(
+ TtmlNode.ITALIC.equals(attributeValue.toLowerCase()));
+ break;
+ case TtmlNode.ATTR_TTS_TEXT_ALIGN:
+ switch (attributeValue.toLowerCase()) {
+ 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 (attributeValue.toLowerCase()) {
+ 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;
+ }
+
@Override
public boolean canParse(String mimeType) {
return MimeTypes.APPLICATION_TTML.equals(mimeType);
}
- private TtmlNode parseNode(XmlPullParser parser, TtmlNode parent) throws ParserException {
+ private TtmlNode parseNode(XmlPullParser parser, TtmlNode parent,
+ Map globalStyles) throws ParserException {
long duration = 0;
long startTime = TtmlNode.UNDEFINED_TIME;
long endTime = TtmlNode.UNDEFINED_TIME;
int attributeCount = parser.getAttributeCount();
+ TtmlStyle style = parseStyleAttributes(parser, null);
+ boolean hasInlineStyles = style != null;
+ if (parent != null && parent.style != null) {
+ if (hasInlineStyles) {
+ style.inherit(parent.style);
+ } else {
+ style = parent.style.getInheritableStyle();
+ }
+ }
for (int i = 0; i < attributeCount; i++) {
- // TODO: check if it's safe to ignore the namespace of attributes as follows.
- String attr = parser.getAttributeName(i).replaceFirst("^.*:", "");
+ // TODO: check if it is safe to remove the namespace prefix
+ String attr = ParserUtil.removeNamespacePrefix(parser.getAttributeName(i));
String value = parser.getAttributeValue(i);
if (attr.equals(ATTR_BEGIN)) {
startTime = parseTimeExpression(value,
@@ -179,6 +310,34 @@ public final class TtmlParser implements SubtitleParser {
} else if (attr.equals(ATTR_DURATION)) {
duration = parseTimeExpression(value,
DEFAULT_FRAMERATE, DEFAULT_SUBFRAMERATE, DEFAULT_TICKRATE);
+ } else if (attr.equals(ATTR_STYLE)) {
+ // IDREFS: potentially multiple space delimited ids
+ String[] ids = value.split(" ");
+ if (style == null) {
+ // use global style without overriding
+ if (ids.length == 1) {
+ style = globalStyles.get(value);
+ } else if (ids.length > 1){
+ style = new TtmlStyle();
+ for (int j = 0; j < ids.length; j++) {
+ style.chain(globalStyles.get(ids[j]));
+ }
+ }
+ } else if (hasInlineStyles) {
+ // local attributes inherits from global style
+ for (int j = 0; j < ids.length; j++) {
+ style.chain(globalStyles.get(ids[j]));
+ }
+ } else if (ids.length > 1 || (ids.length == 1 && style != globalStyles.get(ids[0]))) {
+ // merge global style and parent styles
+ TtmlStyle inheritedStyles = style;
+ style = new TtmlStyle();
+ for (int j = 0; j < ids.length; j++) {
+ style.chain(globalStyles.get(ids[j]));
+ }
+ style.inherit(inheritedStyles);
+ }
+
} else {
// Do nothing.
}
@@ -200,7 +359,7 @@ public final class TtmlParser implements SubtitleParser {
endTime = parent.endTimeUs;
}
}
- return TtmlNode.buildNode(parser.getName(), startTime, endTime);
+ return TtmlNode.buildNode(parser.getName(), startTime, endTime, style);
}
private static boolean isSupportedTag(String tag) {
diff --git a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlStyle.java b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlStyle.java
new file mode 100644
index 0000000000..7bb2891d0e
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlStyle.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2015 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 com.google.android.exoplayer.text.ttml;
+
+import com.google.android.exoplayer.util.Assertions;
+
+import android.graphics.Typeface;
+import android.text.Layout;
+
+/**
+ * Style object of a TtmlNode
+ */
+public final class TtmlStyle {
+
+ public static final short UNSPECIFIED = -1;
+
+ public static final short STYLE_NORMAL = Typeface.NORMAL;
+ public static final short STYLE_BOLD = Typeface.BOLD;
+ public static final short STYLE_ITALIC = Typeface.ITALIC;
+ public static final short STYLE_BOLD_ITALIC = Typeface.BOLD_ITALIC;
+
+ private static final short OFF = 0;
+ private static final short ON = 1;
+
+ private String fontFamily;
+ private int color;
+ private boolean colorSpecified;
+ private int backgroundColor;
+ private boolean backgroundColorSpecified;
+ private short linethrough = UNSPECIFIED;
+ private short underline = UNSPECIFIED;
+ private short bold = UNSPECIFIED;
+ private short italic = UNSPECIFIED;
+ private String id;
+ private TtmlStyle inheritableStyle;
+ private Layout.Alignment textAlign;
+
+ /**
+ * Returns the style or UNSPECIFIED when no style information is given.
+ *
+ * @return UNSPECIFIED, STYLE_NORMAL, STYLE_BOLD, STYLE_BOLD or STYLE_BOLD_ITALIC
+ */
+ public short getStyle() {
+ if (bold == UNSPECIFIED && italic == UNSPECIFIED) {
+ return UNSPECIFIED;
+ }
+
+ short style = STYLE_NORMAL;
+ if (bold != UNSPECIFIED) {
+ style += bold;
+ }
+ if (italic != UNSPECIFIED){
+ style += italic;
+ }
+ return style;
+ }
+
+ 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 String getFontFamily() {
+ return fontFamily;
+ }
+
+ public TtmlStyle setFontFamily(String fontFamily) {
+ Assertions.checkState(inheritableStyle == null);
+ this.fontFamily = fontFamily;
+ return this;
+ }
+
+ public int getColor() {
+ return color;
+ }
+
+ public TtmlStyle setColor(int color) {
+ Assertions.checkState(inheritableStyle == null);
+ this.color = color;
+ colorSpecified = true;
+ return this;
+ }
+
+ public boolean hasColorSpecified() {
+ return colorSpecified;
+ }
+
+ public int getBackgroundColor() {
+ return backgroundColor;
+ }
+
+ public TtmlStyle setBackgroundColor(int backgroundColor) {
+ this.backgroundColor = backgroundColor;
+ backgroundColorSpecified = true;
+ return this;
+ }
+
+ public boolean hasBackgroundColorSpecified() {
+ return backgroundColorSpecified;
+ }
+
+ public TtmlStyle setBold(boolean isBold) {
+ Assertions.checkState(inheritableStyle == null);
+ bold = isBold ? STYLE_BOLD : STYLE_NORMAL;
+ return this;
+ }
+
+ public TtmlStyle setItalic(boolean isItalic) {
+ Assertions.checkState(inheritableStyle == null);
+ italic = isItalic ? STYLE_ITALIC : STYLE_NORMAL;
+ return this;
+ }
+
+ public TtmlStyle getInheritableStyle() {
+ if (isFullyInheritable()) {
+ return this;
+ } else if (inheritableStyle == null) {
+ inheritableStyle = new TtmlStyle().inherit(this);
+ }
+ return inheritableStyle;
+ }
+
+ private boolean isFullyInheritable() {
+ return !backgroundColorSpecified;
+ }
+
+ /**
+ * Inherits from an ancestor style. Properties like tts:backgroundColor 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 (!colorSpecified && ancestor.colorSpecified) {
+ setColor(ancestor.color);
+ }
+ 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;
+ }
+ // attributes not inherited as of http://www.w3.org/TR/ttml1/
+ if (chaining && !backgroundColorSpecified && ancestor.backgroundColorSpecified) {
+ 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;
+ }
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlSubtitle.java b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlSubtitle.java
index 4029b0ae24..f29338cd43 100644
--- a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlSubtitle.java
+++ b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlSubtitle.java
@@ -56,6 +56,11 @@ public final class TtmlSubtitle implements Subtitle {
return (eventTimesUs.length == 0 ? -1 : eventTimesUs[eventTimesUs.length - 1]);
}
+ /* @VisibleForTesting */
+ /* package */ TtmlNode getRoot() {
+ return root;
+ }
+
@Override
public List getCues(long timeUs) {
CharSequence cueText = root.getText(timeUs);
diff --git a/library/src/main/java/com/google/android/exoplayer/util/ParserUtil.java b/library/src/main/java/com/google/android/exoplayer/util/ParserUtil.java
new file mode 100644
index 0000000000..32136889a7
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/util/ParserUtil.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2014 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 com.google.android.exoplayer.util;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+/**
+ * Parser utility functions.
+ */
+public final class ParserUtil {
+
+ private ParserUtil() {}
+
+ public static boolean isEndTag(XmlPullParser xpp, String name) throws XmlPullParserException {
+ return xpp.getEventType() == XmlPullParser.END_TAG && name.equals(xpp.getName());
+ }
+
+ public static boolean isStartTag(XmlPullParser xpp, String name)
+ throws XmlPullParserException {
+ return xpp.getEventType() == XmlPullParser.START_TAG && name.equals(xpp.getName());
+ }
+
+ public static boolean isStartTag(XmlPullParser xpp) throws XmlPullParserException {
+ return xpp.getEventType() == XmlPullParser.START_TAG;
+ }
+
+ /**
+ * Removes the namespace part ('^.*:') of the attributeName.
+ *
+ * @param attributeName the string to remove the namespace prefix from
+ * @return the name of the attribute without the prefix
+ */
+ public static String removeNamespacePrefix(String attributeName) {
+ return attributeName.replaceFirst("^.*:", "");
+ }
+}