mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
SsaParser implementation - moved from SsaDecoder
`SsaDecoder` which used to be `SimpleSubtitleDecoder` will now be called `SsaParser` and implement `SubtitleParser` interface. For backwards compatibility, we will have the same functionality provided by `DelegatingSubtitleDecoder` backed-up by a new `SsaParser` instance. PiperOrigin-RevId: 546336035
This commit is contained in:
parent
e1e956cdf3
commit
377d8edf9c
7 changed files with 614 additions and 113 deletions
|
|
@ -24,7 +24,7 @@ import androidx.media3.extractor.text.cea.Cea608Decoder;
|
||||||
import androidx.media3.extractor.text.cea.Cea708Decoder;
|
import androidx.media3.extractor.text.cea.Cea708Decoder;
|
||||||
import androidx.media3.extractor.text.dvb.DvbDecoder;
|
import androidx.media3.extractor.text.dvb.DvbDecoder;
|
||||||
import androidx.media3.extractor.text.pgs.PgsDecoder;
|
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.subrip.SubripDecoder;
|
||||||
import androidx.media3.extractor.text.ttml.TtmlDecoder;
|
import androidx.media3.extractor.text.ttml.TtmlDecoder;
|
||||||
import androidx.media3.extractor.text.tx3g.Tx3gDecoder;
|
import androidx.media3.extractor.text.tx3g.Tx3gDecoder;
|
||||||
|
|
@ -63,7 +63,7 @@ public interface SubtitleDecoderFactory {
|
||||||
* <li>WebVTT (MP4) ({@link Mp4WebvttDecoder})
|
* <li>WebVTT (MP4) ({@link Mp4WebvttDecoder})
|
||||||
* <li>TTML ({@link TtmlDecoder})
|
* <li>TTML ({@link TtmlDecoder})
|
||||||
* <li>SubRip ({@link SubripDecoder})
|
* <li>SubRip ({@link SubripDecoder})
|
||||||
* <li>SSA/ASS ({@link SsaDecoder})
|
* <li>SSA/ASS ({@link SsaParser})
|
||||||
* <li>TX3G ({@link Tx3gDecoder})
|
* <li>TX3G ({@link Tx3gDecoder})
|
||||||
* <li>Cea608 ({@link Cea608Decoder})
|
* <li>Cea608 ({@link Cea608Decoder})
|
||||||
* <li>Cea708 ({@link Cea708Decoder})
|
* <li>Cea708 ({@link Cea708Decoder})
|
||||||
|
|
@ -100,7 +100,9 @@ public interface SubtitleDecoderFactory {
|
||||||
case MimeTypes.TEXT_VTT:
|
case MimeTypes.TEXT_VTT:
|
||||||
return new WebvttDecoder();
|
return new WebvttDecoder();
|
||||||
case MimeTypes.TEXT_SSA:
|
case MimeTypes.TEXT_SSA:
|
||||||
return new SsaDecoder(format.initializationData);
|
return new DelegatingSubtitleDecoder(
|
||||||
|
"DelegatingSubtitleDecoderWithSsaParser",
|
||||||
|
new SsaParser(format.initializationData));
|
||||||
case MimeTypes.APPLICATION_MP4VTT:
|
case MimeTypes.APPLICATION_MP4VTT:
|
||||||
return new Mp4WebvttDecoder();
|
return new Mp4WebvttDecoder();
|
||||||
case MimeTypes.APPLICATION_TTML:
|
case MimeTypes.APPLICATION_TTML:
|
||||||
|
|
|
||||||
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* 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
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* 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
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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.assertThat;
|
||||||
import static com.google.common.truth.Truth.assertWithMessage;
|
import static com.google.common.truth.Truth.assertWithMessage;
|
||||||
|
|
@ -23,6 +23,7 @@ import android.text.Layout;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import androidx.media3.common.text.Cue;
|
import androidx.media3.common.text.Cue;
|
||||||
import androidx.media3.extractor.text.Subtitle;
|
import androidx.media3.extractor.text.Subtitle;
|
||||||
|
import androidx.media3.extractor.text.ssa.SsaParser;
|
||||||
import androidx.media3.test.utils.TestUtil;
|
import androidx.media3.test.utils.TestUtil;
|
||||||
import androidx.media3.test.utils.truth.SpannedSubject;
|
import androidx.media3.test.utils.truth.SpannedSubject;
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
|
|
@ -34,9 +35,9 @@ import java.util.Objects;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
/** Unit test for {@link SsaDecoder}. */
|
/** Unit test for a {@link DelegatingSubtitleDecoder} backed by {@link SsaParser}. */
|
||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public final class SsaDecoderTest {
|
public final class DelegatingSubtitleDecoderWithSsaParserTest {
|
||||||
|
|
||||||
private static final String EMPTY = "media/ssa/empty";
|
private static final String EMPTY = "media/ssa/empty";
|
||||||
private static final String EMPTY_STYLE_LINE = "media/ssa/empty_style_line";
|
private static final String EMPTY_STYLE_LINE = "media/ssa/empty_style_line";
|
||||||
|
|
@ -60,7 +61,7 @@ public final class SsaDecoderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void decodeEmpty() throws IOException {
|
public void decodeEmpty() throws IOException {
|
||||||
SsaDecoder decoder = new SsaDecoder();
|
DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
|
||||||
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), EMPTY);
|
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), EMPTY);
|
||||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||||
|
|
||||||
|
|
@ -69,7 +70,7 @@ public final class SsaDecoderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void decodeEmptyStyleLine() throws IOException {
|
public void decodeEmptyStyleLine() throws IOException {
|
||||||
SsaDecoder decoder = new SsaDecoder();
|
DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
|
||||||
byte[] bytes =
|
byte[] bytes =
|
||||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), EMPTY_STYLE_LINE);
|
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), EMPTY_STYLE_LINE);
|
||||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, /* reset= */ false);
|
Subtitle subtitle = decoder.decode(bytes, bytes.length, /* reset= */ false);
|
||||||
|
|
@ -90,7 +91,7 @@ public final class SsaDecoderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void decodeTypical() throws IOException {
|
public void decodeTypical() throws IOException {
|
||||||
SsaDecoder decoder = new SsaDecoder();
|
DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
|
||||||
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL);
|
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL);
|
||||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||||
|
|
||||||
|
|
@ -122,7 +123,8 @@ public final class SsaDecoderTest {
|
||||||
ArrayList<byte[]> initializationData = new ArrayList<>();
|
ArrayList<byte[]> initializationData = new ArrayList<>();
|
||||||
initializationData.add(formatBytes);
|
initializationData.add(formatBytes);
|
||||||
initializationData.add(headerBytes);
|
initializationData.add(headerBytes);
|
||||||
SsaDecoder decoder = new SsaDecoder(initializationData);
|
DelegatingSubtitleDecoder decoder =
|
||||||
|
new DelegatingSubtitleDecoder("SSA", new SsaParser(initializationData));
|
||||||
byte[] bytes =
|
byte[] bytes =
|
||||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_DIALOGUE_ONLY);
|
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_DIALOGUE_ONLY);
|
||||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||||
|
|
@ -135,7 +137,7 @@ public final class SsaDecoderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void decodeTypicalUtf16le() throws IOException {
|
public void decodeTypicalUtf16le() throws IOException {
|
||||||
SsaDecoder decoder = new SsaDecoder();
|
DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
|
||||||
byte[] bytes =
|
byte[] bytes =
|
||||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_UTF16LE);
|
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_UTF16LE);
|
||||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||||
|
|
@ -161,7 +163,7 @@ public final class SsaDecoderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void decodeTypicalUtf16be() throws IOException {
|
public void decodeTypicalUtf16be() throws IOException {
|
||||||
SsaDecoder decoder = new SsaDecoder();
|
DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
|
||||||
byte[] bytes =
|
byte[] bytes =
|
||||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_UTF16BE);
|
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_UTF16BE);
|
||||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||||
|
|
@ -187,7 +189,7 @@ public final class SsaDecoderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void decodeOverlappingTimecodes() throws IOException {
|
public void decodeOverlappingTimecodes() throws IOException {
|
||||||
SsaDecoder decoder = new SsaDecoder();
|
DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
|
||||||
byte[] bytes =
|
byte[] bytes =
|
||||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), OVERLAPPING_TIMECODES);
|
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), OVERLAPPING_TIMECODES);
|
||||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||||
|
|
@ -237,7 +239,7 @@ public final class SsaDecoderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void decodePositions() throws IOException {
|
public void decodePositions() throws IOException {
|
||||||
SsaDecoder decoder = new SsaDecoder();
|
DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
|
||||||
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), POSITIONS);
|
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), POSITIONS);
|
||||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||||
|
|
||||||
|
|
@ -290,7 +292,7 @@ public final class SsaDecoderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void decodeInvalidPositions() throws IOException {
|
public void decodeInvalidPositions() throws IOException {
|
||||||
SsaDecoder decoder = new SsaDecoder();
|
DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
|
||||||
byte[] bytes =
|
byte[] bytes =
|
||||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), INVALID_POSITIONS);
|
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), INVALID_POSITIONS);
|
||||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||||
|
|
@ -326,7 +328,7 @@ public final class SsaDecoderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void decodePositionsWithMissingPlayResY() throws IOException {
|
public void decodePositionsWithMissingPlayResY() throws IOException {
|
||||||
SsaDecoder decoder = new SsaDecoder();
|
DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
|
||||||
byte[] bytes =
|
byte[] bytes =
|
||||||
TestUtil.getByteArray(
|
TestUtil.getByteArray(
|
||||||
ApplicationProvider.getApplicationContext(), POSITIONS_WITHOUT_PLAYRES);
|
ApplicationProvider.getApplicationContext(), POSITIONS_WITHOUT_PLAYRES);
|
||||||
|
|
@ -343,7 +345,7 @@ public final class SsaDecoderTest {
|
||||||
@Test
|
@Test
|
||||||
public void decodeInvalidTimecodes() throws IOException {
|
public void decodeInvalidTimecodes() throws IOException {
|
||||||
// Parsing should succeed, parsing the third cue only.
|
// Parsing should succeed, parsing the third cue only.
|
||||||
SsaDecoder decoder = new SsaDecoder();
|
DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
|
||||||
byte[] bytes =
|
byte[] bytes =
|
||||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), INVALID_TIMECODES);
|
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), INVALID_TIMECODES);
|
||||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||||
|
|
@ -354,7 +356,7 @@ public final class SsaDecoderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void decodePrimaryColor() throws IOException {
|
public void decodePrimaryColor() throws IOException {
|
||||||
SsaDecoder decoder = new SsaDecoder();
|
DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
|
||||||
byte[] bytes =
|
byte[] bytes =
|
||||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_PRIMARY_COLOR);
|
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_PRIMARY_COLOR);
|
||||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||||
|
|
@ -403,7 +405,7 @@ public final class SsaDecoderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void decodeOutlineColor() throws IOException {
|
public void decodeOutlineColor() throws IOException {
|
||||||
SsaDecoder decoder = new SsaDecoder();
|
DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
|
||||||
byte[] bytes =
|
byte[] bytes =
|
||||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_OUTLINE_COLOR);
|
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_OUTLINE_COLOR);
|
||||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||||
|
|
@ -423,7 +425,7 @@ public final class SsaDecoderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void decodeFontSize() throws IOException {
|
public void decodeFontSize() throws IOException {
|
||||||
SsaDecoder decoder = new SsaDecoder();
|
DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
|
||||||
byte[] bytes =
|
byte[] bytes =
|
||||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_FONT_SIZE);
|
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_FONT_SIZE);
|
||||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||||
|
|
@ -439,7 +441,7 @@ public final class SsaDecoderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void decodeBoldItalic() throws IOException {
|
public void decodeBoldItalic() throws IOException {
|
||||||
SsaDecoder decoder = new SsaDecoder();
|
DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
|
||||||
byte[] bytes =
|
byte[] bytes =
|
||||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_BOLD_ITALIC);
|
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_BOLD_ITALIC);
|
||||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||||
|
|
@ -458,7 +460,7 @@ public final class SsaDecoderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void decodeUnderline() throws IOException {
|
public void decodeUnderline() throws IOException {
|
||||||
SsaDecoder decoder = new SsaDecoder();
|
DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
|
||||||
byte[] bytes =
|
byte[] bytes =
|
||||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_UNDERLINE);
|
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_UNDERLINE);
|
||||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||||
|
|
@ -474,7 +476,7 @@ public final class SsaDecoderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void decodeStrikeout() throws IOException {
|
public void decodeStrikeout() throws IOException {
|
||||||
SsaDecoder decoder = new SsaDecoder();
|
DelegatingSubtitleDecoder decoder = new DelegatingSubtitleDecoder("SSA", new SsaParser());
|
||||||
byte[] bytes =
|
byte[] bytes =
|
||||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_STRIKEOUT);
|
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_STRIKEOUT);
|
||||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
*/
|
*/
|
||||||
package androidx.media3.extractor.text.ssa;
|
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 android.text.TextUtils;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
|
||||||
|
|
@ -34,10 +34,11 @@ import androidx.media3.common.util.Log;
|
||||||
import androidx.media3.common.util.ParsableByteArray;
|
import androidx.media3.common.util.ParsableByteArray;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import androidx.media3.common.util.Util;
|
import androidx.media3.common.util.Util;
|
||||||
import androidx.media3.extractor.text.SimpleSubtitleDecoder;
|
import androidx.media3.extractor.text.CuesWithTiming;
|
||||||
import androidx.media3.extractor.text.Subtitle;
|
import androidx.media3.extractor.text.SubtitleParser;
|
||||||
import com.google.common.base.Ascii;
|
import com.google.common.base.Ascii;
|
||||||
import com.google.common.base.Charsets;
|
import com.google.common.base.Charsets;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
|
@ -47,11 +48,11 @@ import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
|
||||||
/** A {@link SimpleSubtitleDecoder} for SSA/ASS. */
|
/** A {@link SubtitleParser} for SSA/ASS. */
|
||||||
@UnstableApi
|
@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 =
|
private static final Pattern SSA_TIMECODE_PATTERN =
|
||||||
Pattern.compile("(?:(\\d+):)?(\\d+):(\\d+)[:.](\\d+)");
|
Pattern.compile("(?:(\\d+):)?(\\d+):(\\d+)[:.](\\d+)");
|
||||||
|
|
@ -81,21 +82,22 @@ public final class SsaDecoder extends SimpleSubtitleDecoder {
|
||||||
*/
|
*/
|
||||||
private float screenHeight;
|
private float screenHeight;
|
||||||
|
|
||||||
public SsaDecoder() {
|
private byte[] dataScratch = Util.EMPTY_BYTE_ARRAY;
|
||||||
|
|
||||||
|
public SsaParser() {
|
||||||
this(/* initializationData= */ null);
|
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
|
* 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
|
* 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.
|
* 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.
|
* {@code [Script Info]} and optional {@code [V4+ Styles]} section.
|
||||||
*/
|
*/
|
||||||
public SsaDecoder(@Nullable List<byte[]> initializationData) {
|
public SsaParser(@Nullable List<byte[]> initializationData) {
|
||||||
super("SsaDecoder");
|
|
||||||
screenWidth = Cue.DIMEN_UNSET;
|
screenWidth = Cue.DIMEN_UNSET;
|
||||||
screenHeight = Cue.DIMEN_UNSET;
|
screenHeight = Cue.DIMEN_UNSET;
|
||||||
|
|
||||||
|
|
@ -116,18 +118,38 @@ public final class SsaDecoder extends SimpleSubtitleDecoder {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Subtitle decode(byte[] data, int length, boolean reset) {
|
public void reset() {}
|
||||||
List<List<Cue>> cues = new ArrayList<>();
|
|
||||||
List<Long> cueTimesUs = new ArrayList<>();
|
|
||||||
|
|
||||||
ParsableByteArray parsableData = new ParsableByteArray(data, length);
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public ImmutableList<CuesWithTiming> parse(byte[] data, int offset, int length) {
|
||||||
|
List<List<Cue>> cues = new ArrayList<>();
|
||||||
|
List<Long> 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);
|
Charset charset = detectUtfCharset(parsableData);
|
||||||
|
|
||||||
if (!haveInitializationData) {
|
if (!haveInitializationData) {
|
||||||
parseHeader(parsableData, charset);
|
parseHeader(parsableData, charset);
|
||||||
}
|
}
|
||||||
parseEventBody(parsableData, cues, cueTimesUs, charset);
|
parseEventBody(parsableData, cues, startTimesUs, charset);
|
||||||
return new SsaSubtitle(cues, cueTimesUs);
|
|
||||||
|
ImmutableList.Builder<CuesWithTiming> cuesWithStartTimeAndDuration = ImmutableList.builder();
|
||||||
|
for (int i = 0; i < cues.size(); i++) {
|
||||||
|
List<Cue> 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
package androidx.media3.extractor.text.ssa;
|
package androidx.media3.extractor.text.ssa;
|
||||||
|
|
||||||
import static androidx.media3.common.util.Assertions.checkArgument;
|
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.ElementType.TYPE_USE;
|
||||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||||
|
|
||||||
|
|
@ -371,7 +371,7 @@ import java.util.regex.Pattern;
|
||||||
int strikeoutIndex = C.INDEX_UNSET;
|
int strikeoutIndex = C.INDEX_UNSET;
|
||||||
int borderStyleIndex = C.INDEX_UNSET;
|
int borderStyleIndex = C.INDEX_UNSET;
|
||||||
String[] keys =
|
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++) {
|
for (int i = 0; i < keys.length; i++) {
|
||||||
switch (Ascii.toLowerCase(keys[i].trim())) {
|
switch (Ascii.toLowerCase(keys[i].trim())) {
|
||||||
case "name":
|
case "name":
|
||||||
|
|
|
||||||
|
|
@ -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<List<Cue>> cues;
|
|
||||||
private final List<Long> cueTimesUs;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param cues The cues in the subtitle.
|
|
||||||
* @param cueTimesUs The cue times, in microseconds.
|
|
||||||
*/
|
|
||||||
public SsaSubtitle(List<List<Cue>> cues, List<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.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<Cue> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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<CuesWithTiming> 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<CuesWithTiming> 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<CuesWithTiming> 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<byte[]> initializationData = new ArrayList<>();
|
||||||
|
initializationData.add(formatBytes);
|
||||||
|
initializationData.add(headerBytes);
|
||||||
|
SsaParser parser = new SsaParser(initializationData);
|
||||||
|
byte[] bytes =
|
||||||
|
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_DIALOGUE_ONLY);
|
||||||
|
List<CuesWithTiming> 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<byte[]> initializationData = new ArrayList<>();
|
||||||
|
initializationData.add(formatBytes);
|
||||||
|
initializationData.add(headerBytes);
|
||||||
|
SsaParser parser = new SsaParser(initializationData);
|
||||||
|
byte[] bytes =
|
||||||
|
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_DIALOGUE_ONLY);
|
||||||
|
ImmutableList<CuesWithTiming> 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<CuesWithTiming> 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<CuesWithTiming> 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<CuesWithTiming> 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<CuesWithTiming> 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<CuesWithTiming> 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<CuesWithTiming> 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<CuesWithTiming> 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<CuesWithTiming> 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<CuesWithTiming> 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<CuesWithTiming> 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<CuesWithTiming> 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<CuesWithTiming> 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<CuesWithTiming> 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue