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 b600af6e3a..0e1a6dac72 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,7 @@ import androidx.media3.extractor.text.cea.Cea608Decoder;
import androidx.media3.extractor.text.cea.Cea708Decoder;
import androidx.media3.extractor.text.dvb.DvbDecoder;
import androidx.media3.extractor.text.pgs.PgsDecoder;
-import androidx.media3.extractor.text.ssa.SsaDecoder;
+import androidx.media3.extractor.text.ssa.SsaParser;
import androidx.media3.extractor.text.subrip.SubripDecoder;
import androidx.media3.extractor.text.ttml.TtmlDecoder;
import androidx.media3.extractor.text.tx3g.Tx3gDecoder;
@@ -63,7 +63,7 @@ public interface SubtitleDecoderFactory {
*
WebVTT (MP4) ({@link Mp4WebvttDecoder})
* TTML ({@link TtmlDecoder})
* SubRip ({@link SubripDecoder})
- * SSA/ASS ({@link SsaDecoder})
+ * SSA/ASS ({@link SsaParser})
* TX3G ({@link Tx3gDecoder})
* Cea608 ({@link Cea608Decoder})
* Cea708 ({@link Cea708Decoder})
@@ -100,7 +100,9 @@ public interface SubtitleDecoderFactory {
case MimeTypes.TEXT_VTT:
return new WebvttDecoder();
case MimeTypes.TEXT_SSA:
- return new SsaDecoder(format.initializationData);
+ return new DelegatingSubtitleDecoder(
+ "DelegatingSubtitleDecoderWithSsaParser",
+ new SsaParser(format.initializationData));
case MimeTypes.APPLICATION_MP4VTT:
return new Mp4WebvttDecoder();
case MimeTypes.APPLICATION_TTML:
diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/text/ssa/SsaDecoderTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/text/DelegatingSubtitleDecoderWithSsaParserTest.java
similarity index 92%
rename from libraries/extractor/src/test/java/androidx/media3/extractor/text/ssa/SsaDecoderTest.java
rename to libraries/exoplayer/src/test/java/androidx/media3/exoplayer/text/DelegatingSubtitleDecoderWithSsaParserTest.java
index e831a0460c..02a1a87f76 100644
--- a/libraries/extractor/src/test/java/androidx/media3/extractor/text/ssa/SsaDecoderTest.java
+++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/text/DelegatingSubtitleDecoderWithSsaParserTest.java
@@ -1,11 +1,11 @@
/*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright 2023 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
+ * https://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,
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.media3.extractor.text.ssa;
+package androidx.media3.exoplayer.text;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
@@ -23,6 +23,7 @@ import android.text.Layout;
import android.text.Spanned;
import androidx.media3.common.text.Cue;
import androidx.media3.extractor.text.Subtitle;
+import androidx.media3.extractor.text.ssa.SsaParser;
import androidx.media3.test.utils.TestUtil;
import androidx.media3.test.utils.truth.SpannedSubject;
import androidx.test.core.app.ApplicationProvider;
@@ -34,9 +35,9 @@ import java.util.Objects;
import org.junit.Test;
import org.junit.runner.RunWith;
-/** Unit test for {@link SsaDecoder}. */
+/** Unit test for a {@link DelegatingSubtitleDecoder} backed by {@link SsaParser}. */
@RunWith(AndroidJUnit4.class)
-public final class SsaDecoderTest {
+public final class DelegatingSubtitleDecoderWithSsaParserTest {
private static final String EMPTY = "media/ssa/empty";
private static final String EMPTY_STYLE_LINE = "media/ssa/empty_style_line";
@@ -60,7 +61,7 @@ public final class SsaDecoderTest {
@Test
public void decodeEmpty() throws IOException {
- SsaDecoder decoder = new SsaDecoder();
+ DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), EMPTY);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
@@ -69,7 +70,7 @@ public final class SsaDecoderTest {
@Test
public void decodeEmptyStyleLine() throws IOException {
- SsaDecoder decoder = new SsaDecoder();
+ DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
byte[] bytes =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), EMPTY_STYLE_LINE);
Subtitle subtitle = decoder.decode(bytes, bytes.length, /* reset= */ false);
@@ -90,7 +91,7 @@ public final class SsaDecoderTest {
@Test
public void decodeTypical() throws IOException {
- SsaDecoder decoder = new SsaDecoder();
+ DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
@@ -122,7 +123,8 @@ public final class SsaDecoderTest {
ArrayList initializationData = new ArrayList<>();
initializationData.add(formatBytes);
initializationData.add(headerBytes);
- SsaDecoder decoder = new SsaDecoder(initializationData);
+ DelegatingSubtitleDecoder decoder =
+ new DelegatingSubtitleDecoder("SSA", new SsaParser(initializationData));
byte[] bytes =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_DIALOGUE_ONLY);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
@@ -135,7 +137,7 @@ public final class SsaDecoderTest {
@Test
public void decodeTypicalUtf16le() throws IOException {
- SsaDecoder decoder = new SsaDecoder();
+ DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
byte[] bytes =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_UTF16LE);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
@@ -161,7 +163,7 @@ public final class SsaDecoderTest {
@Test
public void decodeTypicalUtf16be() throws IOException {
- SsaDecoder decoder = new SsaDecoder();
+ DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
byte[] bytes =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_UTF16BE);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
@@ -187,7 +189,7 @@ public final class SsaDecoderTest {
@Test
public void decodeOverlappingTimecodes() throws IOException {
- SsaDecoder decoder = new SsaDecoder();
+ DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
byte[] bytes =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), OVERLAPPING_TIMECODES);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
@@ -237,7 +239,7 @@ public final class SsaDecoderTest {
@Test
public void decodePositions() throws IOException {
- SsaDecoder decoder = new SsaDecoder();
+ DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), POSITIONS);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
@@ -290,7 +292,7 @@ public final class SsaDecoderTest {
@Test
public void decodeInvalidPositions() throws IOException {
- SsaDecoder decoder = new SsaDecoder();
+ DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
byte[] bytes =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), INVALID_POSITIONS);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
@@ -326,7 +328,7 @@ public final class SsaDecoderTest {
@Test
public void decodePositionsWithMissingPlayResY() throws IOException {
- SsaDecoder decoder = new SsaDecoder();
+ DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
byte[] bytes =
TestUtil.getByteArray(
ApplicationProvider.getApplicationContext(), POSITIONS_WITHOUT_PLAYRES);
@@ -343,7 +345,7 @@ public final class SsaDecoderTest {
@Test
public void decodeInvalidTimecodes() throws IOException {
// Parsing should succeed, parsing the third cue only.
- SsaDecoder decoder = new SsaDecoder();
+ DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
byte[] bytes =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), INVALID_TIMECODES);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
@@ -354,7 +356,7 @@ public final class SsaDecoderTest {
@Test
public void decodePrimaryColor() throws IOException {
- SsaDecoder decoder = new SsaDecoder();
+ DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
byte[] bytes =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_PRIMARY_COLOR);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
@@ -403,7 +405,7 @@ public final class SsaDecoderTest {
@Test
public void decodeOutlineColor() throws IOException {
- SsaDecoder decoder = new SsaDecoder();
+ DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
byte[] bytes =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_OUTLINE_COLOR);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
@@ -423,7 +425,7 @@ public final class SsaDecoderTest {
@Test
public void decodeFontSize() throws IOException {
- SsaDecoder decoder = new SsaDecoder();
+ DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
byte[] bytes =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_FONT_SIZE);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
@@ -439,7 +441,7 @@ public final class SsaDecoderTest {
@Test
public void decodeBoldItalic() throws IOException {
- SsaDecoder decoder = new SsaDecoder();
+ DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
byte[] bytes =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_BOLD_ITALIC);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
@@ -458,7 +460,7 @@ public final class SsaDecoderTest {
@Test
public void decodeUnderline() throws IOException {
- SsaDecoder decoder = new SsaDecoder();
+ DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
byte[] bytes =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_UNDERLINE);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
@@ -474,7 +476,7 @@ public final class SsaDecoderTest {
@Test
public void decodeStrikeout() throws IOException {
- SsaDecoder decoder = new SsaDecoder();
+ DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
byte[] bytes =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_STRIKEOUT);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/ssa/SsaDialogueFormat.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/ssa/SsaDialogueFormat.java
index 64944e09c3..360ab05d52 100644
--- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/ssa/SsaDialogueFormat.java
+++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/ssa/SsaDialogueFormat.java
@@ -16,7 +16,7 @@
*/
package androidx.media3.extractor.text.ssa;
-import static androidx.media3.extractor.text.ssa.SsaDecoder.FORMAT_LINE_PREFIX;
+import static androidx.media3.extractor.text.ssa.SsaParser.FORMAT_LINE_PREFIX;
import android.text.TextUtils;
import androidx.annotation.Nullable;
diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/ssa/SsaDecoder.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/ssa/SsaParser.java
similarity index 92%
rename from libraries/extractor/src/main/java/androidx/media3/extractor/text/ssa/SsaDecoder.java
rename to libraries/extractor/src/main/java/androidx/media3/extractor/text/ssa/SsaParser.java
index 0067e68f65..371aad6484 100644
--- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/ssa/SsaDecoder.java
+++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/ssa/SsaParser.java
@@ -34,10 +34,11 @@ import androidx.media3.common.util.Log;
import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
-import androidx.media3.extractor.text.SimpleSubtitleDecoder;
-import androidx.media3.extractor.text.Subtitle;
+import androidx.media3.extractor.text.CuesWithTiming;
+import androidx.media3.extractor.text.SubtitleParser;
import com.google.common.base.Ascii;
import com.google.common.base.Charsets;
+import com.google.common.collect.ImmutableList;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.LinkedHashMap;
@@ -47,11 +48,11 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
-/** A {@link SimpleSubtitleDecoder} for SSA/ASS. */
+/** A {@link SubtitleParser} for SSA/ASS. */
@UnstableApi
-public final class SsaDecoder extends SimpleSubtitleDecoder {
+public final class SsaParser implements SubtitleParser {
- private static final String TAG = "SsaDecoder";
+ private static final String TAG = "SsaParser";
private static final Pattern SSA_TIMECODE_PATTERN =
Pattern.compile("(?:(\\d+):)?(\\d+):(\\d+)[:.](\\d+)");
@@ -81,21 +82,22 @@ public final class SsaDecoder extends SimpleSubtitleDecoder {
*/
private float screenHeight;
- public SsaDecoder() {
+ private byte[] dataScratch = Util.EMPTY_BYTE_ARRAY;
+
+ public SsaParser() {
this(/* initializationData= */ null);
}
/**
- * Constructs an SsaDecoder with optional format and header info.
+ * Constructs an instance with optional format and header info.
*
- * @param initializationData Optional initialization data for the decoder. If not null or empty,
+ * @param initializationData Optional initialization data for the parser. If not null or empty,
* the initialization data must consist of two byte arrays. The first must contain an SSA
* format line. The second must contain an SSA header that will be assumed common to all
* samples. The header is everything in an SSA file before the {@code [Events]} section (i.e.
* {@code [Script Info]} and optional {@code [V4+ Styles]} section.
*/
- public SsaDecoder(@Nullable List initializationData) {
- super("SsaDecoder");
+ public SsaParser(@Nullable List initializationData) {
screenWidth = Cue.DIMEN_UNSET;
screenHeight = Cue.DIMEN_UNSET;
@@ -116,18 +118,38 @@ public final class SsaDecoder extends SimpleSubtitleDecoder {
}
@Override
- protected Subtitle decode(byte[] data, int length, boolean reset) {
- List> cues = new ArrayList<>();
- List cueTimesUs = new ArrayList<>();
+ public void reset() {}
- ParsableByteArray parsableData = new ParsableByteArray(data, length);
+ @Nullable
+ @Override
+ public ImmutableList parse(byte[] data, int offset, int length) {
+ List> cues = new ArrayList<>();
+ List startTimesUs = new ArrayList<>();
+
+ if (dataScratch.length < length) {
+ dataScratch = new byte[length];
+ }
+ System.arraycopy(
+ /* src= */ data, /* scrPos= */ offset, /* dest= */ dataScratch, /* destPos= */ 0, length);
+ ParsableByteArray parsableData = new ParsableByteArray(dataScratch, length);
Charset charset = detectUtfCharset(parsableData);
if (!haveInitializationData) {
parseHeader(parsableData, charset);
}
- parseEventBody(parsableData, cues, cueTimesUs, charset);
- return new SsaSubtitle(cues, cueTimesUs);
+ parseEventBody(parsableData, cues, startTimesUs, charset);
+
+ ImmutableList.Builder cuesWithStartTimeAndDuration = ImmutableList.builder();
+ for (int i = 0; i < cues.size(); i++) {
+ List cuesForThisStartTime = cues.get(i);
+ long startTimeUs = startTimesUs.get(i);
+ // The duration of the last CuesWithTiming is C.TIME_UNSET by design
+ long durationUs =
+ i == cues.size() - 1 ? C.TIME_UNSET : startTimesUs.get(i + 1) - startTimesUs.get(i);
+ cuesWithStartTimeAndDuration.add(
+ new CuesWithTiming(cuesForThisStartTime, startTimeUs, durationUs));
+ }
+ return cuesWithStartTimeAndDuration.build();
}
/**
diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/ssa/SsaStyle.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/ssa/SsaStyle.java
index e8b27a75ee..88fcd2a735 100644
--- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/ssa/SsaStyle.java
+++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/ssa/SsaStyle.java
@@ -17,7 +17,7 @@
package androidx.media3.extractor.text.ssa;
import static androidx.media3.common.util.Assertions.checkArgument;
-import static androidx.media3.extractor.text.ssa.SsaDecoder.STYLE_LINE_PREFIX;
+import static androidx.media3.extractor.text.ssa.SsaParser.STYLE_LINE_PREFIX;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.SOURCE;
@@ -371,7 +371,7 @@ import java.util.regex.Pattern;
int strikeoutIndex = C.INDEX_UNSET;
int borderStyleIndex = C.INDEX_UNSET;
String[] keys =
- TextUtils.split(styleFormatLine.substring(SsaDecoder.FORMAT_LINE_PREFIX.length()), ",");
+ TextUtils.split(styleFormatLine.substring(SsaParser.FORMAT_LINE_PREFIX.length()), ",");
for (int i = 0; i < keys.length; i++) {
switch (Ascii.toLowerCase(keys[i].trim())) {
case "name":
diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/ssa/SsaSubtitle.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/ssa/SsaSubtitle.java
deleted file mode 100644
index caaefc2088..0000000000
--- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/ssa/SsaSubtitle.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (C) 2017 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.ssa;
-
-import androidx.media3.common.C;
-import androidx.media3.common.text.Cue;
-import androidx.media3.common.util.Assertions;
-import androidx.media3.common.util.Util;
-import androidx.media3.extractor.text.Subtitle;
-import java.util.Collections;
-import java.util.List;
-
-/** A representation of an SSA/ASS subtitle. */
-/* package */ final class SsaSubtitle implements Subtitle {
-
- private final List> cues;
- private final List cueTimesUs;
-
- /**
- * @param cues The cues in the subtitle.
- * @param cueTimesUs The cue times, in microseconds.
- */
- public SsaSubtitle(List> cues, List cueTimesUs) {
- this.cues = cues;
- this.cueTimesUs = cueTimesUs;
- }
-
- @Override
- public int getNextEventTimeIndex(long timeUs) {
- int index = Util.binarySearchCeil(cueTimesUs, timeUs, false, false);
- return index < cueTimesUs.size() ? index : C.INDEX_UNSET;
- }
-
- @Override
- public int getEventTimeCount() {
- return cueTimesUs.size();
- }
-
- @Override
- public long getEventTime(int index) {
- Assertions.checkArgument(index >= 0);
- Assertions.checkArgument(index < cueTimesUs.size());
- return cueTimesUs.get(index);
- }
-
- @Override
- public List getCues(long timeUs) {
- int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false);
- if (index == -1) {
- // timeUs is earlier than the start of the first cue.
- return Collections.emptyList();
- } else {
- return cues.get(index);
- }
- }
-}
diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/text/ssa/SsaParserTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/text/ssa/SsaParserTest.java
new file mode 100644
index 0000000000..0d83c8fa6f
--- /dev/null
+++ b/libraries/extractor/src/test/java/androidx/media3/extractor/text/ssa/SsaParserTest.java
@@ -0,0 +1,544 @@
+/*
+ * 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.ssa;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.graphics.Color;
+import android.text.Layout;
+import android.text.Spanned;
+import androidx.media3.common.C;
+import androidx.media3.common.text.Cue;
+import androidx.media3.extractor.text.CuesWithTiming;
+import androidx.media3.test.utils.TestUtil;
+import androidx.media3.test.utils.truth.SpannedSubject;
+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 java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit test {@link SsaParser}. */
+@RunWith(AndroidJUnit4.class)
+public final class SsaParserTest {
+
+ private static final String EMPTY = "media/ssa/empty";
+ private static final String EMPTY_STYLE_LINE = "media/ssa/empty_style_line";
+ private static final String TYPICAL = "media/ssa/typical";
+ private static final String TYPICAL_HEADER_ONLY = "media/ssa/typical_header";
+ private static final String TYPICAL_DIALOGUE_ONLY = "media/ssa/typical_dialogue";
+ private static final String TYPICAL_FORMAT_ONLY = "media/ssa/typical_format";
+ private static final String TYPICAL_UTF16LE = "media/ssa/typical_utf16le";
+ private static final String TYPICAL_UTF16BE = "media/ssa/typical_utf16be";
+ private static final String OVERLAPPING_TIMECODES = "media/ssa/overlapping_timecodes";
+ private static final String POSITIONS = "media/ssa/positioning";
+ private static final String INVALID_TIMECODES = "media/ssa/invalid_timecodes";
+ private static final String INVALID_POSITIONS = "media/ssa/invalid_positioning";
+ private static final String POSITIONS_WITHOUT_PLAYRES = "media/ssa/positioning_without_playres";
+ private static final String STYLE_PRIMARY_COLOR = "media/ssa/style_primary_color";
+ private static final String STYLE_OUTLINE_COLOR = "media/ssa/style_outline_color";
+ private static final String STYLE_FONT_SIZE = "media/ssa/style_font_size";
+ private static final String STYLE_BOLD_ITALIC = "media/ssa/style_bold_italic";
+ private static final String STYLE_UNDERLINE = "media/ssa/style_underline";
+ private static final String STYLE_STRIKEOUT = "media/ssa/style_strikeout";
+
+ @Test
+ public void parseEmpty() throws IOException {
+ SsaParser parser = new SsaParser();
+ byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), EMPTY);
+ List allCues = parser.parse(bytes);
+
+ assertThat(allCues).isEmpty();
+ }
+
+ @Test
+ public void parseEmptyStyleLine() throws IOException {
+ SsaParser parser = new SsaParser();
+ byte[] bytes =
+ TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), EMPTY_STYLE_LINE);
+ List allCues = parser.parse(bytes);
+
+ assertThat(allCues).hasSize(2);
+ Cue cue = Iterables.getOnlyElement(allCues.get(0).cues);
+ SpannedSubject.assertThat((Spanned) cue.text).hasNoSpans();
+ assertThat(cue.textSize).isEqualTo(Cue.DIMEN_UNSET);
+ assertThat(cue.textSizeType).isEqualTo(Cue.TYPE_UNSET);
+ assertThat(cue.textAlignment).isNull();
+ assertThat(cue.positionAnchor).isEqualTo(Cue.TYPE_UNSET);
+ assertThat(cue.position).isEqualTo(Cue.DIMEN_UNSET);
+ assertThat(cue.size).isEqualTo(Cue.DIMEN_UNSET);
+ assertThat(cue.lineAnchor).isEqualTo(Cue.TYPE_UNSET);
+ assertThat(cue.line).isEqualTo(Cue.DIMEN_UNSET);
+ assertThat(cue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION);
+ }
+
+ @Test
+ public void parseTypical() throws IOException {
+ SsaParser parser = new SsaParser();
+ byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL);
+ List allCues = parser.parse(bytes);
+
+ assertThat(allCues).hasSize(6);
+ // Check position, line, anchors & alignment are set from Alignment Style (2 - bottom-center).
+ Cue firstCue = allCues.get(0).cues.get(0);
+ assertWithMessage("Cue.textAlignment")
+ .that(firstCue.textAlignment)
+ .isEqualTo(Layout.Alignment.ALIGN_CENTER);
+ assertWithMessage("Cue.positionAnchor")
+ .that(firstCue.positionAnchor)
+ .isEqualTo(Cue.ANCHOR_TYPE_MIDDLE);
+ assertThat(firstCue.position).isEqualTo(0.5f);
+ assertThat(firstCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END);
+ assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION);
+ assertThat(firstCue.line).isEqualTo(0.95f);
+
+ assertTypicalCue1(allCues.get(0));
+ assertTypicalCue2(allCues.get(2));
+ assertTypicalCue3(allCues.get(4));
+ }
+
+ @Test
+ public void parseTypicalWithInitializationData() throws IOException {
+ byte[] headerBytes =
+ TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_HEADER_ONLY);
+ byte[] formatBytes =
+ TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_FORMAT_ONLY);
+ ArrayList initializationData = new ArrayList<>();
+ initializationData.add(formatBytes);
+ initializationData.add(headerBytes);
+ SsaParser parser = new SsaParser(initializationData);
+ byte[] bytes =
+ TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_DIALOGUE_ONLY);
+ List allCues = parser.parse(bytes);
+
+ assertThat(allCues).hasSize(6);
+ assertTypicalCue1(allCues.get(0));
+ assertTypicalCue2(allCues.get(2));
+ assertTypicalCue3(allCues.get(4));
+ }
+
+ @Test
+ public void parseTypicalWithInitializationDataAtOffsetIntoDialogueAndRestrictedLength()
+ throws IOException {
+ byte[] headerBytes =
+ TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_HEADER_ONLY);
+ byte[] formatBytes =
+ TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_FORMAT_ONLY);
+ ArrayList initializationData = new ArrayList<>();
+ initializationData.add(formatBytes);
+ initializationData.add(headerBytes);
+ SsaParser parser = new SsaParser(initializationData);
+ byte[] bytes =
+ TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_DIALOGUE_ONLY);
+ ImmutableList allCues =
+ parser.parse(bytes, /* offset= */ 10, /* length= */ bytes.length - 30);
+
+ assertThat(allCues).hasSize(4);
+ // Because of the offset, we skip the first line of dialogue
+ assertTypicalCue2(allCues.get(0));
+ // Because of the length restriction, we only partially parse the third line of dialogue
+ assertThat(allCues.get(2).startTimeUs).isEqualTo(4560000);
+ assertThat(allCues.get(2).durationUs).isEqualTo(8900000 - 4560000);
+ assertThat(allCues.get(2).cues.get(0).text.toString()).isEqualTo("This is the third subt");
+ }
+
+ @Test
+ public void parseTypicalUtf16le() throws IOException {
+ SsaParser parser = new SsaParser();
+ byte[] bytes =
+ TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_UTF16LE);
+ List allCues = parser.parse(bytes);
+
+ assertThat(allCues).hasSize(6);
+ // Check position, line, anchors & alignment are set from Alignment Style (2 - bottom-center).
+ Cue firstCue = allCues.get(0).cues.get(0);
+ assertWithMessage("Cue.textAlignment")
+ .that(firstCue.textAlignment)
+ .isEqualTo(Layout.Alignment.ALIGN_CENTER);
+ assertWithMessage("Cue.positionAnchor")
+ .that(firstCue.positionAnchor)
+ .isEqualTo(Cue.ANCHOR_TYPE_MIDDLE);
+ assertThat(firstCue.position).isEqualTo(0.5f);
+ assertThat(firstCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END);
+ assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION);
+ assertThat(firstCue.line).isEqualTo(0.95f);
+
+ assertTypicalCue1(allCues.get(0));
+ assertTypicalCue2(allCues.get(2));
+ assertTypicalCue3(allCues.get(4));
+ }
+
+ @Test
+ public void parseTypicalUtf16be() throws IOException {
+ SsaParser parser = new SsaParser();
+ byte[] bytes =
+ TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_UTF16BE);
+ List allCues = parser.parse(bytes);
+
+ assertThat(allCues).hasSize(6);
+ // Check position, line, anchors & alignment are set from Alignment Style (2 - bottom-center).
+ Cue firstCue = allCues.get(0).cues.get(0);
+ assertWithMessage("Cue.textAlignment")
+ .that(firstCue.textAlignment)
+ .isEqualTo(Layout.Alignment.ALIGN_CENTER);
+ assertWithMessage("Cue.positionAnchor")
+ .that(firstCue.positionAnchor)
+ .isEqualTo(Cue.ANCHOR_TYPE_MIDDLE);
+ assertThat(firstCue.position).isEqualTo(0.5f);
+ assertThat(firstCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END);
+ assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION);
+ assertThat(firstCue.line).isEqualTo(0.95f);
+
+ assertTypicalCue1(allCues.get(0));
+ assertTypicalCue2(allCues.get(2));
+ assertTypicalCue3(allCues.get(4));
+ }
+
+ @Test
+ public void parseOverlappingTimecodes() throws IOException {
+ SsaParser parser = new SsaParser();
+ byte[] bytes =
+ TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), OVERLAPPING_TIMECODES);
+ List allCues = parser.parse(bytes);
+
+ String firstSubtitleText = "First subtitle - end overlaps second";
+ String secondSubtitleText = "Second subtitle - beginning overlaps first";
+ String thirdSubtitleText = "Third subtitle - out of order";
+ String fourthSubtitleText = "Fourth subtitle - same timings as fifth";
+ String fifthSubtitleText = "Fifth subtitle - same timings as fourth";
+ String sixthSubtitleText = "Sixth subtitle - fully encompasses seventh";
+ String seventhSubtitleText = "Seventh subtitle - nested fully inside sixth";
+
+ assertThat(allCues).hasSize(11);
+ assertThat(allCues.get(0).startTimeUs).isEqualTo(1_000_000);
+ assertThat(allCues.get(0).durationUs).isEqualTo(1_000_000);
+ assertThat(Iterables.transform(allCues.get(0).cues, cue -> cue.text.toString()))
+ .containsExactly(firstSubtitleText);
+
+ assertThat(allCues.get(1).startTimeUs).isEqualTo(2_000_000);
+ assertThat(allCues.get(1).durationUs).isEqualTo(2_230_000);
+ assertThat(Iterables.transform(allCues.get(1).cues, cue -> cue.text.toString()))
+ .containsExactly(firstSubtitleText, secondSubtitleText);
+
+ assertThat(allCues.get(2).startTimeUs).isEqualTo(4_230_000);
+ assertThat(allCues.get(2).durationUs).isEqualTo(1_000_000);
+ assertThat(Iterables.transform(allCues.get(2).cues, cue -> cue.text.toString()))
+ .containsExactly(secondSubtitleText);
+
+ assertThat(allCues.get(3).startTimeUs).isEqualTo(5_230_000);
+ assertThat(allCues.get(3).durationUs).isEqualTo(770_000);
+ assertThat(allCues.get(3).cues).isEmpty();
+
+ assertThat(allCues.get(4).startTimeUs).isEqualTo(6_000_000);
+ assertThat(allCues.get(4).durationUs).isEqualTo(2_440_000);
+ assertThat(Iterables.transform(allCues.get(4).cues, cue -> cue.text.toString()))
+ .containsExactly(thirdSubtitleText);
+
+ assertThat(allCues.get(5).startTimeUs).isEqualTo(8_440_000);
+ assertThat(allCues.get(5).durationUs).isEqualTo(1_000_000);
+ assertThat(Iterables.transform(allCues.get(5).cues, cue -> cue.text.toString()))
+ .containsExactly(fourthSubtitleText, fifthSubtitleText);
+
+ assertThat(allCues.get(6).startTimeUs).isEqualTo(9_440_000);
+ assertThat(allCues.get(6).durationUs).isEqualTo(1_280_000);
+ assertThat(allCues.get(6).cues).isEmpty();
+
+ assertThat(allCues.get(7).startTimeUs).isEqualTo(10_720_000);
+ assertThat(allCues.get(7).durationUs).isEqualTo(2_500_000);
+ assertThat(Iterables.transform(allCues.get(7).cues, cue -> cue.text.toString()))
+ .containsExactly(sixthSubtitleText);
+
+ assertThat(allCues.get(8).startTimeUs).isEqualTo(13_220_000);
+ assertThat(allCues.get(8).durationUs).isEqualTo(1_000_000);
+ assertThat(Iterables.transform(allCues.get(8).cues, cue -> cue.text.toString()))
+ .containsExactly(sixthSubtitleText, seventhSubtitleText);
+
+ assertThat(allCues.get(9).startTimeUs).isEqualTo(14_220_000);
+ assertThat(allCues.get(9).durationUs).isEqualTo(1_430_000);
+ assertThat(Iterables.transform(allCues.get(9).cues, cue -> cue.text.toString()))
+ .containsExactly(sixthSubtitleText);
+
+ assertThat(allCues.get(10).startTimeUs).isEqualTo(15_650_000);
+ assertThat(allCues.get(10).durationUs).isEqualTo(C.TIME_UNSET);
+ assertThat(allCues.get(10).cues).isEmpty();
+ }
+
+ @Test
+ public void parsePositions() throws IOException {
+ SsaParser parser = new SsaParser();
+ byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), POSITIONS);
+ List allCues = parser.parse(bytes);
+
+ // Check \pos() sets position & line
+ Cue firstCue = Iterables.getOnlyElement(allCues.get(0).cues);
+ assertThat(firstCue.position).isEqualTo(0.5f);
+ assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION);
+ assertThat(firstCue.line).isEqualTo(0.25f);
+
+ // Check the \pos() doesn't need to be at the start of the line.
+ Cue secondCue = Iterables.getOnlyElement(allCues.get(2).cues);
+ assertThat(secondCue.position).isEqualTo(0.25f);
+ assertThat(secondCue.line).isEqualTo(0.25f);
+
+ // Check only the last \pos() value is used.
+ Cue thirdCue = Iterables.getOnlyElement(allCues.get(4).cues);
+ assertThat(thirdCue.position).isEqualTo(0.25f);
+
+ // Check \move() is treated as \pos()
+ Cue fourthCue = Iterables.getOnlyElement(allCues.get(6).cues);
+ assertThat(fourthCue.position).isEqualTo(0.5f);
+ assertThat(fourthCue.line).isEqualTo(0.25f);
+
+ // Check alignment override in a separate brace (to bottom-center) affects textAlignment and
+ // both line & position anchors.
+ Cue fifthCue = Iterables.getOnlyElement(allCues.get(8).cues);
+ assertThat(fifthCue.position).isEqualTo(0.5f);
+ assertThat(fifthCue.line).isEqualTo(0.5f);
+ assertWithMessage("Cue.positionAnchor")
+ .that(fifthCue.positionAnchor)
+ .isEqualTo(Cue.ANCHOR_TYPE_MIDDLE);
+ assertThat(fifthCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END);
+ assertWithMessage("Cue.textAlignment")
+ .that(fifthCue.textAlignment)
+ .isEqualTo(Layout.Alignment.ALIGN_CENTER);
+
+ // Check alignment override in the same brace (to top-right) affects textAlignment and both line
+ // & position anchors.
+ Cue sixthCue = Iterables.getOnlyElement(allCues.get(10).cues);
+ assertThat(sixthCue.position).isEqualTo(0.5f);
+ assertThat(sixthCue.line).isEqualTo(0.5f);
+ assertWithMessage("Cue.positionAnchor")
+ .that(sixthCue.positionAnchor)
+ .isEqualTo(Cue.ANCHOR_TYPE_END);
+ assertThat(sixthCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_START);
+ assertWithMessage("Cue.textAlignment")
+ .that(sixthCue.textAlignment)
+ .isEqualTo(Layout.Alignment.ALIGN_OPPOSITE);
+ }
+
+ @Test
+ public void parseInvalidPositions() throws IOException {
+ SsaParser parser = new SsaParser();
+ byte[] bytes =
+ TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), INVALID_POSITIONS);
+ List allCues = parser.parse(bytes);
+
+ // Negative parameter to \pos() - fall back to the positions implied by middle-left alignment.
+ Cue firstCue = Iterables.getOnlyElement(allCues.get(0).cues);
+ assertThat(firstCue.position).isEqualTo(0.05f);
+ assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION);
+ assertThat(firstCue.line).isEqualTo(0.5f);
+
+ // Negative parameter to \move() - fall back to the positions implied by middle-left alignment.
+ Cue secondCue = Iterables.getOnlyElement(allCues.get(2).cues);
+ assertThat(secondCue.position).isEqualTo(0.05f);
+ assertThat(secondCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION);
+ assertThat(secondCue.line).isEqualTo(0.5f);
+
+ // Check invalid alignment override (11) is skipped and style-provided one is used (4).
+ Cue thirdCue = Iterables.getOnlyElement(allCues.get(4).cues);
+ assertWithMessage("Cue.positionAnchor")
+ .that(thirdCue.positionAnchor)
+ .isEqualTo(Cue.ANCHOR_TYPE_START);
+ assertThat(thirdCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE);
+ assertWithMessage("Cue.textAlignment")
+ .that(thirdCue.textAlignment)
+ .isEqualTo(Layout.Alignment.ALIGN_NORMAL);
+
+ // No braces - fall back to the positions implied by middle-left alignment
+ Cue fourthCue = Iterables.getOnlyElement(allCues.get(6).cues);
+ assertThat(fourthCue.position).isEqualTo(0.05f);
+ assertThat(fourthCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION);
+ assertThat(fourthCue.line).isEqualTo(0.5f);
+ }
+
+ @Test
+ public void parsePositionsWithMissingPlayResY() throws IOException {
+ SsaParser parser = new SsaParser();
+ byte[] bytes =
+ TestUtil.getByteArray(
+ ApplicationProvider.getApplicationContext(), POSITIONS_WITHOUT_PLAYRES);
+ List allCues = parser.parse(bytes);
+
+ // The dialogue line has a valid \pos() override, but it's ignored because PlayResY isn't
+ // set (so we don't know the denominator).
+ Cue firstCue = Iterables.getOnlyElement(allCues.get(0).cues);
+ assertThat(firstCue.position).isEqualTo(Cue.DIMEN_UNSET);
+ assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION);
+ assertThat(firstCue.line).isEqualTo(Cue.DIMEN_UNSET);
+ }
+
+ @Test
+ public void parseInvalidTimecodes() throws IOException {
+ // Parsing should succeed, parsing the third cue only.
+ SsaParser parser = new SsaParser();
+ byte[] bytes =
+ TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), INVALID_TIMECODES);
+ List allCues = parser.parse(bytes);
+
+ assertThat(allCues).hasSize(2);
+ assertTypicalCue3(allCues.get(0));
+ }
+
+ @Test
+ public void parsePrimaryColor() throws IOException {
+ SsaParser parser = new SsaParser();
+ byte[] bytes =
+ TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_PRIMARY_COLOR);
+ List allCues = parser.parse(bytes);
+ assertThat(allCues).hasSize(14);
+ // &H000000FF (AABBGGRR) -> #FFFF0000 (AARRGGBB)
+ Spanned firstCueText = (Spanned) Iterables.getOnlyElement(allCues.get(0).cues).text;
+ SpannedSubject.assertThat(firstCueText)
+ .hasForegroundColorSpanBetween(0, firstCueText.length())
+ .withColor(Color.RED);
+ // &H0000FFFF (AABBGGRR) -> #FFFFFF00 (AARRGGBB)
+ Spanned secondCueText = (Spanned) Iterables.getOnlyElement(allCues.get(2).cues).text;
+ SpannedSubject.assertThat(secondCueText)
+ .hasForegroundColorSpanBetween(0, secondCueText.length())
+ .withColor(Color.YELLOW);
+ // &HFF00 (GGRR) -> #FF00FF00 (AARRGGBB)
+ Spanned thirdCueText = (Spanned) Iterables.getOnlyElement(allCues.get(4).cues).text;
+ SpannedSubject.assertThat(thirdCueText)
+ .hasForegroundColorSpanBetween(0, thirdCueText.length())
+ .withColor(Color.GREEN);
+ // &HA00000FF (AABBGGRR) -> #5FFF0000 (AARRGGBB)
+ Spanned fourthCueText = (Spanned) Iterables.getOnlyElement(allCues.get(6).cues).text;
+ SpannedSubject.assertThat(fourthCueText)
+ .hasForegroundColorSpanBetween(0, fourthCueText.length())
+ .withColor(0x5FFF0000);
+ // 16711680 (AABBGGRR) -> &H00FF0000 (AABBGGRR) -> #FF0000FF (AARRGGBB)
+ Spanned fifthCueText = (Spanned) Iterables.getOnlyElement(allCues.get(8).cues).text;
+ SpannedSubject.assertThat(fifthCueText)
+ .hasForegroundColorSpanBetween(0, fifthCueText.length())
+ .withColor(0xFF0000FF);
+ // 2164195328 (AABBGGRR) -> &H80FF0000 (AABBGGRR) -> #7F0000FF (AARRGGBB)
+ Spanned sixthCueText = (Spanned) Iterables.getOnlyElement(allCues.get(10).cues).text;
+ SpannedSubject.assertThat(sixthCueText)
+ .hasForegroundColorSpanBetween(0, sixthCueText.length())
+ .withColor(0x7F0000FF);
+ Spanned seventhCueText = (Spanned) Iterables.getOnlyElement(allCues.get(12).cues).text;
+ SpannedSubject.assertThat(seventhCueText)
+ .hasNoForegroundColorSpanBetween(0, seventhCueText.length());
+ }
+
+ @Test
+ public void parseOutlineColor() throws IOException {
+ SsaParser parser = new SsaParser();
+ byte[] bytes =
+ TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_OUTLINE_COLOR);
+ List allCues = parser.parse(bytes);
+ assertThat(allCues).hasSize(4);
+ Spanned firstCueText = (Spanned) Iterables.getOnlyElement(allCues.get(0).cues).text;
+ SpannedSubject.assertThat(firstCueText)
+ .hasBackgroundColorSpanBetween(0, firstCueText.length())
+ .withColor(Color.BLUE);
+
+ // OutlineColour should be treated as background only when BorderStyle=3
+ Spanned secondCueText = (Spanned) Iterables.getOnlyElement(allCues.get(2).cues).text;
+ SpannedSubject.assertThat(secondCueText)
+ .hasNoBackgroundColorSpanBetween(0, secondCueText.length());
+ }
+
+ @Test
+ public void parseFontSize() throws IOException {
+ SsaParser parser = new SsaParser();
+ byte[] bytes =
+ TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_FONT_SIZE);
+ List allCues = parser.parse(bytes);
+ assertThat(allCues).hasSize(4);
+
+ Cue firstCue = Iterables.getOnlyElement(allCues.get(0).cues);
+ assertThat(firstCue.textSize).isWithin(1.0e-8f).of(30f / 720f);
+ assertThat(firstCue.textSizeType).isEqualTo(Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING);
+ Cue secondCue = Iterables.getOnlyElement(allCues.get(2).cues);
+ assertThat(secondCue.textSize).isWithin(1.0e-8f).of(72.2f / 720f);
+ assertThat(secondCue.textSizeType).isEqualTo(Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING);
+ }
+
+ @Test
+ public void parseBoldItalic() throws IOException {
+ SsaParser parser = new SsaParser();
+ byte[] bytes =
+ TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_BOLD_ITALIC);
+ List allCues = parser.parse(bytes);
+ assertThat(allCues).hasSize(6);
+
+ Spanned firstCueText = (Spanned) Iterables.getOnlyElement(allCues.get(0).cues).text;
+ SpannedSubject.assertThat(firstCueText).hasBoldSpanBetween(0, firstCueText.length());
+ Spanned secondCueText = (Spanned) Iterables.getOnlyElement(allCues.get(2).cues).text;
+ SpannedSubject.assertThat(secondCueText).hasItalicSpanBetween(0, secondCueText.length());
+ Spanned thirdCueText = (Spanned) Iterables.getOnlyElement(allCues.get(4).cues).text;
+ SpannedSubject.assertThat(thirdCueText).hasBoldItalicSpanBetween(0, thirdCueText.length());
+ }
+
+ @Test
+ public void parseUnderline() throws IOException {
+ SsaParser parser = new SsaParser();
+ byte[] bytes =
+ TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_UNDERLINE);
+ List allCues = parser.parse(bytes);
+ assertThat(allCues).hasSize(4);
+
+ Spanned firstCueText = (Spanned) Iterables.getOnlyElement(allCues.get(0).cues).text;
+ SpannedSubject.assertThat(firstCueText).hasUnderlineSpanBetween(0, firstCueText.length());
+ Spanned secondCueText = (Spanned) Iterables.getOnlyElement(allCues.get(2).cues).text;
+ SpannedSubject.assertThat(secondCueText).hasNoUnderlineSpanBetween(0, secondCueText.length());
+ }
+
+ @Test
+ public void parseStrikeout() throws IOException {
+ SsaParser parser = new SsaParser();
+ byte[] bytes =
+ TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_STRIKEOUT);
+ List allCues = parser.parse(bytes);
+ assertThat(allCues).hasSize(4);
+
+ Spanned firstCueText = (Spanned) Iterables.getOnlyElement(allCues.get(0).cues).text;
+ SpannedSubject.assertThat(firstCueText).hasStrikethroughSpanBetween(0, firstCueText.length());
+ Spanned secondCueText = (Spanned) Iterables.getOnlyElement(allCues.get(2).cues).text;
+ SpannedSubject.assertThat(secondCueText)
+ .hasNoStrikethroughSpanBetween(0, secondCueText.length());
+ }
+
+ private static void assertTypicalCue1(CuesWithTiming cuesWithTiming) {
+ assertThat(cuesWithTiming.startTimeUs).isEqualTo(0);
+ assertThat(cuesWithTiming.durationUs).isEqualTo(1230000);
+ assertThat(cuesWithTiming.cues.get(0).text.toString()).isEqualTo("This is the first subtitle.");
+ assertThat(Objects.requireNonNull(cuesWithTiming.cues.get(0).textAlignment))
+ .isEqualTo(Layout.Alignment.ALIGN_CENTER);
+ }
+
+ private static void assertTypicalCue2(CuesWithTiming cuesWithTiming) {
+ assertThat(cuesWithTiming.startTimeUs).isEqualTo(2340000);
+ assertThat(cuesWithTiming.durationUs).isEqualTo(3450000 - 2340000);
+ assertThat(cuesWithTiming.cues.get(0).text.toString())
+ .isEqualTo("This is the second subtitle \nwith a newline \nand another.");
+ }
+
+ private static void assertTypicalCue3(CuesWithTiming cuesWithTiming) {
+ assertThat(cuesWithTiming.startTimeUs).isEqualTo(4560000);
+ assertThat(cuesWithTiming.durationUs).isEqualTo(8900000 - 4560000);
+ assertThat(cuesWithTiming.cues.get(0).text.toString())
+ .isEqualTo("This is the third subtitle, with a comma.");
+ }
+}