diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/SubtitleDecoderFactory.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/SubtitleDecoderFactory.java
index 2ebd09a0da..3b545da21f 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/SubtitleDecoderFactory.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/text/SubtitleDecoderFactory.java
@@ -24,7 +24,6 @@ import androidx.media3.extractor.text.SubtitleDecoder;
import androidx.media3.extractor.text.SubtitleParser;
import androidx.media3.extractor.text.cea.Cea608Decoder;
import androidx.media3.extractor.text.cea.Cea708Decoder;
-import androidx.media3.extractor.text.ttml.TtmlDecoder;
import java.util.Objects;
/** A factory for {@link SubtitleDecoder} instances. */
@@ -55,7 +54,6 @@ public interface SubtitleDecoderFactory {
*
Supports formats supported by {@link DefaultSubtitleParserFactory} as well as the following:
*
*
- * - TTML ({@link TtmlDecoder})
*
- Cea608 ({@link Cea608Decoder})
*
- Cea708 ({@link Cea708Decoder})
*
- Exoplayer Cues ({@link ExoplayerCuesDecoder})
@@ -70,7 +68,6 @@ public interface SubtitleDecoderFactory {
public boolean supportsFormat(Format format) {
@Nullable String mimeType = format.sampleMimeType;
return delegate.supportsFormat(format)
- || Objects.equals(mimeType, MimeTypes.APPLICATION_TTML)
|| Objects.equals(mimeType, MimeTypes.APPLICATION_CEA608)
|| Objects.equals(mimeType, MimeTypes.APPLICATION_MP4CEA608)
|| Objects.equals(mimeType, MimeTypes.APPLICATION_CEA708)
@@ -87,8 +84,6 @@ public interface SubtitleDecoderFactory {
@Nullable String mimeType = format.sampleMimeType;
if (mimeType != null) {
switch (mimeType) {
- case MimeTypes.APPLICATION_TTML:
- return new TtmlDecoder();
case MimeTypes.APPLICATION_CEA608:
case MimeTypes.APPLICATION_MP4CEA608:
return new Cea608Decoder(
diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/text/ttml/TtmlDecoderTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/text/DelegatingSubtitleDecoderTtmlParserTest.java
similarity index 88%
rename from libraries/extractor/src/test/java/androidx/media3/extractor/text/ttml/TtmlDecoderTest.java
rename to libraries/exoplayer/src/test/java/androidx/media3/exoplayer/text/DelegatingSubtitleDecoderTtmlParserTest.java
index 41db1c79c8..6e8a87c745 100644
--- a/libraries/extractor/src/test/java/androidx/media3/extractor/text/ttml/TtmlDecoderTest.java
+++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/text/DelegatingSubtitleDecoderTtmlParserTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.media3.extractor.text.ttml;
+package androidx.media3.exoplayer.text;
import static androidx.media3.test.utils.truth.SpannedSubject.assertThat;
import static com.google.common.truth.Truth.assertThat;
@@ -26,7 +26,7 @@ import androidx.media3.common.text.TextEmphasisSpan;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.ColorParser;
import androidx.media3.extractor.text.Subtitle;
-import androidx.media3.extractor.text.SubtitleDecoderException;
+import androidx.media3.extractor.text.ttml.TtmlParser;
import androidx.media3.test.utils.TestUtil;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -35,9 +35,9 @@ import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
-/** Unit test for {@link TtmlDecoder}. */
+/** Unit test for {@link TtmlParser}. */
@RunWith(AndroidJUnit4.class)
-public final class TtmlDecoderTest {
+public final class DelegatingSubtitleDecoderTtmlParserTest {
private static final String INLINE_ATTRIBUTES_TTML_FILE =
"media/ttml/inline_style_attributes.xml";
@@ -71,8 +71,8 @@ public final class TtmlDecoderTest {
private static final String SHEAR_FILE = "media/ttml/shear.xml";
@Test
- public void inlineAttributes() throws IOException, SubtitleDecoderException {
- TtmlSubtitle subtitle = getSubtitle(INLINE_ATTRIBUTES_TTML_FILE);
+ public void inlineAttributes() throws IOException {
+ Subtitle subtitle = getSubtitle(INLINE_ATTRIBUTES_TTML_FILE);
assertThat(subtitle.getEventTimeCount()).isEqualTo(4);
@@ -90,8 +90,8 @@ public final class TtmlDecoderTest {
}
@Test
- public void inheritInlineAttributes() throws IOException, SubtitleDecoderException {
- TtmlSubtitle subtitle = getSubtitle(INLINE_ATTRIBUTES_TTML_FILE);
+ public void inheritInlineAttributes() throws IOException {
+ Subtitle subtitle = getSubtitle(INLINE_ATTRIBUTES_TTML_FILE);
assertThat(subtitle.getEventTimeCount()).isEqualTo(4);
@@ -116,8 +116,8 @@ public final class TtmlDecoderTest {
// KitKat Color:
// https://github.com/android/platform_frameworks_base/blob/kitkat-mr2.2-release/graphics/java/android/graphics/Color.java#L414
@Test
- public void lime() throws IOException, SubtitleDecoderException {
- TtmlSubtitle subtitle = getSubtitle(INLINE_ATTRIBUTES_TTML_FILE);
+ public void lime() throws IOException {
+ Subtitle subtitle = getSubtitle(INLINE_ATTRIBUTES_TTML_FILE);
assertThat(subtitle.getEventTimeCount()).isEqualTo(4);
@@ -131,8 +131,8 @@ public final class TtmlDecoderTest {
}
@Test
- public void inheritGlobalStyle() throws IOException, SubtitleDecoderException {
- TtmlSubtitle subtitle = getSubtitle(INHERIT_STYLE_TTML_FILE);
+ public void inheritGlobalStyle() throws IOException {
+ Subtitle subtitle = getSubtitle(INHERIT_STYLE_TTML_FILE);
assertThat(subtitle.getEventTimeCount()).isEqualTo(2);
@@ -146,9 +146,8 @@ public final class TtmlDecoderTest {
}
@Test
- public void inheritGlobalStyleOverriddenByInlineAttributes()
- throws IOException, SubtitleDecoderException {
- TtmlSubtitle subtitle = getSubtitle(INHERIT_STYLE_OVERRIDE_TTML_FILE);
+ public void inheritGlobalStyleOverriddenByInlineAttributes() throws IOException {
+ Subtitle subtitle = getSubtitle(INHERIT_STYLE_OVERRIDE_TTML_FILE);
assertThat(subtitle.getEventTimeCount()).isEqualTo(4);
@@ -180,8 +179,8 @@ public final class TtmlDecoderTest {
}
@Test
- public void inheritGlobalAndParent() throws IOException, SubtitleDecoderException {
- TtmlSubtitle subtitle = getSubtitle(INHERIT_GLOBAL_AND_PARENT_TTML_FILE);
+ public void inheritGlobalAndParent() throws IOException {
+ Subtitle subtitle = getSubtitle(INHERIT_GLOBAL_AND_PARENT_TTML_FILE);
assertThat(subtitle.getEventTimeCount()).isEqualTo(4);
@@ -213,8 +212,8 @@ public final class TtmlDecoderTest {
}
@Test
- public void inheritMultipleStyles() throws IOException, SubtitleDecoderException {
- TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);
+ public void inheritMultipleStyles() throws IOException {
+ Subtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);
assertThat(subtitle.getEventTimeCount()).isEqualTo(12);
@@ -228,9 +227,8 @@ public final class TtmlDecoderTest {
}
@Test
- public void inheritMultipleStylesWithoutLocalAttributes()
- throws IOException, SubtitleDecoderException {
- TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);
+ public void inheritMultipleStylesWithoutLocalAttributes() throws IOException {
+ Subtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);
assertThat(subtitle.getEventTimeCount()).isEqualTo(12);
@@ -250,8 +248,8 @@ public final class TtmlDecoderTest {
}
@Test
- public void mergeMultipleStylesWithParentStyle() throws IOException, SubtitleDecoderException {
- TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);
+ public void mergeMultipleStylesWithParentStyle() throws IOException {
+ Subtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);
assertThat(subtitle.getEventTimeCount()).isEqualTo(12);
@@ -272,8 +270,8 @@ public final class TtmlDecoderTest {
}
@Test
- public void multipleRegions() throws IOException, SubtitleDecoderException {
- TtmlSubtitle subtitle = getSubtitle(MULTIPLE_REGIONS_TTML_FILE);
+ public void multipleRegions() throws IOException {
+ Subtitle subtitle = getSubtitle(MULTIPLE_REGIONS_TTML_FILE);
List cues = subtitle.getCues(1_000_000);
assertThat(cues).hasSize(2);
@@ -321,8 +319,8 @@ public final class TtmlDecoderTest {
}
@Test
- public void emptyStyleAttribute() throws IOException, SubtitleDecoderException {
- TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);
+ public void emptyStyleAttribute() throws IOException {
+ Subtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);
assertThat(subtitle.getEventTimeCount()).isEqualTo(12);
@@ -333,8 +331,8 @@ public final class TtmlDecoderTest {
}
@Test
- public void nonexistingStyleId() throws IOException, SubtitleDecoderException {
- TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);
+ public void nonexistingStyleId() throws IOException {
+ Subtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);
assertThat(subtitle.getEventTimeCount()).isEqualTo(12);
@@ -345,9 +343,8 @@ public final class TtmlDecoderTest {
}
@Test
- public void nonExistingAndExistingStyleIdWithRedundantSpaces()
- throws IOException, SubtitleDecoderException {
- TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);
+ public void nonExistingAndExistingStyleIdWithRedundantSpaces() throws IOException {
+ Subtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE);
assertThat(subtitle.getEventTimeCount()).isEqualTo(12);
@@ -357,8 +354,8 @@ public final class TtmlDecoderTest {
}
@Test
- public void multipleChaining() throws IOException, SubtitleDecoderException {
- TtmlSubtitle subtitle = getSubtitle(CHAIN_MULTIPLE_STYLES_TTML_FILE);
+ public void multipleChaining() throws IOException {
+ Subtitle subtitle = getSubtitle(CHAIN_MULTIPLE_STYLES_TTML_FILE);
assertThat(subtitle.getEventTimeCount()).isEqualTo(4);
@@ -381,8 +378,8 @@ public final class TtmlDecoderTest {
}
@Test
- public void noUnderline() throws IOException, SubtitleDecoderException {
- TtmlSubtitle subtitle = getSubtitle(NO_UNDERLINE_LINETHROUGH_TTML_FILE);
+ public void noUnderline() throws IOException {
+ Subtitle subtitle = getSubtitle(NO_UNDERLINE_LINETHROUGH_TTML_FILE);
assertThat(subtitle.getEventTimeCount()).isEqualTo(4);
@@ -393,8 +390,8 @@ public final class TtmlDecoderTest {
}
@Test
- public void noLinethrough() throws IOException, SubtitleDecoderException {
- TtmlSubtitle subtitle = getSubtitle(NO_UNDERLINE_LINETHROUGH_TTML_FILE);
+ public void noLinethrough() throws IOException {
+ Subtitle subtitle = getSubtitle(NO_UNDERLINE_LINETHROUGH_TTML_FILE);
assertThat(subtitle.getEventTimeCount()).isEqualTo(4);
@@ -405,8 +402,8 @@ public final class TtmlDecoderTest {
}
@Test
- public void fontSizeSpans() throws IOException, SubtitleDecoderException {
- TtmlSubtitle subtitle = getSubtitle(FONT_SIZE_TTML_FILE);
+ public void fontSizeSpans() throws IOException {
+ Subtitle subtitle = getSubtitle(FONT_SIZE_TTML_FILE);
assertThat(subtitle.getEventTimeCount()).isEqualTo(10);
@@ -432,8 +429,8 @@ public final class TtmlDecoderTest {
}
@Test
- public void fontSizeWithMissingUnitIsIgnored() throws IOException, SubtitleDecoderException {
- TtmlSubtitle subtitle = getSubtitle(FONT_SIZE_MISSING_UNIT_TTML_FILE);
+ public void fontSizeWithMissingUnitIsIgnored() throws IOException {
+ Subtitle subtitle = getSubtitle(FONT_SIZE_MISSING_UNIT_TTML_FILE);
assertThat(subtitle.getEventTimeCount()).isEqualTo(2);
@@ -444,8 +441,8 @@ public final class TtmlDecoderTest {
}
@Test
- public void fontSizeWithInvalidValueIsIgnored() throws IOException, SubtitleDecoderException {
- TtmlSubtitle subtitle = getSubtitle(FONT_SIZE_INVALID_TTML_FILE);
+ public void fontSizeWithInvalidValueIsIgnored() throws IOException {
+ Subtitle subtitle = getSubtitle(FONT_SIZE_INVALID_TTML_FILE);
assertThat(subtitle.getEventTimeCount()).isEqualTo(6);
@@ -466,8 +463,8 @@ public final class TtmlDecoderTest {
}
@Test
- public void fontSizeWithEmptyValueIsIgnored() throws IOException, SubtitleDecoderException {
- TtmlSubtitle subtitle = getSubtitle(FONT_SIZE_EMPTY_TTML_FILE);
+ public void fontSizeWithEmptyValueIsIgnored() throws IOException {
+ Subtitle subtitle = getSubtitle(FONT_SIZE_EMPTY_TTML_FILE);
assertThat(subtitle.getEventTimeCount()).isEqualTo(2);
@@ -478,8 +475,8 @@ public final class TtmlDecoderTest {
}
@Test
- public void frameRate() throws IOException, SubtitleDecoderException {
- TtmlSubtitle subtitle = getSubtitle(FRAME_RATE_TTML_FILE);
+ public void frameRate() throws IOException {
+ Subtitle subtitle = getSubtitle(FRAME_RATE_TTML_FILE);
assertThat(subtitle.getEventTimeCount()).isEqualTo(4);
assertThat(subtitle.getEventTime(0)).isEqualTo(1_000_000);
@@ -489,8 +486,8 @@ public final class TtmlDecoderTest {
}
@Test
- public void bitmapPercentageRegion() throws IOException, SubtitleDecoderException {
- TtmlSubtitle subtitle = getSubtitle(BITMAP_REGION_FILE);
+ public void bitmapPercentageRegion() throws IOException {
+ Subtitle subtitle = getSubtitle(BITMAP_REGION_FILE);
Cue cue = getOnlyCueAtTimeUs(subtitle, 1_000_000);
assertThat(cue.text).isNull();
@@ -518,8 +515,8 @@ public final class TtmlDecoderTest {
}
@Test
- public void bitmapPixelRegion() throws IOException, SubtitleDecoderException {
- TtmlSubtitle subtitle = getSubtitle(BITMAP_PIXEL_REGION_FILE);
+ public void bitmapPixelRegion() throws IOException {
+ Subtitle subtitle = getSubtitle(BITMAP_PIXEL_REGION_FILE);
Cue cue = getOnlyCueAtTimeUs(subtitle, 1_000_000);
assertThat(cue.text).isNull();
@@ -539,8 +536,8 @@ public final class TtmlDecoderTest {
}
@Test
- public void bitmapUnsupportedRegion() throws IOException, SubtitleDecoderException {
- TtmlSubtitle subtitle = getSubtitle(BITMAP_UNSUPPORTED_REGION_FILE);
+ public void bitmapUnsupportedRegion() throws IOException {
+ Subtitle subtitle = getSubtitle(BITMAP_UNSUPPORTED_REGION_FILE);
Cue cue = getOnlyCueAtTimeUs(subtitle, 1_000_000);
assertThat(cue.text).isNull();
@@ -560,8 +557,8 @@ public final class TtmlDecoderTest {
}
@Test
- public void textAlign() throws IOException, SubtitleDecoderException {
- TtmlSubtitle subtitle = getSubtitle(TEXT_ALIGN_FILE);
+ public void textAlign() throws IOException {
+ Subtitle subtitle = getSubtitle(TEXT_ALIGN_FILE);
Cue firstCue = getOnlyCueAtTimeUs(subtitle, 10_000_000);
assertThat(firstCue.text.toString()).isEqualTo("Start alignment");
@@ -601,8 +598,8 @@ public final class TtmlDecoderTest {
}
@Test
- public void multiRowAlign() throws IOException, SubtitleDecoderException {
- TtmlSubtitle subtitle = getSubtitle(MULTI_ROW_ALIGN_FILE);
+ public void multiRowAlign() throws IOException {
+ Subtitle subtitle = getSubtitle(MULTI_ROW_ALIGN_FILE);
Cue firstCue = getOnlyCueAtTimeUs(subtitle, 10_000_000);
assertThat(firstCue.multiRowAlignment).isEqualTo(Layout.Alignment.ALIGN_NORMAL);
@@ -627,8 +624,8 @@ public final class TtmlDecoderTest {
}
@Test
- public void verticalText() throws IOException, SubtitleDecoderException {
- TtmlSubtitle subtitle = getSubtitle(VERTICAL_TEXT_FILE);
+ public void verticalText() throws IOException {
+ Subtitle subtitle = getSubtitle(VERTICAL_TEXT_FILE);
Cue firstCue = getOnlyCueAtTimeUs(subtitle, 10_000_000);
assertThat(firstCue.verticalType).isEqualTo(Cue.VERTICAL_TYPE_RL);
@@ -641,8 +638,8 @@ public final class TtmlDecoderTest {
}
@Test
- public void textCombine() throws IOException, SubtitleDecoderException {
- TtmlSubtitle subtitle = getSubtitle(TEXT_COMBINE_FILE);
+ public void textCombine() throws IOException {
+ Subtitle subtitle = getSubtitle(TEXT_COMBINE_FILE);
Spanned firstCue = getOnlyCueTextAtTimeUs(subtitle, 10_000_000);
assertThat(firstCue)
@@ -659,8 +656,8 @@ public final class TtmlDecoderTest {
}
@Test
- public void rubies() throws IOException, SubtitleDecoderException {
- TtmlSubtitle subtitle = getSubtitle(RUBIES_FILE);
+ public void rubies() throws IOException {
+ Subtitle subtitle = getSubtitle(RUBIES_FILE);
Spanned firstCue = getOnlyCueTextAtTimeUs(subtitle, 10_000_000);
assertThat(firstCue.toString()).isEqualTo("Cue with annotated text.");
@@ -679,7 +676,9 @@ public final class TtmlDecoderTest {
Spanned thirdCue = getOnlyCueTextAtTimeUs(subtitle, 30_000_000);
assertThat(thirdCue.toString()).isEqualTo("Cue with annotated text.");
- assertThat(thirdCue).hasRubySpanBetween("Cue with ".length(), "Cue with annotated".length());
+ assertThat(thirdCue)
+ .hasRubySpanBetween("Cue with ".length(), "Cue with annotated".length())
+ .withTextAndPosition("rubies", TextAnnotation.POSITION_UNKNOWN);
Spanned fourthCue = getOnlyCueTextAtTimeUs(subtitle, 40_000_000);
assertThat(fourthCue.toString()).isEqualTo("Cue with annotated text.");
@@ -707,8 +706,8 @@ public final class TtmlDecoderTest {
}
@Test
- public void textEmphasis() throws IOException, SubtitleDecoderException {
- TtmlSubtitle subtitle = getSubtitle(TEXT_EMPHASIS_FILE);
+ public void textEmphasis() throws IOException {
+ Subtitle subtitle = getSubtitle(TEXT_EMPHASIS_FILE);
Spanned firstCue = getOnlyCueTextAtTimeUs(subtitle, 10_000_000);
assertThat(firstCue)
@@ -847,8 +846,8 @@ public final class TtmlDecoderTest {
}
@Test
- public void shear() throws IOException, SubtitleDecoderException {
- TtmlSubtitle subtitle = getSubtitle(SHEAR_FILE);
+ public void shear() throws IOException {
+ Subtitle subtitle = getSubtitle(SHEAR_FILE);
Cue firstCue = getOnlyCueAtTimeUs(subtitle, 10_000_000);
assertThat(firstCue.shearDegrees).isZero();
@@ -887,10 +886,10 @@ public final class TtmlDecoderTest {
return cues.get(0);
}
- private static TtmlSubtitle getSubtitle(String file)
- throws IOException, SubtitleDecoderException {
- TtmlDecoder ttmlDecoder = new TtmlDecoder();
+ private static Subtitle getSubtitle(String file) throws IOException {
+ DelegatingSubtitleDecoder ttmlDecoder =
+ new DelegatingSubtitleDecoder("TtmlParserDecoder", new TtmlParser());
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), file);
- return (TtmlSubtitle) ttmlDecoder.decode(bytes, bytes.length, false);
+ return ttmlDecoder.decode(bytes, bytes.length, false);
}
}
diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/DefaultSubtitleParserFactory.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/DefaultSubtitleParserFactory.java
index 0c2c80c799..1d3db619eb 100644
--- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/DefaultSubtitleParserFactory.java
+++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/DefaultSubtitleParserFactory.java
@@ -23,6 +23,7 @@ import androidx.media3.extractor.text.dvb.DvbParser;
import androidx.media3.extractor.text.pgs.PgsParser;
import androidx.media3.extractor.text.ssa.SsaParser;
import androidx.media3.extractor.text.subrip.SubripParser;
+import androidx.media3.extractor.text.ttml.TtmlParser;
import androidx.media3.extractor.text.tx3g.Tx3gParser;
import androidx.media3.extractor.text.webvtt.Mp4WebvttParser;
import androidx.media3.extractor.text.webvtt.WebvttParser;
@@ -41,6 +42,7 @@ import java.util.Objects;
*
- TX3G ({@link Tx3gParser})
*
- PGS ({@link PgsParser})
*
- DVB ({@link DvbParser})
+ *
- TTML ({@link TtmlParser})
*
*/
@UnstableApi
@@ -55,7 +57,8 @@ public final class DefaultSubtitleParserFactory implements SubtitleParser.Factor
|| Objects.equals(mimeType, MimeTypes.APPLICATION_SUBRIP)
|| Objects.equals(mimeType, MimeTypes.APPLICATION_TX3G)
|| Objects.equals(mimeType, MimeTypes.APPLICATION_PGS)
- || Objects.equals(mimeType, MimeTypes.APPLICATION_DVBSUBS);
+ || Objects.equals(mimeType, MimeTypes.APPLICATION_DVBSUBS)
+ || Objects.equals(mimeType, MimeTypes.APPLICATION_TTML);
}
@Override
@@ -77,6 +80,8 @@ public final class DefaultSubtitleParserFactory implements SubtitleParser.Factor
return new PgsParser();
case MimeTypes.APPLICATION_DVBSUBS:
return new DvbParser(format.initializationData);
+ case MimeTypes.APPLICATION_TTML:
+ return new TtmlParser();
default:
break;
}
diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/ttml/TtmlDecoder.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/ttml/TtmlParser.java
similarity index 95%
rename from libraries/extractor/src/main/java/androidx/media3/extractor/text/ttml/TtmlDecoder.java
rename to libraries/extractor/src/main/java/androidx/media3/extractor/text/ttml/TtmlParser.java
index f9dea101f1..c01b758d4f 100644
--- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/ttml/TtmlDecoder.java
+++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/ttml/TtmlParser.java
@@ -15,6 +15,8 @@
*/
package androidx.media3.extractor.text.ttml;
+import static androidx.media3.common.util.Assertions.checkArgument;
+import static androidx.media3.common.util.Assertions.checkNotNull;
import static java.lang.Math.max;
import static java.lang.Math.min;
@@ -25,14 +27,19 @@ import androidx.media3.common.text.Cue;
import androidx.media3.common.text.TextAnnotation;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.ColorParser;
+import androidx.media3.common.util.Consumer;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.common.util.XmlPullParserUtil;
+import androidx.media3.extractor.text.CuesWithTiming;
+import androidx.media3.extractor.text.LegacySubtitleUtil;
import androidx.media3.extractor.text.SimpleSubtitleDecoder;
import androidx.media3.extractor.text.Subtitle;
import androidx.media3.extractor.text.SubtitleDecoderException;
+import androidx.media3.extractor.text.SubtitleParser;
import com.google.common.base.Ascii;
+import com.google.common.collect.ImmutableList;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayDeque;
@@ -68,9 +75,9 @@ import org.xmlpull.v1.XmlPullParserFactory;
* See the TTML specification
*/
@UnstableApi
-public final class TtmlDecoder extends SimpleSubtitleDecoder {
+public final class TtmlParser implements SubtitleParser {
- private static final String TAG = "TtmlDecoder";
+ private static final String TAG = "TtmlParser";
private static final String TTP = "http://www.w3.org/ns/ttml#parameter";
@@ -104,8 +111,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
private final XmlPullParserFactory xmlParserFactory;
- public TtmlDecoder() {
- super("TtmlDecoder");
+ public TtmlParser() {
try {
xmlParserFactory = XmlPullParserFactory.newInstance();
xmlParserFactory.setNamespaceAware(true);
@@ -115,15 +121,32 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
}
@Override
- protected Subtitle decode(byte[] data, int length, boolean reset)
- throws SubtitleDecoderException {
+ public ImmutableList parse(byte[] data, int offset, int length) {
+ ImmutableList.Builder cues = ImmutableList.builder();
+ parse(data, offset, length, OutputOptions.allCues(), cues::add);
+ return cues.build();
+ }
+
+ @Override
+ public void parse(
+ byte[] data,
+ int offset,
+ int length,
+ OutputOptions outputOptions,
+ Consumer output) {
+ Subtitle subtitle = parseToLegacySubtitle(data, offset, length);
+ LegacySubtitleUtil.toCuesWithTiming(subtitle, outputOptions, output);
+ }
+
+ @Override
+ public Subtitle parseToLegacySubtitle(byte[] data, int offset, int length) {
try {
XmlPullParser xmlParser = xmlParserFactory.newPullParser();
Map globalStyles = new HashMap<>();
Map regionMap = new HashMap<>();
Map imageMap = new HashMap<>();
regionMap.put(TtmlNode.ANONYMOUS_REGION_ID, new TtmlRegion(TtmlNode.ANONYMOUS_REGION_ID));
- ByteArrayInputStream inputStream = new ByteArrayInputStream(data, 0, length);
+ ByteArrayInputStream inputStream = new ByteArrayInputStream(data, offset, length);
xmlParser.setInput(inputStream, null);
@Nullable TtmlSubtitle ttmlSubtitle = null;
ArrayDeque nodeStack = new ArrayDeque<>();
@@ -180,20 +203,15 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
xmlParser.next();
eventType = xmlParser.getEventType();
}
- if (ttmlSubtitle != null) {
- return ttmlSubtitle;
- } else {
- throw new SubtitleDecoderException("No TTML subtitles found");
- }
+ return checkNotNull(ttmlSubtitle);
} catch (XmlPullParserException xppe) {
- throw new SubtitleDecoderException("Unable to decode source", xppe);
+ throw new IllegalStateException("Unable to decode source", xppe);
} catch (IOException e) {
throw new IllegalStateException("Unexpected error when reading input.", e);
}
}
- private static FrameAndTickRate parseFrameAndTickRates(XmlPullParser xmlParser)
- throws SubtitleDecoderException {
+ private static FrameAndTickRate parseFrameAndTickRates(XmlPullParser xmlParser) {
int frameRate = DEFAULT_FRAME_RATE;
String frameRateString = xmlParser.getAttributeValue(TTP, "frameRate");
if (frameRateString != null) {
@@ -204,9 +222,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
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");
- }
+ checkArgument(parts.length == 2, "frameRateMultiplier doesn't have 2 parts");
float numerator = Integer.parseInt(parts[0]);
float denominator = Integer.parseInt(parts[1]);
frameRateMultiplier = numerator / denominator;
@@ -227,7 +243,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
}
private static CellResolution parseCellResolution(
- XmlPullParser xmlParser, CellResolution defaultValue) throws SubtitleDecoderException {
+ XmlPullParser xmlParser, CellResolution defaultValue) {
String cellResolution = xmlParser.getAttributeValue(TTP, "cellResolution");
if (cellResolution == null) {
return defaultValue;
@@ -241,9 +257,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
try {
int columns = Integer.parseInt(Assertions.checkNotNull(cellResolutionMatcher.group(1)));
int rows = Integer.parseInt(Assertions.checkNotNull(cellResolutionMatcher.group(2)));
- if (columns == 0 || rows == 0) {
- throw new SubtitleDecoderException("Invalid cell resolution " + columns + " " + rows);
- }
+ checkArgument(columns != 0 && rows != 0, "Invalid cell resolution " + columns + " " + rows);
return new CellResolution(columns, rows);
} catch (NumberFormatException e) {
Log.w(TAG, "Ignoring malformed cell resolution: " + cellResolution);
diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/text/ttml/TtmlParserTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/text/ttml/TtmlParserTest.java
new file mode 100644
index 0000000000..14b883756e
--- /dev/null
+++ b/libraries/extractor/src/test/java/androidx/media3/extractor/text/ttml/TtmlParserTest.java
@@ -0,0 +1,920 @@
+/*
+ * 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 androidx.media3.extractor.text.ttml;
+
+import static androidx.media3.test.utils.truth.SpannedSubject.assertThat;
+import static com.google.common.truth.Truth.assertThat;
+
+import android.text.Layout;
+import android.text.Spanned;
+import androidx.media3.common.text.Cue;
+import androidx.media3.common.text.TextAnnotation;
+import androidx.media3.common.text.TextEmphasisSpan;
+import androidx.media3.common.util.Assertions;
+import androidx.media3.common.util.ColorParser;
+import androidx.media3.extractor.text.CuesWithTiming;
+import androidx.media3.test.utils.TestUtil;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit test for {@link TtmlParser}. */
+@RunWith(AndroidJUnit4.class)
+public final class TtmlParserTest {
+
+ private static final String INLINE_ATTRIBUTES_TTML_FILE =
+ "media/ttml/inline_style_attributes.xml";
+ private static final String INHERIT_STYLE_TTML_FILE = "media/ttml/inherit_style.xml";
+ private static final String INHERIT_STYLE_OVERRIDE_TTML_FILE =
+ "media/ttml/inherit_and_override_style.xml";
+ private static final String INHERIT_GLOBAL_AND_PARENT_TTML_FILE =
+ "media/ttml/inherit_global_and_parent.xml";
+ private static final String INHERIT_MULTIPLE_STYLES_TTML_FILE =
+ "media/ttml/inherit_multiple_styles.xml";
+ private static final String CHAIN_MULTIPLE_STYLES_TTML_FILE =
+ "media/ttml/chain_multiple_styles.xml";
+ private static final String MULTIPLE_REGIONS_TTML_FILE = "media/ttml/multiple_regions.xml";
+ private static final String NO_UNDERLINE_LINETHROUGH_TTML_FILE =
+ "media/ttml/no_underline_linethrough.xml";
+ private static final String FONT_SIZE_TTML_FILE = "media/ttml/font_size.xml";
+ private static final String FONT_SIZE_MISSING_UNIT_TTML_FILE = "media/ttml/font_size_no_unit.xml";
+ private static final String FONT_SIZE_INVALID_TTML_FILE = "media/ttml/font_size_invalid.xml";
+ private static final String FONT_SIZE_EMPTY_TTML_FILE = "media/ttml/font_size_empty.xml";
+ private static final String FRAME_RATE_TTML_FILE = "media/ttml/frame_rate.xml";
+ private static final String BITMAP_REGION_FILE = "media/ttml/bitmap_percentage_region.xml";
+ private static final String BITMAP_PIXEL_REGION_FILE = "media/ttml/bitmap_pixel_region.xml";
+ private static final String BITMAP_UNSUPPORTED_REGION_FILE =
+ "media/ttml/bitmap_unsupported_region.xml";
+ private static final String TEXT_ALIGN_FILE = "media/ttml/text_align.xml";
+ private static final String MULTI_ROW_ALIGN_FILE = "media/ttml/multi_row_align.xml";
+ private static final String VERTICAL_TEXT_FILE = "media/ttml/vertical_text.xml";
+ private static final String TEXT_COMBINE_FILE = "media/ttml/text_combine.xml";
+ private static final String RUBIES_FILE = "media/ttml/rubies.xml";
+ private static final String TEXT_EMPHASIS_FILE = "media/ttml/text_emphasis.xml";
+ private static final String SHEAR_FILE = "media/ttml/shear.xml";
+
+ @Test
+ public void inlineAttributes() throws Exception {
+ ImmutableList allCues = getAllCues(INLINE_ATTRIBUTES_TTML_FILE);
+
+ assertThat(allCues).hasSize(2);
+
+ Spanned spanned = getOnlyCueTextAtIndex(allCues, 0);
+ assertThat(spanned.toString()).isEqualTo("text 1");
+ assertThat(spanned).hasTypefaceSpanBetween(0, spanned.length()).withFamily("serif");
+ assertThat(spanned).hasBoldItalicSpanBetween(0, spanned.length());
+ assertThat(spanned).hasUnderlineSpanBetween(0, spanned.length());
+ assertThat(spanned)
+ .hasBackgroundColorSpanBetween(0, spanned.length())
+ .withColor(ColorParser.parseTtmlColor("blue"));
+ assertThat(spanned)
+ .hasForegroundColorSpanBetween(0, spanned.length())
+ .withColor(ColorParser.parseTtmlColor("yellow"));
+ }
+
+ @Test
+ public void inheritInlineAttributes() throws Exception {
+ ImmutableList allCues = getAllCues(INLINE_ATTRIBUTES_TTML_FILE);
+
+ assertThat(allCues).hasSize(2);
+
+ Spanned spanned = getOnlyCueTextAtIndex(allCues, 1);
+ assertThat(spanned.toString()).isEqualTo("text 2");
+ assertThat(spanned).hasTypefaceSpanBetween(0, spanned.length()).withFamily("sansSerif");
+ assertThat(spanned).hasItalicSpanBetween(0, spanned.length());
+ assertThat(spanned).hasStrikethroughSpanBetween(0, spanned.length());
+ assertThat(spanned).hasBackgroundColorSpanBetween(0, spanned.length()).withColor(0xFF00FFFF);
+ assertThat(spanned)
+ .hasForegroundColorSpanBetween(0, spanned.length())
+ .withColor(ColorParser.parseTtmlColor("lime"));
+ }
+
+ /**
+ * Regression test for devices on JellyBean where some named colors are not correctly defined on
+ * framework level. Tests that lime resolves to #FF00FF00
not #00FF00
+ *
.
+ */
+ // JellyBean Color:
+ // https://github.com/android/platform_frameworks_base/blob/jb-mr2-release/graphics/java/android/graphics/Color.java#L414
+ // KitKat Color:
+ // https://github.com/android/platform_frameworks_base/blob/kitkat-mr2.2-release/graphics/java/android/graphics/Color.java#L414
+ @Test
+ public void lime() throws Exception {
+ ImmutableList allCues = getAllCues(INLINE_ATTRIBUTES_TTML_FILE);
+
+ assertThat(allCues).hasSize(2);
+
+ Spanned spanned = getOnlyCueTextAtIndex(allCues, 1);
+ assertThat(spanned.toString()).isEqualTo("text 2");
+ assertThat(spanned).hasTypefaceSpanBetween(0, spanned.length()).withFamily("sansSerif");
+ assertThat(spanned).hasItalicSpanBetween(0, spanned.length());
+ assertThat(spanned).hasStrikethroughSpanBetween(0, spanned.length());
+ assertThat(spanned).hasBackgroundColorSpanBetween(0, spanned.length()).withColor(0xFF00FFFF);
+ assertThat(spanned).hasForegroundColorSpanBetween(0, spanned.length()).withColor(0xFF00FF00);
+ }
+
+ @Test
+ public void inheritGlobalStyle() throws Exception {
+ ImmutableList allCues = getAllCues(INHERIT_STYLE_TTML_FILE);
+
+ assertThat(allCues).hasSize(1);
+
+ Spanned spanned = getOnlyCueTextAtIndex(allCues, 0);
+ assertThat(spanned.toString()).isEqualTo("text 1");
+ assertThat(spanned).hasTypefaceSpanBetween(0, spanned.length()).withFamily("serif");
+ assertThat(spanned).hasBoldItalicSpanBetween(0, spanned.length());
+ assertThat(spanned).hasUnderlineSpanBetween(0, spanned.length());
+ assertThat(spanned).hasBackgroundColorSpanBetween(0, spanned.length()).withColor(0xFF0000FF);
+ assertThat(spanned).hasForegroundColorSpanBetween(0, spanned.length()).withColor(0xFFFFFF00);
+ }
+
+ @Test
+ public void inheritGlobalStyleOverriddenByInlineAttributes() throws Exception {
+ ImmutableList allCues = getAllCues(INHERIT_STYLE_OVERRIDE_TTML_FILE);
+
+ assertThat(allCues).hasSize(2);
+
+ Spanned firstCueText = getOnlyCueTextAtIndex(allCues, 0);
+ assertThat(firstCueText.toString()).isEqualTo("text 1");
+ assertThat(firstCueText).hasTypefaceSpanBetween(0, firstCueText.length()).withFamily("serif");
+ assertThat(firstCueText).hasBoldItalicSpanBetween(0, firstCueText.length());
+ assertThat(firstCueText).hasUnderlineSpanBetween(0, firstCueText.length());
+ assertThat(firstCueText)
+ .hasBackgroundColorSpanBetween(0, firstCueText.length())
+ .withColor(0xFF0000FF);
+ assertThat(firstCueText)
+ .hasForegroundColorSpanBetween(0, firstCueText.length())
+ .withColor(0xFFFFFF00);
+
+ Spanned secondCueText = getOnlyCueTextAtIndex(allCues, 1);
+ assertThat(secondCueText.toString()).isEqualTo("text 2");
+ assertThat(secondCueText)
+ .hasTypefaceSpanBetween(0, secondCueText.length())
+ .withFamily("sansSerif");
+ assertThat(secondCueText).hasItalicSpanBetween(0, secondCueText.length());
+ assertThat(secondCueText).hasUnderlineSpanBetween(0, secondCueText.length());
+ assertThat(secondCueText)
+ .hasBackgroundColorSpanBetween(0, secondCueText.length())
+ .withColor(0xFFFF0000);
+ assertThat(secondCueText)
+ .hasForegroundColorSpanBetween(0, secondCueText.length())
+ .withColor(0xFFFFFF00);
+ }
+
+ @Test
+ public void inheritGlobalAndParent() throws Exception {
+ ImmutableList allCues = getAllCues(INHERIT_GLOBAL_AND_PARENT_TTML_FILE);
+
+ assertThat(allCues).hasSize(2);
+
+ Spanned firstCueText = getOnlyCueTextAtIndex(allCues, 0);
+ assertThat(firstCueText.toString()).isEqualTo("text 1");
+ assertThat(firstCueText)
+ .hasTypefaceSpanBetween(0, firstCueText.length())
+ .withFamily("sansSerif");
+ assertThat(firstCueText).hasStrikethroughSpanBetween(0, firstCueText.length());
+ assertThat(firstCueText)
+ .hasBackgroundColorSpanBetween(0, firstCueText.length())
+ .withColor(0xFFFF0000);
+ assertThat(firstCueText)
+ .hasForegroundColorSpanBetween(0, firstCueText.length())
+ .withColor(ColorParser.parseTtmlColor("lime"));
+
+ Spanned secondCueText = getOnlyCueTextAtIndex(allCues, 1);
+ assertThat(secondCueText.toString()).isEqualTo("text 2");
+ assertThat(secondCueText).hasTypefaceSpanBetween(0, secondCueText.length()).withFamily("serif");
+ assertThat(secondCueText).hasBoldItalicSpanBetween(0, secondCueText.length());
+ assertThat(secondCueText).hasUnderlineSpanBetween(0, secondCueText.length());
+ assertThat(secondCueText).hasStrikethroughSpanBetween(0, secondCueText.length());
+ assertThat(secondCueText)
+ .hasBackgroundColorSpanBetween(0, secondCueText.length())
+ .withColor(0xFF0000FF);
+ assertThat(secondCueText)
+ .hasForegroundColorSpanBetween(0, secondCueText.length())
+ .withColor(0xFFFFFF00);
+ }
+
+ @Test
+ public void inheritMultipleStyles() throws Exception {
+ ImmutableList allCues = getAllCues(INHERIT_MULTIPLE_STYLES_TTML_FILE);
+
+ assertThat(allCues).hasSize(6);
+
+ Spanned spanned = getOnlyCueTextAtIndex(allCues, 0);
+ assertThat(spanned.toString()).isEqualTo("text 1");
+ assertThat(spanned).hasTypefaceSpanBetween(0, spanned.length()).withFamily("sansSerif");
+ assertThat(spanned).hasBoldItalicSpanBetween(0, spanned.length());
+ assertThat(spanned).hasStrikethroughSpanBetween(0, spanned.length());
+ assertThat(spanned).hasBackgroundColorSpanBetween(0, spanned.length()).withColor(0xFF0000FF);
+ assertThat(spanned).hasForegroundColorSpanBetween(0, spanned.length()).withColor(0xFFFFFF00);
+ }
+
+ @Test
+ public void inheritMultipleStylesWithoutLocalAttributes() throws Exception {
+ ImmutableList allCues = getAllCues(INHERIT_MULTIPLE_STYLES_TTML_FILE);
+
+ assertThat(allCues).hasSize(6);
+
+ Spanned secondCueText = getOnlyCueTextAtIndex(allCues, 1);
+ assertThat(secondCueText.toString()).isEqualTo("text 2");
+ assertThat(secondCueText)
+ .hasTypefaceSpanBetween(0, secondCueText.length())
+ .withFamily("sansSerif");
+ assertThat(secondCueText).hasBoldItalicSpanBetween(0, secondCueText.length());
+ assertThat(secondCueText).hasStrikethroughSpanBetween(0, secondCueText.length());
+ assertThat(secondCueText)
+ .hasBackgroundColorSpanBetween(0, secondCueText.length())
+ .withColor(0xFF0000FF);
+ assertThat(secondCueText)
+ .hasForegroundColorSpanBetween(0, secondCueText.length())
+ .withColor(0xFF000000);
+ }
+
+ @Test
+ public void mergeMultipleStylesWithParentStyle() throws Exception {
+ ImmutableList allCues = getAllCues(INHERIT_MULTIPLE_STYLES_TTML_FILE);
+
+ assertThat(allCues).hasSize(6);
+
+ Spanned thirdCueText = getOnlyCueTextAtIndex(allCues, 2);
+ assertThat(thirdCueText.toString()).isEqualTo("text 2.5");
+ assertThat(thirdCueText)
+ .hasTypefaceSpanBetween(0, thirdCueText.length())
+ .withFamily("sansSerifInline");
+ assertThat(thirdCueText).hasItalicSpanBetween(0, thirdCueText.length());
+ assertThat(thirdCueText).hasUnderlineSpanBetween(0, thirdCueText.length());
+ assertThat(thirdCueText).hasStrikethroughSpanBetween(0, thirdCueText.length());
+ assertThat(thirdCueText)
+ .hasBackgroundColorSpanBetween(0, thirdCueText.length())
+ .withColor(0xFFFF0000);
+ assertThat(thirdCueText)
+ .hasForegroundColorSpanBetween(0, thirdCueText.length())
+ .withColor(0xFFFFFF00);
+ }
+
+ @Test
+ public void multipleRegions() throws Exception {
+ ImmutableList allCues = getAllCues(MULTIPLE_REGIONS_TTML_FILE);
+
+ assertThat(allCues).hasSize(6);
+
+ ImmutableList cues = allCues.get(0).cues;
+ assertThat(cues).hasSize(2);
+ Cue cue = cues.get(0);
+ assertThat(cue.text.toString()).isEqualTo("lorem");
+ assertThat(cue.position).isEqualTo(10f / 100f);
+ assertThat(cue.line).isEqualTo(10f / 100f);
+ assertThat(cue.size).isEqualTo(20f / 100f);
+
+ cue = cues.get(1);
+ assertThat(cue.text.toString()).isEqualTo("amet");
+ assertThat(cue.position).isEqualTo(60f / 100f);
+ assertThat(cue.line).isEqualTo(10f / 100f);
+ assertThat(cue.size).isEqualTo(20f / 100f);
+
+ cue = Iterables.getOnlyElement(allCues.get(1).cues);
+ assertThat(cue.text.toString()).isEqualTo("ipsum");
+ assertThat(cue.position).isEqualTo(40f / 100f);
+ assertThat(cue.line).isEqualTo(40f / 100f);
+ assertThat(cue.size).isEqualTo(20f / 100f);
+
+ cue = Iterables.getOnlyElement(allCues.get(2).cues);
+ assertThat(cue.text.toString()).isEqualTo("dolor");
+ assertThat(cue.position).isEqualTo(Cue.DIMEN_UNSET);
+ assertThat(cue.line).isEqualTo(Cue.DIMEN_UNSET);
+ assertThat(cue.size).isEqualTo(Cue.DIMEN_UNSET);
+ // TODO: Should be as below, once https://github.com/google/ExoPlayer/issues/2953 is fixed.
+ // assertEquals(10f / 100f, cue.position);
+ // assertEquals(80f / 100f, cue.line);
+ // assertEquals(1f, cue.size);
+
+ cue = Iterables.getOnlyElement(allCues.get(3).cues);
+ assertThat(cue.text.toString()).isEqualTo("They first said this");
+ assertThat(cue.position).isEqualTo(45f / 100f);
+ assertThat(cue.line).isEqualTo(45f / 100f);
+ assertThat(cue.size).isEqualTo(35f / 100f);
+
+ cue = Iterables.getOnlyElement(allCues.get(4).cues);
+ assertThat(cue.text.toString()).isEqualTo("They first said this\nThen this");
+
+ cue = Iterables.getOnlyElement(allCues.get(5).cues);
+ assertThat(cue.text.toString()).isEqualTo("They first said this\nThen this\nFinally this");
+ assertThat(cue.position).isEqualTo(45f / 100f);
+ assertThat(cue.line).isEqualTo(45f / 100f);
+ }
+
+ @Test
+ public void emptyStyleAttribute() throws Exception {
+ ImmutableList allCues = getAllCues(INHERIT_MULTIPLE_STYLES_TTML_FILE);
+
+ assertThat(allCues).hasSize(6);
+
+ Spanned spanned = getOnlyCueTextAtIndex(allCues, 3);
+
+ assertThat(allCues.get(3).startTimeUs).isEqualTo(40_000_000);
+ assertThat(spanned.toString()).isEqualTo("text 3");
+ assertThat(spanned).hasNoSpans();
+ }
+
+ @Test
+ public void nonexistingStyleId() throws Exception {
+ ImmutableList allCues = getAllCues(INHERIT_MULTIPLE_STYLES_TTML_FILE);
+
+ assertThat(allCues).hasSize(6);
+
+ Spanned spanned = getOnlyCueTextAtIndex(allCues, 4);
+
+ assertThat(allCues.get(4).startTimeUs).isEqualTo(50_000_000);
+ assertThat(spanned.toString()).isEqualTo("text 4");
+ assertThat(spanned).hasNoSpans();
+ }
+
+ @Test
+ public void nonExistingAndExistingStyleIdWithRedundantSpaces() throws Exception {
+ ImmutableList allCues = getAllCues(INHERIT_MULTIPLE_STYLES_TTML_FILE);
+
+ assertThat(allCues).hasSize(6);
+
+ Spanned spanned = getOnlyCueTextAtIndex(allCues, 5);
+
+ assertThat(allCues.get(5).startTimeUs).isEqualTo(60_000_000);
+ assertThat(spanned.toString()).isEqualTo("text 5");
+ assertThat(spanned).hasBackgroundColorSpanBetween(0, spanned.length()).withColor(0xFFFF0000);
+ }
+
+ @Test
+ public void multipleChaining() throws Exception {
+ ImmutableList allCues = getAllCues(CHAIN_MULTIPLE_STYLES_TTML_FILE);
+
+ assertThat(allCues).hasSize(2);
+
+ Spanned spanned1 = getOnlyCueTextAtIndex(allCues, 0);
+ assertThat(spanned1.toString()).isEqualTo("text 1");
+ assertThat(spanned1).hasTypefaceSpanBetween(0, spanned1.length()).withFamily("serif");
+ assertThat(spanned1).hasBackgroundColorSpanBetween(0, spanned1.length()).withColor(0xFFFF0000);
+ assertThat(spanned1).hasForegroundColorSpanBetween(0, spanned1.length()).withColor(0xFF000000);
+ assertThat(spanned1).hasBoldItalicSpanBetween(0, spanned1.length());
+ assertThat(spanned1).hasStrikethroughSpanBetween(0, spanned1.length());
+
+ // only difference: foreground (font) color must be RED
+ Spanned spanned2 = getOnlyCueTextAtIndex(allCues, 1);
+ assertThat(spanned2.toString()).isEqualTo("text 2");
+ assertThat(spanned2).hasTypefaceSpanBetween(0, spanned2.length()).withFamily("serif");
+ assertThat(spanned2).hasBackgroundColorSpanBetween(0, spanned2.length()).withColor(0xFFFF0000);
+ assertThat(spanned2).hasForegroundColorSpanBetween(0, spanned2.length()).withColor(0xFFFF0000);
+ assertThat(spanned2).hasBoldItalicSpanBetween(0, spanned2.length());
+ assertThat(spanned2).hasStrikethroughSpanBetween(0, spanned2.length());
+ }
+
+ @Test
+ public void noUnderline() throws Exception {
+ ImmutableList allCues = getAllCues(NO_UNDERLINE_LINETHROUGH_TTML_FILE);
+
+ assertThat(allCues).hasSize(2);
+
+ Spanned spanned = getOnlyCueTextAtIndex(allCues, 0);
+ assertThat(spanned.toString()).isEqualTo("text 1");
+ // noUnderline from inline attribute overrides s0 global underline style id
+ assertThat(spanned).hasNoUnderlineSpanBetween(0, spanned.length());
+ }
+
+ @Test
+ public void noLinethrough() throws Exception {
+ ImmutableList allCues = getAllCues(NO_UNDERLINE_LINETHROUGH_TTML_FILE);
+
+ assertThat(allCues).hasSize(2);
+
+ Spanned spanned = getOnlyCueTextAtIndex(allCues, 1);
+ assertThat(spanned.toString()).isEqualTo("text 2");
+ // noLineThrough from inline attribute overrides s1 global lineThrough style id
+ assertThat(spanned).hasNoStrikethroughSpanBetween(0, spanned.length());
+ }
+
+ @Test
+ public void fontSizeSpans() throws Exception {
+ ImmutableList allCues = getAllCues(FONT_SIZE_TTML_FILE);
+
+ assertThat(allCues).hasSize(5);
+
+ Spanned spanned = getOnlyCueTextAtIndex(allCues, 0);
+ assertThat(String.valueOf(spanned)).isEqualTo("text 1");
+ assertThat(spanned).hasAbsoluteSizeSpanBetween(0, spanned.length()).withAbsoluteSize(32);
+
+ spanned = getOnlyCueTextAtIndex(allCues, 1);
+ assertThat(spanned.toString()).isEqualTo("text 2");
+ assertThat(spanned).hasRelativeSizeSpanBetween(0, spanned.length()).withSizeChange(2.2f);
+
+ spanned = getOnlyCueTextAtIndex(allCues, 2);
+ assertThat(spanned.toString()).isEqualTo("text 3");
+ assertThat(spanned).hasRelativeSizeSpanBetween(0, spanned.length()).withSizeChange(1.5f);
+
+ spanned = getOnlyCueTextAtIndex(allCues, 3);
+ assertThat(spanned.toString()).isEqualTo("two values");
+ assertThat(spanned).hasAbsoluteSizeSpanBetween(0, spanned.length()).withAbsoluteSize(16);
+
+ spanned = getOnlyCueTextAtIndex(allCues, 4);
+ assertThat(spanned.toString()).isEqualTo("leading dot");
+ assertThat(spanned).hasRelativeSizeSpanBetween(0, spanned.length()).withSizeChange(0.5f);
+ }
+
+ @Test
+ public void fontSizeWithMissingUnitIsIgnored() throws Exception {
+ ImmutableList allCues = getAllCues(FONT_SIZE_MISSING_UNIT_TTML_FILE);
+
+ assertThat(allCues).hasSize(1);
+
+ Spanned spanned = getOnlyCueTextAtIndex(allCues, 0);
+ assertThat(spanned.toString()).isEqualTo("no unit");
+ assertThat(spanned).hasNoRelativeSizeSpanBetween(0, spanned.length());
+ assertThat(spanned).hasNoAbsoluteSizeSpanBetween(0, spanned.length());
+ }
+
+ @Test
+ public void fontSizeWithInvalidValueIsIgnored() throws Exception {
+ ImmutableList allCues = getAllCues(FONT_SIZE_INVALID_TTML_FILE);
+
+ assertThat(allCues).hasSize(3);
+
+ Spanned spanned = getOnlyCueTextAtIndex(allCues, 0);
+ assertThat(String.valueOf(spanned)).isEqualTo("invalid");
+ assertThat(spanned).hasNoRelativeSizeSpanBetween(0, spanned.length());
+ assertThat(spanned).hasNoAbsoluteSizeSpanBetween(0, spanned.length());
+
+ spanned = getOnlyCueTextAtIndex(allCues, 1);
+ assertThat(String.valueOf(spanned)).isEqualTo("invalid");
+ assertThat(spanned).hasNoRelativeSizeSpanBetween(0, spanned.length());
+ assertThat(spanned).hasNoAbsoluteSizeSpanBetween(0, spanned.length());
+
+ spanned = getOnlyCueTextAtIndex(allCues, 2);
+ assertThat(String.valueOf(spanned)).isEqualTo("invalid dot");
+ assertThat(spanned).hasNoRelativeSizeSpanBetween(0, spanned.length());
+ assertThat(spanned).hasNoAbsoluteSizeSpanBetween(0, spanned.length());
+ }
+
+ @Test
+ public void fontSizeWithEmptyValueIsIgnored() throws Exception {
+ ImmutableList allCues = getAllCues(FONT_SIZE_EMPTY_TTML_FILE);
+
+ assertThat(allCues).hasSize(1);
+
+ Spanned spanned = getOnlyCueTextAtIndex(allCues, 0);
+ assertThat(String.valueOf(spanned)).isEqualTo("empty");
+ assertThat(spanned).hasNoRelativeSizeSpanBetween(0, spanned.length());
+ assertThat(spanned).hasNoAbsoluteSizeSpanBetween(0, spanned.length());
+ }
+
+ @Test
+ public void frameRate() throws Exception {
+ ImmutableList allCues = getAllCues(FRAME_RATE_TTML_FILE);
+
+ assertThat(allCues).hasSize(2);
+ assertThat(allCues.get(0).startTimeUs).isEqualTo(1_000_000);
+ assertThat(allCues.get(0).durationUs).isEqualTo(10_000);
+ assertThat((double) allCues.get(1).startTimeUs).isWithin(1000).of(1_001_000_000);
+ assertThat((double) allCues.get(1).durationUs).isWithin(2000).of(1_001_000_000);
+ }
+
+ @Test
+ public void bitmapPercentageRegion() throws Exception {
+ ImmutableList allCues = getAllCues(BITMAP_REGION_FILE);
+
+ assertThat(allCues).hasSize(3);
+
+ assertThat(allCues.get(0).startTimeUs).isEqualTo(200_000);
+ assertThat(allCues.get(0).durationUs).isEqualTo(2_800_000);
+ Cue cue = Iterables.getOnlyElement(allCues.get(0).cues);
+ assertThat(cue.text).isNull();
+ assertThat(cue.bitmap).isNotNull();
+ assertThat(cue.position).isEqualTo(24f / 100f);
+ assertThat(cue.line).isEqualTo(28f / 100f);
+ assertThat(cue.size).isEqualTo(51f / 100f);
+ assertThat(cue.bitmapHeight).isEqualTo(12f / 100f);
+
+ assertThat(allCues.get(1).startTimeUs).isEqualTo(3_200_000);
+ assertThat(allCues.get(1).durationUs).isEqualTo(3_737_000);
+ cue = Iterables.getOnlyElement(allCues.get(1).cues);
+ assertThat(cue.text).isNull();
+ assertThat(cue.bitmap).isNotNull();
+ assertThat(cue.position).isEqualTo(21f / 100f);
+ assertThat(cue.line).isEqualTo(35f / 100f);
+ assertThat(cue.size).isEqualTo(57f / 100f);
+ assertThat(cue.bitmapHeight).isEqualTo(6f / 100f);
+
+ assertThat(allCues.get(2).startTimeUs).isEqualTo(7_200_000);
+ cue = Iterables.getOnlyElement(allCues.get(2).cues);
+ assertThat(cue.text).isNull();
+ assertThat(cue.bitmap).isNotNull();
+ assertThat(cue.position).isEqualTo(24f / 100f);
+ assertThat(cue.line).isEqualTo(28f / 100f);
+ assertThat(cue.size).isEqualTo(51f / 100f);
+ assertThat(cue.bitmapHeight).isEqualTo(12f / 100f);
+ }
+
+ @Test
+ public void bitmapPixelRegion() throws Exception {
+ ImmutableList allCues = getAllCues(BITMAP_PIXEL_REGION_FILE);
+
+ assertThat(allCues).hasSize(2);
+
+ assertThat(allCues.get(0).startTimeUs).isEqualTo(200_000);
+ assertThat(allCues.get(0).durationUs).isEqualTo(2_800_000);
+ Cue cue = Iterables.getOnlyElement(allCues.get(0).cues);
+ assertThat(cue.text).isNull();
+ assertThat(cue.bitmap).isNotNull();
+ assertThat(cue.position).isEqualTo(307f / 1280f);
+ assertThat(cue.line).isEqualTo(562f / 720f);
+ assertThat(cue.size).isEqualTo(653f / 1280f);
+ assertThat(cue.bitmapHeight).isEqualTo(86f / 720f);
+
+ assertThat(allCues.get(1).startTimeUs).isEqualTo(3_200_000);
+ assertThat(allCues.get(1).durationUs).isEqualTo(3_737_000);
+ cue = Iterables.getOnlyElement(allCues.get(1).cues);
+ assertThat(cue.text).isNull();
+ assertThat(cue.bitmap).isNotNull();
+ assertThat(cue.position).isEqualTo(269f / 1280f);
+ assertThat(cue.line).isEqualTo(612f / 720f);
+ assertThat(cue.size).isEqualTo(730f / 1280f);
+ assertThat(cue.bitmapHeight).isEqualTo(43f / 720f);
+ }
+
+ @Test
+ public void bitmapUnsupportedRegion() throws Exception {
+ ImmutableList allCues = getAllCues(BITMAP_UNSUPPORTED_REGION_FILE);
+
+ assertThat(allCues).hasSize(2);
+
+ assertThat(allCues.get(0).startTimeUs).isEqualTo(200_000);
+ assertThat(allCues.get(0).durationUs).isEqualTo(2_800_000);
+ Cue cue = Iterables.getOnlyElement(allCues.get(0).cues);
+ assertThat(cue.text).isNull();
+ assertThat(cue.bitmap).isNotNull();
+ assertThat(cue.position).isEqualTo(Cue.DIMEN_UNSET);
+ assertThat(cue.line).isEqualTo(Cue.DIMEN_UNSET);
+ assertThat(cue.size).isEqualTo(Cue.DIMEN_UNSET);
+ assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
+
+ assertThat(allCues.get(1).startTimeUs).isEqualTo(3_200_000);
+ assertThat(allCues.get(1).durationUs).isEqualTo(3_737_000);
+ cue = Iterables.getOnlyElement(allCues.get(1).cues);
+ assertThat(cue.text).isNull();
+ assertThat(cue.bitmap).isNotNull();
+ assertThat(cue.position).isEqualTo(Cue.DIMEN_UNSET);
+ assertThat(cue.line).isEqualTo(Cue.DIMEN_UNSET);
+ assertThat(cue.size).isEqualTo(Cue.DIMEN_UNSET);
+ assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
+ }
+
+ @Test
+ public void textAlign() throws Exception {
+ ImmutableList allCues = getAllCues(TEXT_ALIGN_FILE);
+
+ assertThat(allCues).hasSize(9);
+
+ Cue firstCue = Iterables.getOnlyElement(allCues.get(0).cues);
+ assertThat(firstCue.text.toString()).isEqualTo("Start alignment");
+ assertThat(firstCue.textAlignment).isEqualTo(Layout.Alignment.ALIGN_NORMAL);
+
+ Cue secondCue = Iterables.getOnlyElement(allCues.get(1).cues);
+ assertThat(secondCue.text.toString()).isEqualTo("Left alignment");
+ assertThat(secondCue.textAlignment).isEqualTo(Layout.Alignment.ALIGN_NORMAL);
+
+ Cue thirdCue = Iterables.getOnlyElement(allCues.get(2).cues);
+ assertThat(thirdCue.text.toString()).isEqualTo("Center alignment");
+ assertThat(thirdCue.textAlignment).isEqualTo(Layout.Alignment.ALIGN_CENTER);
+
+ Cue fourthCue = Iterables.getOnlyElement(allCues.get(3).cues);
+ assertThat(fourthCue.text.toString()).isEqualTo("Right alignment");
+ assertThat(fourthCue.textAlignment).isEqualTo(Layout.Alignment.ALIGN_OPPOSITE);
+
+ Cue fifthCue = Iterables.getOnlyElement(allCues.get(4).cues);
+ assertThat(fifthCue.text.toString()).isEqualTo("End alignment");
+ assertThat(fifthCue.textAlignment).isEqualTo(Layout.Alignment.ALIGN_OPPOSITE);
+
+ Cue sixthCue = Iterables.getOnlyElement(allCues.get(5).cues);
+ assertThat(sixthCue.text.toString()).isEqualTo("Justify alignment (unsupported)");
+ assertThat(sixthCue.textAlignment).isNull();
+
+ Cue seventhCue = Iterables.getOnlyElement(allCues.get(6).cues);
+ assertThat(seventhCue.text.toString()).isEqualTo("No textAlign property");
+ assertThat(seventhCue.textAlignment).isNull();
+
+ Cue eighthCue = Iterables.getOnlyElement(allCues.get(7).cues);
+ assertThat(eighthCue.text.toString()).isEqualTo("Ancestor start alignment");
+ assertThat(eighthCue.textAlignment).isEqualTo(Layout.Alignment.ALIGN_NORMAL);
+
+ Cue ninthCue = Iterables.getOnlyElement(allCues.get(8).cues);
+ assertThat(ninthCue.text.toString()).isEqualTo("Not a P node");
+ assertThat(ninthCue.textAlignment).isNull();
+ }
+
+ @Test
+ public void multiRowAlign() throws Exception {
+ ImmutableList allCues = getAllCues(MULTI_ROW_ALIGN_FILE);
+
+ Cue firstCue = Iterables.getOnlyElement(allCues.get(0).cues);
+ assertThat(firstCue.multiRowAlignment).isEqualTo(Layout.Alignment.ALIGN_NORMAL);
+
+ Cue secondCue = Iterables.getOnlyElement(allCues.get(1).cues);
+ assertThat(secondCue.multiRowAlignment).isEqualTo(Layout.Alignment.ALIGN_CENTER);
+
+ Cue thirdCue = Iterables.getOnlyElement(allCues.get(2).cues);
+ assertThat(thirdCue.multiRowAlignment).isEqualTo(Layout.Alignment.ALIGN_OPPOSITE);
+
+ Cue fourthCue = Iterables.getOnlyElement(allCues.get(3).cues);
+ assertThat(fourthCue.multiRowAlignment).isEqualTo(Layout.Alignment.ALIGN_NORMAL);
+
+ Cue fifthCue = Iterables.getOnlyElement(allCues.get(4).cues);
+ assertThat(fifthCue.multiRowAlignment).isEqualTo(Layout.Alignment.ALIGN_OPPOSITE);
+
+ Cue sixthCue = Iterables.getOnlyElement(allCues.get(5).cues);
+ assertThat(sixthCue.multiRowAlignment).isNull();
+
+ Cue seventhCue = Iterables.getOnlyElement(allCues.get(6).cues);
+ assertThat(seventhCue.multiRowAlignment).isNull();
+ }
+
+ @Test
+ public void verticalText() throws Exception {
+ ImmutableList allCues = getAllCues(VERTICAL_TEXT_FILE);
+
+ Cue firstCue = Iterables.getOnlyElement(allCues.get(0).cues);
+ assertThat(firstCue.verticalType).isEqualTo(Cue.VERTICAL_TYPE_RL);
+
+ Cue secondCue = Iterables.getOnlyElement(allCues.get(1).cues);
+ assertThat(secondCue.verticalType).isEqualTo(Cue.VERTICAL_TYPE_LR);
+
+ Cue thirdCue = Iterables.getOnlyElement(allCues.get(2).cues);
+ assertThat(thirdCue.verticalType).isEqualTo(Cue.TYPE_UNSET);
+ }
+
+ @Test
+ public void textCombine() throws Exception {
+ ImmutableList allCues = getAllCues(TEXT_COMBINE_FILE);
+
+ Spanned firstCue = getOnlyCueTextAtIndex(allCues, 0);
+ assertThat(firstCue)
+ .hasHorizontalTextInVerticalContextSpanBetween(
+ "text with ".length(), "text with combined".length());
+
+ Spanned secondCue = getOnlyCueTextAtIndex(allCues, 1);
+ assertThat(secondCue)
+ .hasNoHorizontalTextInVerticalContextSpanBetween(
+ "text with ".length(), "text with un-combined".length());
+
+ Spanned thirdCue = getOnlyCueTextAtIndex(allCues, 2);
+ assertThat(thirdCue).hasNoHorizontalTextInVerticalContextSpanBetween(0, thirdCue.length());
+ }
+
+ @Test
+ public void rubies() throws Exception {
+ ImmutableList allCues = getAllCues(RUBIES_FILE);
+
+ Spanned firstCue = getOnlyCueTextAtIndex(allCues, 0);
+ assertThat(firstCue.toString()).isEqualTo("Cue with annotated text.");
+ assertThat(firstCue)
+ .hasRubySpanBetween("Cue with ".length(), "Cue with annotated".length())
+ .withTextAndPosition("1st rubies", TextAnnotation.POSITION_BEFORE);
+ assertThat(firstCue)
+ .hasRubySpanBetween("Cue with annotated ".length(), "Cue with annotated text".length())
+ .withTextAndPosition("2nd rubies", TextAnnotation.POSITION_UNKNOWN);
+
+ Spanned secondCue = getOnlyCueTextAtIndex(allCues, 1);
+ assertThat(secondCue.toString()).isEqualTo("Cue with annotated text.");
+ assertThat(secondCue)
+ .hasRubySpanBetween("Cue with ".length(), "Cue with annotated".length())
+ .withTextAndPosition("rubies", TextAnnotation.POSITION_UNKNOWN);
+
+ Spanned thirdCue = getOnlyCueTextAtIndex(allCues, 2);
+ assertThat(thirdCue.toString()).isEqualTo("Cue with annotated text.");
+ assertThat(thirdCue)
+ .hasRubySpanBetween("Cue with ".length(), "Cue with annotated".length())
+ .withTextAndPosition("rubies", TextAnnotation.POSITION_UNKNOWN);
+
+ Spanned fourthCue = getOnlyCueTextAtIndex(allCues, 3);
+ assertThat(fourthCue.toString()).isEqualTo("Cue with annotated text.");
+ assertThat(fourthCue).hasNoRubySpanBetween(0, fourthCue.length());
+
+ Spanned fifthCue = getOnlyCueTextAtIndex(allCues, 4);
+ assertThat(fifthCue.toString()).isEqualTo("Cue with text.");
+ assertThat(fifthCue).hasNoRubySpanBetween(0, fifthCue.length());
+
+ Spanned sixthCue = getOnlyCueTextAtIndex(allCues, 5);
+ assertThat(sixthCue.toString()).isEqualTo("Cue with annotated text.");
+ assertThat(sixthCue).hasNoRubySpanBetween(0, sixthCue.length());
+
+ Spanned seventhCue = getOnlyCueTextAtIndex(allCues, 6);
+ assertThat(seventhCue.toString()).isEqualTo("Cue with annotated text.");
+ assertThat(seventhCue)
+ .hasRubySpanBetween("Cue with ".length(), "Cue with annotated".length())
+ .withTextAndPosition("rubies", TextAnnotation.POSITION_BEFORE);
+
+ Spanned eighthCue = getOnlyCueTextAtIndex(allCues, 7);
+ assertThat(eighthCue.toString()).isEqualTo("Cue with annotated text.");
+ assertThat(eighthCue)
+ .hasRubySpanBetween("Cue with ".length(), "Cue with annotated".length())
+ .withTextAndPosition("rubies", TextAnnotation.POSITION_AFTER);
+ }
+
+ @Test
+ public void textEmphasis() throws Exception {
+ ImmutableList allCues = getAllCues(TEXT_EMPHASIS_FILE);
+
+ Spanned firstCue = getOnlyCueTextAtIndex(allCues, 0);
+ assertThat(firstCue)
+ .hasTextEmphasisSpanBetween("None ".length(), "None おはよ".length())
+ .withMarkAndPosition(
+ TextEmphasisSpan.MARK_SHAPE_NONE,
+ TextEmphasisSpan.MARK_FILL_UNKNOWN,
+ TextAnnotation.POSITION_BEFORE);
+
+ Spanned secondCue = getOnlyCueTextAtIndex(allCues, 1);
+ assertThat(secondCue)
+ .hasTextEmphasisSpanBetween("Auto ".length(), "Auto ございます".length())
+ .withMarkAndPosition(
+ TextEmphasisSpan.MARK_SHAPE_CIRCLE,
+ TextEmphasisSpan.MARK_FILL_FILLED,
+ TextAnnotation.POSITION_BEFORE);
+
+ Spanned thirdCue = getOnlyCueTextAtIndex(allCues, 2);
+ assertThat(thirdCue)
+ .hasTextEmphasisSpanBetween("Filled circle ".length(), "Filled circle こんばんは".length())
+ .withMarkAndPosition(
+ TextEmphasisSpan.MARK_SHAPE_CIRCLE,
+ TextEmphasisSpan.MARK_FILL_FILLED,
+ TextAnnotation.POSITION_BEFORE);
+
+ Spanned fourthCue = getOnlyCueTextAtIndex(allCues, 3);
+ assertThat(fourthCue)
+ .hasTextEmphasisSpanBetween("Filled dot ".length(), "Filled dot ございます".length())
+ .withMarkAndPosition(
+ TextEmphasisSpan.MARK_SHAPE_DOT,
+ TextEmphasisSpan.MARK_FILL_FILLED,
+ TextAnnotation.POSITION_BEFORE);
+
+ Spanned fifthCue = getOnlyCueTextAtIndex(allCues, 4);
+ assertThat(fifthCue)
+ .hasTextEmphasisSpanBetween("Filled sesame ".length(), "Filled sesame おはよ".length())
+ .withMarkAndPosition(
+ TextEmphasisSpan.MARK_SHAPE_SESAME,
+ TextEmphasisSpan.MARK_FILL_FILLED,
+ TextAnnotation.POSITION_BEFORE);
+
+ Spanned sixthCue = getOnlyCueTextAtIndex(allCues, 5);
+ assertThat(sixthCue)
+ .hasTextEmphasisSpanBetween(
+ "Open circle before ".length(), "Open circle before ございます".length())
+ .withMarkAndPosition(
+ TextEmphasisSpan.MARK_SHAPE_CIRCLE,
+ TextEmphasisSpan.MARK_FILL_OPEN,
+ TextAnnotation.POSITION_BEFORE);
+
+ Spanned seventhCue = getOnlyCueTextAtIndex(allCues, 6);
+ assertThat(seventhCue)
+ .hasTextEmphasisSpanBetween("Open dot after ".length(), "Open dot after おはよ".length())
+ .withMarkAndPosition(
+ TextEmphasisSpan.MARK_SHAPE_DOT,
+ TextEmphasisSpan.MARK_FILL_OPEN,
+ TextAnnotation.POSITION_AFTER);
+
+ Spanned eighthCue = getOnlyCueTextAtIndex(allCues, 7);
+ assertThat(eighthCue)
+ .hasTextEmphasisSpanBetween(
+ "Open sesame outside ".length(), "Open sesame outside ございます".length())
+ .withMarkAndPosition(
+ TextEmphasisSpan.MARK_SHAPE_SESAME,
+ TextEmphasisSpan.MARK_FILL_OPEN,
+ TextAnnotation.POSITION_BEFORE);
+
+ Spanned ninthCue = getOnlyCueTextAtIndex(allCues, 8);
+ assertThat(ninthCue)
+ .hasTextEmphasisSpanBetween("Auto outside ".length(), "Auto outside おはよ".length())
+ .withMarkAndPosition(
+ TextEmphasisSpan.MARK_SHAPE_CIRCLE,
+ TextEmphasisSpan.MARK_FILL_FILLED,
+ TextAnnotation.POSITION_BEFORE);
+
+ Spanned tenthCue = getOnlyCueTextAtIndex(allCues, 9);
+ assertThat(tenthCue)
+ .hasTextEmphasisSpanBetween("Circle before ".length(), "Circle before ございます".length())
+ .withMarkAndPosition(
+ TextEmphasisSpan.MARK_SHAPE_CIRCLE,
+ TextEmphasisSpan.MARK_FILL_FILLED,
+ TextAnnotation.POSITION_BEFORE);
+
+ Spanned eleventhCue = getOnlyCueTextAtIndex(allCues, 10);
+ assertThat(eleventhCue)
+ .hasTextEmphasisSpanBetween("Sesame after ".length(), "Sesame after おはよ".length())
+ .withMarkAndPosition(
+ TextEmphasisSpan.MARK_SHAPE_SESAME,
+ TextEmphasisSpan.MARK_FILL_FILLED,
+ TextAnnotation.POSITION_AFTER);
+
+ Spanned twelfthCue = getOnlyCueTextAtIndex(allCues, 11);
+ assertThat(twelfthCue)
+ .hasTextEmphasisSpanBetween("Dot outside ".length(), "Dot outside ございます".length())
+ .withMarkAndPosition(
+ TextEmphasisSpan.MARK_SHAPE_DOT,
+ TextEmphasisSpan.MARK_FILL_FILLED,
+ TextAnnotation.POSITION_BEFORE);
+
+ Spanned thirteenthCue = getOnlyCueTextAtIndex(allCues, 12);
+ assertThat(thirteenthCue)
+ .hasNoTextEmphasisSpanBetween(
+ "No textEmphasis property ".length(), "No textEmphasis property おはよ".length());
+
+ Spanned fourteenthCue = getOnlyCueTextAtIndex(allCues, 13);
+ assertThat(fourteenthCue)
+ .hasTextEmphasisSpanBetween("Auto (TBLR) ".length(), "Auto (TBLR) ございます".length())
+ .withMarkAndPosition(
+ TextEmphasisSpan.MARK_SHAPE_SESAME,
+ TextEmphasisSpan.MARK_FILL_FILLED,
+ TextAnnotation.POSITION_BEFORE);
+
+ Spanned fifteenthCue = getOnlyCueTextAtIndex(allCues, 14);
+ assertThat(fifteenthCue)
+ .hasTextEmphasisSpanBetween("Auto (TBRL) ".length(), "Auto (TBRL) おはよ".length())
+ .withMarkAndPosition(
+ TextEmphasisSpan.MARK_SHAPE_SESAME,
+ TextEmphasisSpan.MARK_FILL_FILLED,
+ TextAnnotation.POSITION_BEFORE);
+
+ Spanned sixteenthCue = getOnlyCueTextAtIndex(allCues, 15);
+ assertThat(sixteenthCue)
+ .hasTextEmphasisSpanBetween("Auto (TB) ".length(), "Auto (TB) ございます".length())
+ .withMarkAndPosition(
+ TextEmphasisSpan.MARK_SHAPE_SESAME,
+ TextEmphasisSpan.MARK_FILL_FILLED,
+ TextAnnotation.POSITION_BEFORE);
+
+ Spanned seventeenthCue = getOnlyCueTextAtIndex(allCues, 16);
+ assertThat(seventeenthCue)
+ .hasTextEmphasisSpanBetween("Auto (LR) ".length(), "Auto (LR) おはよ".length())
+ .withMarkAndPosition(
+ TextEmphasisSpan.MARK_SHAPE_CIRCLE,
+ TextEmphasisSpan.MARK_FILL_FILLED,
+ TextAnnotation.POSITION_BEFORE);
+ }
+
+ @Test
+ public void shear() throws Exception {
+ ImmutableList allCues = getAllCues(SHEAR_FILE);
+
+ Cue firstCue = Iterables.getOnlyElement(allCues.get(0).cues);
+ assertThat(firstCue.shearDegrees).isZero();
+
+ Cue secondCue = Iterables.getOnlyElement(allCues.get(1).cues);
+ assertThat(secondCue.shearDegrees).isWithin(0.01f).of(-15f);
+
+ Cue thirdCue = Iterables.getOnlyElement(allCues.get(2).cues);
+ assertThat(thirdCue.shearDegrees).isWithin(0.01f).of(15f);
+
+ Cue fourthCue = Iterables.getOnlyElement(allCues.get(3).cues);
+ assertThat(fourthCue.shearDegrees).isWithin(0.01f).of(-15f);
+
+ Cue fifthCue = Iterables.getOnlyElement(allCues.get(4).cues);
+ assertThat(fifthCue.shearDegrees).isWithin(0.01f).of(-22.5f);
+
+ Cue sixthCue = Iterables.getOnlyElement(allCues.get(5).cues);
+ assertThat(sixthCue.shearDegrees).isWithin(0.01f).of(0f);
+
+ Cue seventhCue = Iterables.getOnlyElement(allCues.get(6).cues);
+ assertThat(seventhCue.shearDegrees).isWithin(0.01f).of(-90f);
+
+ Cue eighthCue = Iterables.getOnlyElement(allCues.get(7).cues);
+ assertThat(eighthCue.shearDegrees).isWithin(0.01f).of(90f);
+ }
+
+ private static Spanned getOnlyCueTextAtIndex(ImmutableList allCues, int index) {
+ Cue cue = getOnlyCueAtIndex(allCues, index);
+ assertThat(cue.text).isInstanceOf(Spanned.class);
+ return (Spanned) Assertions.checkNotNull(cue.text);
+ }
+
+ private static Cue getOnlyCueAtIndex(ImmutableList allCues, int index) {
+ ImmutableList cues = allCues.get(index).cues;
+ assertThat(cues).hasSize(1);
+ return cues.get(0);
+ }
+
+ private static ImmutableList getAllCues(String file) throws Exception {
+ TtmlParser ttmlParser = new TtmlParser();
+ byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), file);
+ return ttmlParser.parse(bytes, 0, bytes.length);
+ }
+}