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 0e1a6dac72..2cf7703e6c 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
@@ -25,7 +25,7 @@ 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.SsaParser;
-import androidx.media3.extractor.text.subrip.SubripDecoder;
+import androidx.media3.extractor.text.subrip.SubripParser;
import androidx.media3.extractor.text.ttml.TtmlDecoder;
import androidx.media3.extractor.text.tx3g.Tx3gDecoder;
import androidx.media3.extractor.text.webvtt.Mp4WebvttDecoder;
@@ -62,7 +62,7 @@ public interface SubtitleDecoderFactory {
*
WebVTT ({@link WebvttDecoder})
* WebVTT (MP4) ({@link Mp4WebvttDecoder})
* TTML ({@link TtmlDecoder})
- * SubRip ({@link SubripDecoder})
+ * SubRip ({@link SubripParser})
* SSA/ASS ({@link SsaParser})
* TX3G ({@link Tx3gDecoder})
* Cea608 ({@link Cea608Decoder})
@@ -108,7 +108,8 @@ public interface SubtitleDecoderFactory {
case MimeTypes.APPLICATION_TTML:
return new TtmlDecoder();
case MimeTypes.APPLICATION_SUBRIP:
- return new SubripDecoder();
+ return new DelegatingSubtitleDecoder(
+ "DelegatingSubtitleDecoderWithSubripParser", new SubripParser());
case MimeTypes.APPLICATION_TX3G:
return new Tx3gDecoder(format.initializationData);
case MimeTypes.APPLICATION_CEA608:
diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/text/subrip/SubripDecoderTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/text/DelegatingSubtitleDecoderWithSubripParserTest.java
similarity index 84%
rename from libraries/extractor/src/test/java/androidx/media3/extractor/text/subrip/SubripDecoderTest.java
rename to libraries/exoplayer/src/test/java/androidx/media3/exoplayer/text/DelegatingSubtitleDecoderWithSubripParserTest.java
index 642e20e259..9e8a650955 100644
--- a/libraries/extractor/src/test/java/androidx/media3/extractor/text/subrip/SubripDecoderTest.java
+++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/text/DelegatingSubtitleDecoderWithSubripParserTest.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,12 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.media3.extractor.text.subrip;
+package androidx.media3.exoplayer.text;
import static com.google.common.truth.Truth.assertThat;
import androidx.media3.common.text.Cue;
import androidx.media3.extractor.text.Subtitle;
+import androidx.media3.extractor.text.subrip.SubripParser;
import androidx.media3.test.utils.TestUtil;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -26,9 +27,9 @@ import java.io.IOException;
import org.junit.Test;
import org.junit.runner.RunWith;
-/** Unit test for {@link SubripDecoder}. */
+/** Unit test for a DelegatingSubtitleDecoder backed by {@link SubripParser}. */
@RunWith(AndroidJUnit4.class)
-public final class SubripDecoderTest {
+public class DelegatingSubtitleDecoderWithSubripParserTest {
private static final String EMPTY_FILE = "media/subrip/empty";
private static final String TYPICAL_FILE = "media/subrip/typical";
@@ -48,7 +49,8 @@ public final class SubripDecoderTest {
@Test
public void decodeEmpty() throws IOException {
- SubripDecoder decoder = new SubripDecoder();
+ DelegatingSubtitleDecoder decoder =
+ new DelegatingSubtitleDecoder("SubripDecoder", new SubripParser());
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), EMPTY_FILE);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
@@ -59,7 +61,8 @@ public final class SubripDecoderTest {
@Test
public void decodeTypical() throws IOException {
- SubripDecoder decoder = new SubripDecoder();
+ DelegatingSubtitleDecoder decoder =
+ new DelegatingSubtitleDecoder("SubripDecoder", new SubripParser());
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_FILE);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
@@ -72,7 +75,8 @@ public final class SubripDecoderTest {
@Test
public void decodeTypicalWithByteOrderMark() throws IOException {
- SubripDecoder decoder = new SubripDecoder();
+ DelegatingSubtitleDecoder decoder =
+ new DelegatingSubtitleDecoder("SubripDecoder", new SubripParser());
byte[] bytes =
TestUtil.getByteArray(
ApplicationProvider.getApplicationContext(), TYPICAL_WITH_BYTE_ORDER_MARK);
@@ -87,7 +91,8 @@ public final class SubripDecoderTest {
@Test
public void decodeTypicalExtraBlankLine() throws IOException {
- SubripDecoder decoder = new SubripDecoder();
+ DelegatingSubtitleDecoder decoder =
+ new DelegatingSubtitleDecoder("SubripDecoder", new SubripParser());
byte[] bytes =
TestUtil.getByteArray(
ApplicationProvider.getApplicationContext(), TYPICAL_EXTRA_BLANK_LINE);
@@ -103,7 +108,8 @@ public final class SubripDecoderTest {
@Test
public void decodeTypicalMissingTimecode() throws IOException {
// Parsing should succeed, parsing the first and third cues only.
- SubripDecoder decoder = new SubripDecoder();
+ DelegatingSubtitleDecoder decoder =
+ new DelegatingSubtitleDecoder("SubripDecoder", new SubripParser());
byte[] bytes =
TestUtil.getByteArray(
ApplicationProvider.getApplicationContext(), TYPICAL_MISSING_TIMECODE);
@@ -118,7 +124,8 @@ public final class SubripDecoderTest {
@Test
public void decodeTypicalMissingSequence() throws IOException {
// Parsing should succeed, parsing the first and third cues only.
- SubripDecoder decoder = new SubripDecoder();
+ DelegatingSubtitleDecoder decoder =
+ new DelegatingSubtitleDecoder("SubripDecoder", new SubripParser());
byte[] bytes =
TestUtil.getByteArray(
ApplicationProvider.getApplicationContext(), TYPICAL_MISSING_SEQUENCE);
@@ -133,7 +140,8 @@ public final class SubripDecoderTest {
@Test
public void decodeTypicalNegativeTimestamps() throws IOException {
// Parsing should succeed, parsing the third cue only.
- SubripDecoder decoder = new SubripDecoder();
+ DelegatingSubtitleDecoder decoder =
+ new DelegatingSubtitleDecoder("SubripDecoder", new SubripParser());
byte[] bytes =
TestUtil.getByteArray(
ApplicationProvider.getApplicationContext(), TYPICAL_NEGATIVE_TIMESTAMPS);
@@ -147,7 +155,8 @@ public final class SubripDecoderTest {
@Test
public void decodeTypicalUnexpectedEnd() throws IOException {
// Parsing should succeed, parsing the first and second cues only.
- SubripDecoder decoder = new SubripDecoder();
+ DelegatingSubtitleDecoder decoder =
+ new DelegatingSubtitleDecoder("SubripDecoder", new SubripParser());
byte[] bytes =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_UNEXPECTED_END);
@@ -160,7 +169,8 @@ public final class SubripDecoderTest {
@Test
public void decodeTypicalUtf16LittleEndian() throws IOException {
- SubripDecoder decoder = new SubripDecoder();
+ DelegatingSubtitleDecoder decoder =
+ new DelegatingSubtitleDecoder("SubripDecoder", new SubripParser());
byte[] bytes =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_UTF16LE);
@@ -174,7 +184,8 @@ public final class SubripDecoderTest {
@Test
public void decodeTypicalUtf16BigEndian() throws IOException {
- SubripDecoder decoder = new SubripDecoder();
+ DelegatingSubtitleDecoder decoder =
+ new DelegatingSubtitleDecoder("SubripDecoder", new SubripParser());
byte[] bytes =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_UTF16BE);
@@ -188,7 +199,8 @@ public final class SubripDecoderTest {
@Test
public void decodeCueWithTag() throws IOException {
- SubripDecoder decoder = new SubripDecoder();
+ DelegatingSubtitleDecoder decoder =
+ new DelegatingSubtitleDecoder("SubripDecoder", new SubripParser());
byte[] bytes =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_WITH_TAGS);
@@ -217,7 +229,8 @@ public final class SubripDecoderTest {
@Test
public void decodeTypicalNoHoursAndMillis() throws IOException {
- SubripDecoder decoder = new SubripDecoder();
+ DelegatingSubtitleDecoder decoder =
+ new DelegatingSubtitleDecoder("SubripDecoder", new SubripParser());
byte[] bytes =
TestUtil.getByteArray(
ApplicationProvider.getApplicationContext(), TYPICAL_NO_HOURS_AND_MILLIS);
@@ -263,9 +276,9 @@ public final class SubripDecoderTest {
Cue cue = subtitle.getCues(eventTimeUs).get(0);
assertThat(cue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION);
assertThat(cue.lineAnchor).isEqualTo(lineAnchor);
- assertThat(cue.line).isEqualTo(SubripDecoder.getFractionalPositionForAnchorType(lineAnchor));
assertThat(cue.positionAnchor).isEqualTo(positionAnchor);
+ assertThat(cue.line).isEqualTo(SubripParser.getFractionalPositionForAnchorType(lineAnchor));
assertThat(cue.position)
- .isEqualTo(SubripDecoder.getFractionalPositionForAnchorType(positionAnchor));
+ .isEqualTo(SubripParser.getFractionalPositionForAnchorType(positionAnchor));
}
}
diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/subrip/SubripDecoder.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/subrip/SubripParser.java
similarity index 79%
rename from libraries/extractor/src/main/java/androidx/media3/extractor/text/subrip/SubripDecoder.java
rename to libraries/extractor/src/main/java/androidx/media3/extractor/text/subrip/SubripParser.java
index 6147ff92ad..ca99741fc4 100644
--- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/subrip/SubripDecoder.java
+++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/subrip/SubripParser.java
@@ -15,34 +15,40 @@
*/
package androidx.media3.extractor.text.subrip;
+import static androidx.annotation.VisibleForTesting.PRIVATE;
+
import android.text.Html;
import android.text.Spanned;
import android.text.TextUtils;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.media3.common.C;
import androidx.media3.common.text.Cue;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.LongArray;
import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.common.util.UnstableApi;
-import androidx.media3.extractor.text.SimpleSubtitleDecoder;
-import androidx.media3.extractor.text.Subtitle;
+import androidx.media3.common.util.Util;
+import androidx.media3.extractor.text.CuesWithTiming;
+import androidx.media3.extractor.text.SubtitleParser;
import com.google.common.base.Charsets;
+import com.google.common.collect.ImmutableList;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-/** A {@link SimpleSubtitleDecoder} for SubRip. */
+/** A {@link SubtitleParser} for SubRip. */
@UnstableApi
-public final class SubripDecoder extends SimpleSubtitleDecoder {
+public final class SubripParser implements SubtitleParser {
// Fractional positions for use when alignment tags are present.
private static final float START_FRACTION = 0.08f;
private static final float END_FRACTION = 1 - START_FRACTION;
private static final float MID_FRACTION = 0.5f;
- private static final String TAG = "SubripDecoder";
+ private static final String TAG = "SubripParser";
// Some SRT files don't include hours or milliseconds in the timecode, so we use optional groups.
private static final String SUBRIP_TIMECODE = "(?:(\\d+):)?(\\d+):(\\d+)(?:,(\\d+))?";
@@ -66,18 +72,25 @@ public final class SubripDecoder extends SimpleSubtitleDecoder {
private final StringBuilder textBuilder;
private final ArrayList tags;
+ private byte[] dataScratch = Util.EMPTY_BYTE_ARRAY;
- public SubripDecoder() {
- super("SubripDecoder");
+ public SubripParser() {
textBuilder = new StringBuilder();
tags = new ArrayList<>();
}
+ @Nullable
@Override
- protected Subtitle decode(byte[] data, int length, boolean reset) {
+ public ImmutableList parse(byte[] data, int offset, int length) {
ArrayList cues = new ArrayList<>();
- LongArray cueTimesUs = new LongArray();
- ParsableByteArray subripData = new ParsableByteArray(data, length);
+ LongArray startTimesUs = new LongArray();
+
+ if (dataScratch.length < length) {
+ dataScratch = new byte[length];
+ }
+ System.arraycopy(
+ /* src= */ data, /* scrPos= */ offset, /* dest= */ dataScratch, /* destPos= */ 0, length);
+ ParsableByteArray subripData = new ParsableByteArray(dataScratch, length);
Charset charset = detectUtfCharset(subripData);
@Nullable String currentLine;
@@ -104,8 +117,8 @@ public final class SubripDecoder extends SimpleSubtitleDecoder {
Matcher matcher = SUBRIP_TIMING_LINE.matcher(currentLine);
if (matcher.matches()) {
- cueTimesUs.add(parseTimecode(matcher, /* groupOffset= */ 1));
- cueTimesUs.add(parseTimecode(matcher, /* groupOffset= */ 6));
+ startTimesUs.add(parseTimecode(matcher, /* groupOffset= */ 1));
+ startTimesUs.add(parseTimecode(matcher, /* groupOffset= */ 6));
} else {
Log.w(TAG, "Skipping invalid timing: " + currentLine);
continue;
@@ -138,11 +151,22 @@ public final class SubripDecoder extends SimpleSubtitleDecoder {
cues.add(Cue.EMPTY);
}
- Cue[] cuesArray = cues.toArray(new Cue[0]);
- long[] cueTimesUsArray = cueTimesUs.toArray();
- return new SubripSubtitle(cuesArray, cueTimesUsArray);
+ ImmutableList.Builder cuesWithStartTimeAndDuration = ImmutableList.builder();
+ for (int i = 0; i < cues.size(); i++) {
+ cuesWithStartTimeAndDuration.add(
+ new CuesWithTiming(
+ /* cues= */ ImmutableList.of(cues.get(i)),
+ /* startTimeUs= */ startTimesUs.get(i),
+ /* durationUs= */ i == cues.size() - 1
+ ? C.TIME_UNSET
+ : startTimesUs.get(i + 1) - startTimesUs.get(i)));
+ }
+ return cuesWithStartTimeAndDuration.build();
}
+ @Override
+ public void reset() {}
+
/**
* Determine UTF encoding of the byte array from a byte order mark (BOM), defaulting to UTF-8 if
* no BOM is found.
@@ -248,14 +272,17 @@ public final class SubripDecoder extends SimpleSubtitleDecoder {
return timestampMs * 1000;
}
- /* package */ static float getFractionalPositionForAnchorType(@Cue.AnchorType int anchorType) {
+ // TODO(b/289983417): Make package-private again, once it is no longer needed in
+ // DelegatingSubtitleDecoderWithSubripParserTest.java (i.e. legacy subtitle flow is removed)
+ @VisibleForTesting(otherwise = PRIVATE)
+ public static float getFractionalPositionForAnchorType(@Cue.AnchorType int anchorType) {
switch (anchorType) {
case Cue.ANCHOR_TYPE_START:
- return SubripDecoder.START_FRACTION;
+ return START_FRACTION;
case Cue.ANCHOR_TYPE_MIDDLE:
- return SubripDecoder.MID_FRACTION;
+ return MID_FRACTION;
case Cue.ANCHOR_TYPE_END:
- return SubripDecoder.END_FRACTION;
+ return END_FRACTION;
case Cue.TYPE_UNSET:
default:
// Should never happen.
diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/subrip/SubripSubtitle.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/subrip/SubripSubtitle.java
deleted file mode 100644
index 68ee42ff50..0000000000
--- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/subrip/SubripSubtitle.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * 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.subrip;
-
-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 a SubRip subtitle. */
-/* package */ final class SubripSubtitle implements Subtitle {
-
- private final Cue[] cues;
- private final long[] cueTimesUs;
-
- /**
- * @param cues The cues in the subtitle.
- * @param cueTimesUs The cue times, in microseconds.
- */
- public SubripSubtitle(Cue[] cues, long[] cueTimesUs) {
- this.cues = cues;
- this.cueTimesUs = cueTimesUs;
- }
-
- @Override
- public int getNextEventTimeIndex(long timeUs) {
- int index = Util.binarySearchCeil(cueTimesUs, timeUs, false, false);
- return index < cueTimesUs.length ? index : C.INDEX_UNSET;
- }
-
- @Override
- public int getEventTimeCount() {
- return cueTimesUs.length;
- }
-
- @Override
- public long getEventTime(int index) {
- Assertions.checkArgument(index >= 0);
- Assertions.checkArgument(index < cueTimesUs.length);
- return cueTimesUs[index];
- }
-
- @Override
- public List getCues(long timeUs) {
- int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false);
- if (index == -1 || cues[index] == Cue.EMPTY) {
- // timeUs is earlier than the start of the first cue, or we have an empty cue.
- return Collections.emptyList();
- } else {
- return Collections.singletonList(cues[index]);
- }
- }
-}
diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/text/subrip/SubripParserTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/text/subrip/SubripParserTest.java
new file mode 100644
index 0000000000..f887af9fa9
--- /dev/null
+++ b/libraries/extractor/src/test/java/androidx/media3/extractor/text/subrip/SubripParserTest.java
@@ -0,0 +1,282 @@
+/*
+ * 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.subrip;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.media3.common.text.Cue;
+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 java.io.IOException;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit test for {@link SubripParser}. */
+@RunWith(AndroidJUnit4.class)
+public final class SubripParserTest {
+
+ private static final String EMPTY_FILE = "media/subrip/empty";
+ private static final String TYPICAL_FILE = "media/subrip/typical";
+ private static final String TYPICAL_WITH_BYTE_ORDER_MARK =
+ "media/subrip/typical_with_byte_order_mark";
+ private static final String TYPICAL_EXTRA_BLANK_LINE = "media/subrip/typical_extra_blank_line";
+ private static final String TYPICAL_MISSING_TIMECODE = "media/subrip/typical_missing_timecode";
+ private static final String TYPICAL_MISSING_SEQUENCE = "media/subrip/typical_missing_sequence";
+ private static final String TYPICAL_NEGATIVE_TIMESTAMPS =
+ "media/subrip/typical_negative_timestamps";
+ private static final String TYPICAL_UNEXPECTED_END = "media/subrip/typical_unexpected_end";
+ private static final String TYPICAL_UTF16BE = "media/subrip/typical_utf16be";
+ private static final String TYPICAL_UTF16LE = "media/subrip/typical_utf16le";
+ private static final String TYPICAL_WITH_TAGS = "media/subrip/typical_with_tags";
+ private static final String TYPICAL_NO_HOURS_AND_MILLIS =
+ "media/subrip/typical_no_hours_and_millis";
+
+ @Test
+ public void parseEmpty() throws IOException {
+ SubripParser parser = new SubripParser();
+ byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), EMPTY_FILE);
+
+ List allCues = parser.parse(bytes);
+
+ assertThat(allCues).isEmpty();
+ }
+
+ @Test
+ public void parseTypical() throws IOException {
+ SubripParser parser = new SubripParser();
+ byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_FILE);
+
+ List allCues = parser.parse(bytes);
+
+ assertThat(allCues).hasSize(6);
+ assertTypicalCue1(allCues.get(0));
+ assertTypicalCue2(allCues.get(2));
+ assertTypicalCue3(allCues.get(4));
+ }
+
+ @Test
+ public void parseTypicalAtOffsetAndRestrictedLength() throws IOException {
+ SubripParser parser = new SubripParser();
+ byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_FILE);
+
+ ImmutableList allCues = parser.parse(bytes, 10, bytes.length - 15);
+
+ 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
+ Cue thirdCue = allCues.get(2).cues.get(0);
+ assertThat(thirdCue.text.toString()).isEqualTo("This is the third subti");
+ }
+
+ @Test
+ public void parseTypicalWithByteOrderMark() throws IOException {
+ SubripParser parser = new SubripParser();
+ byte[] bytes =
+ TestUtil.getByteArray(
+ ApplicationProvider.getApplicationContext(), TYPICAL_WITH_BYTE_ORDER_MARK);
+
+ List allCues = parser.parse(bytes);
+
+ assertThat(allCues).hasSize(6);
+ assertTypicalCue1(allCues.get(0));
+ assertTypicalCue2(allCues.get(2));
+ assertTypicalCue3(allCues.get(4));
+ }
+
+ @Test
+ public void parseTypicalExtraBlankLine() throws IOException {
+ SubripParser parser = new SubripParser();
+ byte[] bytes =
+ TestUtil.getByteArray(
+ ApplicationProvider.getApplicationContext(), TYPICAL_EXTRA_BLANK_LINE);
+
+ List allCues = parser.parse(bytes);
+
+ assertThat(allCues).hasSize(6);
+ assertTypicalCue1(allCues.get(0));
+ assertTypicalCue2(allCues.get(2));
+ assertTypicalCue3(allCues.get(4));
+ }
+
+ @Test
+ public void parseTypicalMissingTimecode() throws IOException {
+ // Parsing should succeed, parsing the first and third cues only.
+ SubripParser parser = new SubripParser();
+ byte[] bytes =
+ TestUtil.getByteArray(
+ ApplicationProvider.getApplicationContext(), TYPICAL_MISSING_TIMECODE);
+
+ List allCues = parser.parse(bytes);
+
+ assertThat(allCues).hasSize(4);
+ assertTypicalCue1(allCues.get(0));
+ assertTypicalCue3(allCues.get(2));
+ }
+
+ @Test
+ public void parseTypicalMissingSequence() throws IOException {
+ // Parsing should succeed, parsing the first and third cues only.
+ SubripParser parser = new SubripParser();
+ byte[] bytes =
+ TestUtil.getByteArray(
+ ApplicationProvider.getApplicationContext(), TYPICAL_MISSING_SEQUENCE);
+
+ List allCues = parser.parse(bytes);
+
+ assertThat(allCues).hasSize(4);
+ assertTypicalCue1(allCues.get(0));
+ assertTypicalCue3(allCues.get(2));
+ }
+
+ @Test
+ public void parseTypicalNegativeTimestamps() throws IOException {
+ // Parsing should succeed, parsing the third cue only.
+ SubripParser parser = new SubripParser();
+ byte[] bytes =
+ TestUtil.getByteArray(
+ ApplicationProvider.getApplicationContext(), TYPICAL_NEGATIVE_TIMESTAMPS);
+
+ List allCues = parser.parse(bytes);
+
+ assertThat(allCues).hasSize(2);
+ assertTypicalCue3(allCues.get(0));
+ }
+
+ @Test
+ public void parseTypicalUnexpectedEnd() throws IOException {
+ // Parsing should succeed, parsing the first and second cues only.
+ SubripParser parser = new SubripParser();
+ byte[] bytes =
+ TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_UNEXPECTED_END);
+
+ List allCues = parser.parse(bytes);
+
+ assertThat(allCues).hasSize(4);
+ assertTypicalCue1(allCues.get(0));
+ assertTypicalCue2(allCues.get(2));
+ }
+
+ @Test
+ public void parseTypicalUtf16LittleEndian() throws IOException {
+ SubripParser parser = new SubripParser();
+ byte[] bytes =
+ TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_UTF16LE);
+
+ List allCues = parser.parse(bytes);
+
+ assertThat(allCues).hasSize(6);
+ assertTypicalCue1(allCues.get(0));
+ assertTypicalCue2(allCues.get(2));
+ assertTypicalCue3(allCues.get(4));
+ }
+
+ @Test
+ public void parseTypicalUtf16BigEndian() throws IOException {
+ SubripParser parser = new SubripParser();
+ byte[] bytes =
+ TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_UTF16BE);
+
+ List allCues = parser.parse(bytes);
+
+ assertThat(allCues).hasSize(6);
+ assertTypicalCue1(allCues.get(0));
+ assertTypicalCue2(allCues.get(2));
+ assertTypicalCue3(allCues.get(4));
+ }
+
+ @Test
+ public void parseCueWithTag() throws IOException {
+ SubripParser parser = new SubripParser();
+ byte[] bytes =
+ TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_WITH_TAGS);
+
+ List allCues = parser.parse(bytes);
+
+ assertThat(allCues).isNotNull();
+ assertThat(allCues.get(0).cues.get(0).text.toString()).isEqualTo("This is the first subtitle.");
+ assertThat(allCues.get(2).cues.get(0).text.toString())
+ .isEqualTo("This is the second subtitle.\nSecond subtitle with second line.");
+ assertThat(allCues.get(4).cues.get(0).text.toString()).isEqualTo("This is the third subtitle.");
+ assertThat(allCues.get(6).cues.get(0).text.toString())
+ .isEqualTo("This { \\an2} is not a valid tag due to the space after the opening bracket.");
+ assertThat(allCues.get(8).cues.get(0).text.toString())
+ .isEqualTo("This is the fifth subtitle with multiple valid tags.");
+ assertAlignmentCue(allCues.get(10), Cue.ANCHOR_TYPE_END, Cue.ANCHOR_TYPE_START); // {/an1}
+ assertAlignmentCue(allCues.get(12), Cue.ANCHOR_TYPE_END, Cue.ANCHOR_TYPE_MIDDLE); // {/an2}
+ assertAlignmentCue(allCues.get(14), Cue.ANCHOR_TYPE_END, Cue.ANCHOR_TYPE_END); // {/an3}
+ assertAlignmentCue(allCues.get(16), Cue.ANCHOR_TYPE_MIDDLE, Cue.ANCHOR_TYPE_START); // {/an4}
+ assertAlignmentCue(allCues.get(18), Cue.ANCHOR_TYPE_MIDDLE, Cue.ANCHOR_TYPE_MIDDLE); // {/an5}
+ assertAlignmentCue(allCues.get(20), Cue.ANCHOR_TYPE_MIDDLE, Cue.ANCHOR_TYPE_END); // {/an6}
+ assertAlignmentCue(allCues.get(22), Cue.ANCHOR_TYPE_START, Cue.ANCHOR_TYPE_START); // {/an7}
+ assertAlignmentCue(allCues.get(24), Cue.ANCHOR_TYPE_START, Cue.ANCHOR_TYPE_MIDDLE); // {/an8}
+ assertAlignmentCue(allCues.get(26), Cue.ANCHOR_TYPE_START, Cue.ANCHOR_TYPE_END); // {/an9}
+ }
+
+ @Test
+ public void parseTypicalNoHoursAndMillis() throws IOException {
+ SubripParser parser = new SubripParser();
+ byte[] bytes =
+ TestUtil.getByteArray(
+ ApplicationProvider.getApplicationContext(), TYPICAL_NO_HOURS_AND_MILLIS);
+
+ List allCues = parser.parse(bytes);
+
+ assertThat(allCues).hasSize(6);
+ assertTypicalCue1(allCues.get(0));
+ assertThat(allCues.get(2).startTimeUs).isEqualTo(2_000_000);
+ assertThat(allCues.get(3).startTimeUs).isEqualTo(3_000_000);
+ assertTypicalCue3(allCues.get(4));
+ }
+
+ private static void assertTypicalCue1(CuesWithTiming cuesWithTiming) {
+ assertThat(cuesWithTiming.startTimeUs).isEqualTo(0);
+ assertThat(cuesWithTiming.cues.get(0).text.toString()).isEqualTo("This is the first subtitle.");
+ assertThat(cuesWithTiming.durationUs).isEqualTo(1234000);
+ }
+
+ private static void assertTypicalCue2(CuesWithTiming cuesWithTiming) {
+ assertThat(cuesWithTiming.startTimeUs).isEqualTo(2345000);
+ assertThat(cuesWithTiming.cues.get(0).text.toString())
+ .isEqualTo("This is the second subtitle.\nSecond subtitle with second line.");
+ assertThat(cuesWithTiming.durationUs).isEqualTo(3456000 - 2345000);
+ }
+
+ private static void assertTypicalCue3(CuesWithTiming cuesWithTiming) {
+ long expectedStartTimeUs = (((2L * 60L * 60L) + 4L) * 1000L + 567L) * 1000L;
+ assertThat(cuesWithTiming.startTimeUs).isEqualTo(expectedStartTimeUs);
+ assertThat(cuesWithTiming.cues.get(0).text.toString()).isEqualTo("This is the third subtitle.");
+ long expectedEndTimeUs = (((2L * 60L * 60L) + 8L) * 1000L + 901L) * 1000L;
+ assertThat(cuesWithTiming.durationUs).isEqualTo(expectedEndTimeUs - expectedStartTimeUs);
+ }
+
+ private static void assertAlignmentCue(
+ CuesWithTiming cuesWithTiming,
+ @Cue.AnchorType int lineAnchor,
+ @Cue.AnchorType int positionAnchor) {
+ Cue cue = cuesWithTiming.cues.get(0);
+ assertThat(cue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION);
+ assertThat(cue.lineAnchor).isEqualTo(lineAnchor);
+ assertThat(cue.line).isEqualTo(SubripParser.getFractionalPositionForAnchorType(lineAnchor));
+ assertThat(cue.positionAnchor).isEqualTo(positionAnchor);
+ assertThat(cue.position)
+ .isEqualTo(SubripParser.getFractionalPositionForAnchorType(positionAnchor));
+ }
+}