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."); + } +}