mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
SubripParser implementation - moved from SubripDecoder
`SubripDecoder` which used to be `SimpleSubtitleDecoder` will now be called `SubripParser` and implement `SubtitleParser` interface. For backwards compatibility, we will have the same functionality provided by `DelegatingSubtitleDecoder` backed-up by a new `SubripParser` instance. PiperOrigin-RevId: 546538113
This commit is contained in:
parent
377d8edf9c
commit
ca483a3c2c
@ -25,7 +25,7 @@ import androidx.media3.extractor.text.cea.Cea708Decoder;
|
||||
import androidx.media3.extractor.text.dvb.DvbDecoder;
|
||||
import androidx.media3.extractor.text.pgs.PgsDecoder;
|
||||
import androidx.media3.extractor.text.ssa.SsaParser;
|
||||
import androidx.media3.extractor.text.subrip.SubripDecoder;
|
||||
import androidx.media3.extractor.text.subrip.SubripParser;
|
||||
import androidx.media3.extractor.text.ttml.TtmlDecoder;
|
||||
import androidx.media3.extractor.text.tx3g.Tx3gDecoder;
|
||||
import androidx.media3.extractor.text.webvtt.Mp4WebvttDecoder;
|
||||
@ -62,7 +62,7 @@ public interface SubtitleDecoderFactory {
|
||||
* <li>WebVTT ({@link WebvttDecoder})
|
||||
* <li>WebVTT (MP4) ({@link Mp4WebvttDecoder})
|
||||
* <li>TTML ({@link TtmlDecoder})
|
||||
* <li>SubRip ({@link SubripDecoder})
|
||||
* <li>SubRip ({@link SubripParser})
|
||||
* <li>SSA/ASS ({@link SsaParser})
|
||||
* <li>TX3G ({@link Tx3gDecoder})
|
||||
* <li>Cea608 ({@link Cea608Decoder})
|
||||
@ -108,7 +108,8 @@ public interface SubtitleDecoderFactory {
|
||||
case MimeTypes.APPLICATION_TTML:
|
||||
return new TtmlDecoder();
|
||||
case MimeTypes.APPLICATION_SUBRIP:
|
||||
return new SubripDecoder();
|
||||
return new DelegatingSubtitleDecoder(
|
||||
"DelegatingSubtitleDecoderWithSubripParser", new SubripParser());
|
||||
case MimeTypes.APPLICATION_TX3G:
|
||||
return new Tx3gDecoder(format.initializationData);
|
||||
case MimeTypes.APPLICATION_CEA608:
|
||||
|
@ -1,11 +1,11 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
* Copyright 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@ -13,12 +13,13 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.extractor.text.subrip;
|
||||
package androidx.media3.exoplayer.text;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import androidx.media3.common.text.Cue;
|
||||
import androidx.media3.extractor.text.Subtitle;
|
||||
import androidx.media3.extractor.text.subrip.SubripParser;
|
||||
import androidx.media3.test.utils.TestUtil;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
@ -26,9 +27,9 @@ import java.io.IOException;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Unit test for {@link SubripDecoder}. */
|
||||
/** Unit test for a DelegatingSubtitleDecoder backed by {@link SubripParser}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class SubripDecoderTest {
|
||||
public class DelegatingSubtitleDecoderWithSubripParserTest {
|
||||
|
||||
private static final String EMPTY_FILE = "media/subrip/empty";
|
||||
private static final String TYPICAL_FILE = "media/subrip/typical";
|
||||
@ -48,7 +49,8 @@ public final class SubripDecoderTest {
|
||||
|
||||
@Test
|
||||
public void decodeEmpty() throws IOException {
|
||||
SubripDecoder decoder = new SubripDecoder();
|
||||
DelegatingSubtitleDecoder decoder =
|
||||
new DelegatingSubtitleDecoder("SubripDecoder", new SubripParser());
|
||||
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), EMPTY_FILE);
|
||||
|
||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||
@ -59,7 +61,8 @@ public final class SubripDecoderTest {
|
||||
|
||||
@Test
|
||||
public void decodeTypical() throws IOException {
|
||||
SubripDecoder decoder = new SubripDecoder();
|
||||
DelegatingSubtitleDecoder decoder =
|
||||
new DelegatingSubtitleDecoder("SubripDecoder", new SubripParser());
|
||||
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_FILE);
|
||||
|
||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||
@ -72,7 +75,8 @@ public final class SubripDecoderTest {
|
||||
|
||||
@Test
|
||||
public void decodeTypicalWithByteOrderMark() throws IOException {
|
||||
SubripDecoder decoder = new SubripDecoder();
|
||||
DelegatingSubtitleDecoder decoder =
|
||||
new DelegatingSubtitleDecoder("SubripDecoder", new SubripParser());
|
||||
byte[] bytes =
|
||||
TestUtil.getByteArray(
|
||||
ApplicationProvider.getApplicationContext(), TYPICAL_WITH_BYTE_ORDER_MARK);
|
||||
@ -87,7 +91,8 @@ public final class SubripDecoderTest {
|
||||
|
||||
@Test
|
||||
public void decodeTypicalExtraBlankLine() throws IOException {
|
||||
SubripDecoder decoder = new SubripDecoder();
|
||||
DelegatingSubtitleDecoder decoder =
|
||||
new DelegatingSubtitleDecoder("SubripDecoder", new SubripParser());
|
||||
byte[] bytes =
|
||||
TestUtil.getByteArray(
|
||||
ApplicationProvider.getApplicationContext(), TYPICAL_EXTRA_BLANK_LINE);
|
||||
@ -103,7 +108,8 @@ public final class SubripDecoderTest {
|
||||
@Test
|
||||
public void decodeTypicalMissingTimecode() throws IOException {
|
||||
// Parsing should succeed, parsing the first and third cues only.
|
||||
SubripDecoder decoder = new SubripDecoder();
|
||||
DelegatingSubtitleDecoder decoder =
|
||||
new DelegatingSubtitleDecoder("SubripDecoder", new SubripParser());
|
||||
byte[] bytes =
|
||||
TestUtil.getByteArray(
|
||||
ApplicationProvider.getApplicationContext(), TYPICAL_MISSING_TIMECODE);
|
||||
@ -118,7 +124,8 @@ public final class SubripDecoderTest {
|
||||
@Test
|
||||
public void decodeTypicalMissingSequence() throws IOException {
|
||||
// Parsing should succeed, parsing the first and third cues only.
|
||||
SubripDecoder decoder = new SubripDecoder();
|
||||
DelegatingSubtitleDecoder decoder =
|
||||
new DelegatingSubtitleDecoder("SubripDecoder", new SubripParser());
|
||||
byte[] bytes =
|
||||
TestUtil.getByteArray(
|
||||
ApplicationProvider.getApplicationContext(), TYPICAL_MISSING_SEQUENCE);
|
||||
@ -133,7 +140,8 @@ public final class SubripDecoderTest {
|
||||
@Test
|
||||
public void decodeTypicalNegativeTimestamps() throws IOException {
|
||||
// Parsing should succeed, parsing the third cue only.
|
||||
SubripDecoder decoder = new SubripDecoder();
|
||||
DelegatingSubtitleDecoder decoder =
|
||||
new DelegatingSubtitleDecoder("SubripDecoder", new SubripParser());
|
||||
byte[] bytes =
|
||||
TestUtil.getByteArray(
|
||||
ApplicationProvider.getApplicationContext(), TYPICAL_NEGATIVE_TIMESTAMPS);
|
||||
@ -147,7 +155,8 @@ public final class SubripDecoderTest {
|
||||
@Test
|
||||
public void decodeTypicalUnexpectedEnd() throws IOException {
|
||||
// Parsing should succeed, parsing the first and second cues only.
|
||||
SubripDecoder decoder = new SubripDecoder();
|
||||
DelegatingSubtitleDecoder decoder =
|
||||
new DelegatingSubtitleDecoder("SubripDecoder", new SubripParser());
|
||||
byte[] bytes =
|
||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_UNEXPECTED_END);
|
||||
|
||||
@ -160,7 +169,8 @@ public final class SubripDecoderTest {
|
||||
|
||||
@Test
|
||||
public void decodeTypicalUtf16LittleEndian() throws IOException {
|
||||
SubripDecoder decoder = new SubripDecoder();
|
||||
DelegatingSubtitleDecoder decoder =
|
||||
new DelegatingSubtitleDecoder("SubripDecoder", new SubripParser());
|
||||
byte[] bytes =
|
||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_UTF16LE);
|
||||
|
||||
@ -174,7 +184,8 @@ public final class SubripDecoderTest {
|
||||
|
||||
@Test
|
||||
public void decodeTypicalUtf16BigEndian() throws IOException {
|
||||
SubripDecoder decoder = new SubripDecoder();
|
||||
DelegatingSubtitleDecoder decoder =
|
||||
new DelegatingSubtitleDecoder("SubripDecoder", new SubripParser());
|
||||
byte[] bytes =
|
||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_UTF16BE);
|
||||
|
||||
@ -188,7 +199,8 @@ public final class SubripDecoderTest {
|
||||
|
||||
@Test
|
||||
public void decodeCueWithTag() throws IOException {
|
||||
SubripDecoder decoder = new SubripDecoder();
|
||||
DelegatingSubtitleDecoder decoder =
|
||||
new DelegatingSubtitleDecoder("SubripDecoder", new SubripParser());
|
||||
byte[] bytes =
|
||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_WITH_TAGS);
|
||||
|
||||
@ -217,7 +229,8 @@ public final class SubripDecoderTest {
|
||||
|
||||
@Test
|
||||
public void decodeTypicalNoHoursAndMillis() throws IOException {
|
||||
SubripDecoder decoder = new SubripDecoder();
|
||||
DelegatingSubtitleDecoder decoder =
|
||||
new DelegatingSubtitleDecoder("SubripDecoder", new SubripParser());
|
||||
byte[] bytes =
|
||||
TestUtil.getByteArray(
|
||||
ApplicationProvider.getApplicationContext(), TYPICAL_NO_HOURS_AND_MILLIS);
|
||||
@ -263,9 +276,9 @@ public final class SubripDecoderTest {
|
||||
Cue cue = subtitle.getCues(eventTimeUs).get(0);
|
||||
assertThat(cue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION);
|
||||
assertThat(cue.lineAnchor).isEqualTo(lineAnchor);
|
||||
assertThat(cue.line).isEqualTo(SubripDecoder.getFractionalPositionForAnchorType(lineAnchor));
|
||||
assertThat(cue.positionAnchor).isEqualTo(positionAnchor);
|
||||
assertThat(cue.line).isEqualTo(SubripParser.getFractionalPositionForAnchorType(lineAnchor));
|
||||
assertThat(cue.position)
|
||||
.isEqualTo(SubripDecoder.getFractionalPositionForAnchorType(positionAnchor));
|
||||
.isEqualTo(SubripParser.getFractionalPositionForAnchorType(positionAnchor));
|
||||
}
|
||||
}
|
@ -15,34 +15,40 @@
|
||||
*/
|
||||
package androidx.media3.extractor.text.subrip;
|
||||
|
||||
import static androidx.annotation.VisibleForTesting.PRIVATE;
|
||||
|
||||
import android.text.Html;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.text.Cue;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.Log;
|
||||
import androidx.media3.common.util.LongArray;
|
||||
import androidx.media3.common.util.ParsableByteArray;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.extractor.text.SimpleSubtitleDecoder;
|
||||
import androidx.media3.extractor.text.Subtitle;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.extractor.text.CuesWithTiming;
|
||||
import androidx.media3.extractor.text.SubtitleParser;
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/** A {@link SimpleSubtitleDecoder} for SubRip. */
|
||||
/** A {@link SubtitleParser} for SubRip. */
|
||||
@UnstableApi
|
||||
public final class SubripDecoder extends SimpleSubtitleDecoder {
|
||||
public final class SubripParser implements SubtitleParser {
|
||||
|
||||
// Fractional positions for use when alignment tags are present.
|
||||
private static final float START_FRACTION = 0.08f;
|
||||
private static final float END_FRACTION = 1 - START_FRACTION;
|
||||
private static final float MID_FRACTION = 0.5f;
|
||||
|
||||
private static final String TAG = "SubripDecoder";
|
||||
private static final String TAG = "SubripParser";
|
||||
|
||||
// Some SRT files don't include hours or milliseconds in the timecode, so we use optional groups.
|
||||
private static final String SUBRIP_TIMECODE = "(?:(\\d+):)?(\\d+):(\\d+)(?:,(\\d+))?";
|
||||
@ -66,18 +72,25 @@ public final class SubripDecoder extends SimpleSubtitleDecoder {
|
||||
|
||||
private final StringBuilder textBuilder;
|
||||
private final ArrayList<String> tags;
|
||||
private byte[] dataScratch = Util.EMPTY_BYTE_ARRAY;
|
||||
|
||||
public SubripDecoder() {
|
||||
super("SubripDecoder");
|
||||
public SubripParser() {
|
||||
textBuilder = new StringBuilder();
|
||||
tags = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected Subtitle decode(byte[] data, int length, boolean reset) {
|
||||
public ImmutableList<CuesWithTiming> parse(byte[] data, int offset, int length) {
|
||||
ArrayList<Cue> cues = new ArrayList<>();
|
||||
LongArray cueTimesUs = new LongArray();
|
||||
ParsableByteArray subripData = new ParsableByteArray(data, length);
|
||||
LongArray startTimesUs = new LongArray();
|
||||
|
||||
if (dataScratch.length < length) {
|
||||
dataScratch = new byte[length];
|
||||
}
|
||||
System.arraycopy(
|
||||
/* src= */ data, /* scrPos= */ offset, /* dest= */ dataScratch, /* destPos= */ 0, length);
|
||||
ParsableByteArray subripData = new ParsableByteArray(dataScratch, length);
|
||||
Charset charset = detectUtfCharset(subripData);
|
||||
|
||||
@Nullable String currentLine;
|
||||
@ -104,8 +117,8 @@ public final class SubripDecoder extends SimpleSubtitleDecoder {
|
||||
|
||||
Matcher matcher = SUBRIP_TIMING_LINE.matcher(currentLine);
|
||||
if (matcher.matches()) {
|
||||
cueTimesUs.add(parseTimecode(matcher, /* groupOffset= */ 1));
|
||||
cueTimesUs.add(parseTimecode(matcher, /* groupOffset= */ 6));
|
||||
startTimesUs.add(parseTimecode(matcher, /* groupOffset= */ 1));
|
||||
startTimesUs.add(parseTimecode(matcher, /* groupOffset= */ 6));
|
||||
} else {
|
||||
Log.w(TAG, "Skipping invalid timing: " + currentLine);
|
||||
continue;
|
||||
@ -138,11 +151,22 @@ public final class SubripDecoder extends SimpleSubtitleDecoder {
|
||||
cues.add(Cue.EMPTY);
|
||||
}
|
||||
|
||||
Cue[] cuesArray = cues.toArray(new Cue[0]);
|
||||
long[] cueTimesUsArray = cueTimesUs.toArray();
|
||||
return new SubripSubtitle(cuesArray, cueTimesUsArray);
|
||||
ImmutableList.Builder<CuesWithTiming> cuesWithStartTimeAndDuration = ImmutableList.builder();
|
||||
for (int i = 0; i < cues.size(); i++) {
|
||||
cuesWithStartTimeAndDuration.add(
|
||||
new CuesWithTiming(
|
||||
/* cues= */ ImmutableList.of(cues.get(i)),
|
||||
/* startTimeUs= */ startTimesUs.get(i),
|
||||
/* durationUs= */ i == cues.size() - 1
|
||||
? C.TIME_UNSET
|
||||
: startTimesUs.get(i + 1) - startTimesUs.get(i)));
|
||||
}
|
||||
return cuesWithStartTimeAndDuration.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {}
|
||||
|
||||
/**
|
||||
* Determine UTF encoding of the byte array from a byte order mark (BOM), defaulting to UTF-8 if
|
||||
* no BOM is found.
|
||||
@ -248,14 +272,17 @@ public final class SubripDecoder extends SimpleSubtitleDecoder {
|
||||
return timestampMs * 1000;
|
||||
}
|
||||
|
||||
/* package */ static float getFractionalPositionForAnchorType(@Cue.AnchorType int anchorType) {
|
||||
// TODO(b/289983417): Make package-private again, once it is no longer needed in
|
||||
// DelegatingSubtitleDecoderWithSubripParserTest.java (i.e. legacy subtitle flow is removed)
|
||||
@VisibleForTesting(otherwise = PRIVATE)
|
||||
public static float getFractionalPositionForAnchorType(@Cue.AnchorType int anchorType) {
|
||||
switch (anchorType) {
|
||||
case Cue.ANCHOR_TYPE_START:
|
||||
return SubripDecoder.START_FRACTION;
|
||||
return START_FRACTION;
|
||||
case Cue.ANCHOR_TYPE_MIDDLE:
|
||||
return SubripDecoder.MID_FRACTION;
|
||||
return MID_FRACTION;
|
||||
case Cue.ANCHOR_TYPE_END:
|
||||
return SubripDecoder.END_FRACTION;
|
||||
return END_FRACTION;
|
||||
case Cue.TYPE_UNSET:
|
||||
default:
|
||||
// Should never happen.
|
@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.extractor.text.subrip;
|
||||
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.text.Cue;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.extractor.text.Subtitle;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/** A representation of a SubRip subtitle. */
|
||||
/* package */ final class SubripSubtitle implements Subtitle {
|
||||
|
||||
private final Cue[] cues;
|
||||
private final long[] cueTimesUs;
|
||||
|
||||
/**
|
||||
* @param cues The cues in the subtitle.
|
||||
* @param cueTimesUs The cue times, in microseconds.
|
||||
*/
|
||||
public SubripSubtitle(Cue[] cues, long[] cueTimesUs) {
|
||||
this.cues = cues;
|
||||
this.cueTimesUs = cueTimesUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNextEventTimeIndex(long timeUs) {
|
||||
int index = Util.binarySearchCeil(cueTimesUs, timeUs, false, false);
|
||||
return index < cueTimesUs.length ? index : C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEventTimeCount() {
|
||||
return cueTimesUs.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getEventTime(int index) {
|
||||
Assertions.checkArgument(index >= 0);
|
||||
Assertions.checkArgument(index < cueTimesUs.length);
|
||||
return cueTimesUs[index];
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Cue> getCues(long timeUs) {
|
||||
int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false);
|
||||
if (index == -1 || cues[index] == Cue.EMPTY) {
|
||||
// timeUs is earlier than the start of the first cue, or we have an empty cue.
|
||||
return Collections.emptyList();
|
||||
} else {
|
||||
return Collections.singletonList(cues[index]);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,282 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.extractor.text.subrip;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import androidx.media3.common.text.Cue;
|
||||
import androidx.media3.extractor.text.CuesWithTiming;
|
||||
import androidx.media3.test.utils.TestUtil;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Unit test for {@link SubripParser}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class SubripParserTest {
|
||||
|
||||
private static final String EMPTY_FILE = "media/subrip/empty";
|
||||
private static final String TYPICAL_FILE = "media/subrip/typical";
|
||||
private static final String TYPICAL_WITH_BYTE_ORDER_MARK =
|
||||
"media/subrip/typical_with_byte_order_mark";
|
||||
private static final String TYPICAL_EXTRA_BLANK_LINE = "media/subrip/typical_extra_blank_line";
|
||||
private static final String TYPICAL_MISSING_TIMECODE = "media/subrip/typical_missing_timecode";
|
||||
private static final String TYPICAL_MISSING_SEQUENCE = "media/subrip/typical_missing_sequence";
|
||||
private static final String TYPICAL_NEGATIVE_TIMESTAMPS =
|
||||
"media/subrip/typical_negative_timestamps";
|
||||
private static final String TYPICAL_UNEXPECTED_END = "media/subrip/typical_unexpected_end";
|
||||
private static final String TYPICAL_UTF16BE = "media/subrip/typical_utf16be";
|
||||
private static final String TYPICAL_UTF16LE = "media/subrip/typical_utf16le";
|
||||
private static final String TYPICAL_WITH_TAGS = "media/subrip/typical_with_tags";
|
||||
private static final String TYPICAL_NO_HOURS_AND_MILLIS =
|
||||
"media/subrip/typical_no_hours_and_millis";
|
||||
|
||||
@Test
|
||||
public void parseEmpty() throws IOException {
|
||||
SubripParser parser = new SubripParser();
|
||||
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), EMPTY_FILE);
|
||||
|
||||
List<CuesWithTiming> allCues = parser.parse(bytes);
|
||||
|
||||
assertThat(allCues).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseTypical() throws IOException {
|
||||
SubripParser parser = new SubripParser();
|
||||
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_FILE);
|
||||
|
||||
List<CuesWithTiming> allCues = parser.parse(bytes);
|
||||
|
||||
assertThat(allCues).hasSize(6);
|
||||
assertTypicalCue1(allCues.get(0));
|
||||
assertTypicalCue2(allCues.get(2));
|
||||
assertTypicalCue3(allCues.get(4));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseTypicalAtOffsetAndRestrictedLength() throws IOException {
|
||||
SubripParser parser = new SubripParser();
|
||||
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_FILE);
|
||||
|
||||
ImmutableList<CuesWithTiming> allCues = parser.parse(bytes, 10, bytes.length - 15);
|
||||
|
||||
assertThat(allCues).hasSize(4);
|
||||
// Because of the offset, we skip the first line of dialogue
|
||||
assertTypicalCue2(allCues.get(0));
|
||||
// Because of the length restriction, we only partially parse the third line of dialogue
|
||||
Cue thirdCue = allCues.get(2).cues.get(0);
|
||||
assertThat(thirdCue.text.toString()).isEqualTo("This is the third subti");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseTypicalWithByteOrderMark() throws IOException {
|
||||
SubripParser parser = new SubripParser();
|
||||
byte[] bytes =
|
||||
TestUtil.getByteArray(
|
||||
ApplicationProvider.getApplicationContext(), TYPICAL_WITH_BYTE_ORDER_MARK);
|
||||
|
||||
List<CuesWithTiming> allCues = parser.parse(bytes);
|
||||
|
||||
assertThat(allCues).hasSize(6);
|
||||
assertTypicalCue1(allCues.get(0));
|
||||
assertTypicalCue2(allCues.get(2));
|
||||
assertTypicalCue3(allCues.get(4));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseTypicalExtraBlankLine() throws IOException {
|
||||
SubripParser parser = new SubripParser();
|
||||
byte[] bytes =
|
||||
TestUtil.getByteArray(
|
||||
ApplicationProvider.getApplicationContext(), TYPICAL_EXTRA_BLANK_LINE);
|
||||
|
||||
List<CuesWithTiming> allCues = parser.parse(bytes);
|
||||
|
||||
assertThat(allCues).hasSize(6);
|
||||
assertTypicalCue1(allCues.get(0));
|
||||
assertTypicalCue2(allCues.get(2));
|
||||
assertTypicalCue3(allCues.get(4));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseTypicalMissingTimecode() throws IOException {
|
||||
// Parsing should succeed, parsing the first and third cues only.
|
||||
SubripParser parser = new SubripParser();
|
||||
byte[] bytes =
|
||||
TestUtil.getByteArray(
|
||||
ApplicationProvider.getApplicationContext(), TYPICAL_MISSING_TIMECODE);
|
||||
|
||||
List<CuesWithTiming> allCues = parser.parse(bytes);
|
||||
|
||||
assertThat(allCues).hasSize(4);
|
||||
assertTypicalCue1(allCues.get(0));
|
||||
assertTypicalCue3(allCues.get(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseTypicalMissingSequence() throws IOException {
|
||||
// Parsing should succeed, parsing the first and third cues only.
|
||||
SubripParser parser = new SubripParser();
|
||||
byte[] bytes =
|
||||
TestUtil.getByteArray(
|
||||
ApplicationProvider.getApplicationContext(), TYPICAL_MISSING_SEQUENCE);
|
||||
|
||||
List<CuesWithTiming> allCues = parser.parse(bytes);
|
||||
|
||||
assertThat(allCues).hasSize(4);
|
||||
assertTypicalCue1(allCues.get(0));
|
||||
assertTypicalCue3(allCues.get(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseTypicalNegativeTimestamps() throws IOException {
|
||||
// Parsing should succeed, parsing the third cue only.
|
||||
SubripParser parser = new SubripParser();
|
||||
byte[] bytes =
|
||||
TestUtil.getByteArray(
|
||||
ApplicationProvider.getApplicationContext(), TYPICAL_NEGATIVE_TIMESTAMPS);
|
||||
|
||||
List<CuesWithTiming> allCues = parser.parse(bytes);
|
||||
|
||||
assertThat(allCues).hasSize(2);
|
||||
assertTypicalCue3(allCues.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseTypicalUnexpectedEnd() throws IOException {
|
||||
// Parsing should succeed, parsing the first and second cues only.
|
||||
SubripParser parser = new SubripParser();
|
||||
byte[] bytes =
|
||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_UNEXPECTED_END);
|
||||
|
||||
List<CuesWithTiming> allCues = parser.parse(bytes);
|
||||
|
||||
assertThat(allCues).hasSize(4);
|
||||
assertTypicalCue1(allCues.get(0));
|
||||
assertTypicalCue2(allCues.get(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseTypicalUtf16LittleEndian() throws IOException {
|
||||
SubripParser parser = new SubripParser();
|
||||
byte[] bytes =
|
||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_UTF16LE);
|
||||
|
||||
List<CuesWithTiming> allCues = parser.parse(bytes);
|
||||
|
||||
assertThat(allCues).hasSize(6);
|
||||
assertTypicalCue1(allCues.get(0));
|
||||
assertTypicalCue2(allCues.get(2));
|
||||
assertTypicalCue3(allCues.get(4));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseTypicalUtf16BigEndian() throws IOException {
|
||||
SubripParser parser = new SubripParser();
|
||||
byte[] bytes =
|
||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_UTF16BE);
|
||||
|
||||
List<CuesWithTiming> allCues = parser.parse(bytes);
|
||||
|
||||
assertThat(allCues).hasSize(6);
|
||||
assertTypicalCue1(allCues.get(0));
|
||||
assertTypicalCue2(allCues.get(2));
|
||||
assertTypicalCue3(allCues.get(4));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseCueWithTag() throws IOException {
|
||||
SubripParser parser = new SubripParser();
|
||||
byte[] bytes =
|
||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_WITH_TAGS);
|
||||
|
||||
List<CuesWithTiming> allCues = parser.parse(bytes);
|
||||
|
||||
assertThat(allCues).isNotNull();
|
||||
assertThat(allCues.get(0).cues.get(0).text.toString()).isEqualTo("This is the first subtitle.");
|
||||
assertThat(allCues.get(2).cues.get(0).text.toString())
|
||||
.isEqualTo("This is the second subtitle.\nSecond subtitle with second line.");
|
||||
assertThat(allCues.get(4).cues.get(0).text.toString()).isEqualTo("This is the third subtitle.");
|
||||
assertThat(allCues.get(6).cues.get(0).text.toString())
|
||||
.isEqualTo("This { \\an2} is not a valid tag due to the space after the opening bracket.");
|
||||
assertThat(allCues.get(8).cues.get(0).text.toString())
|
||||
.isEqualTo("This is the fifth subtitle with multiple valid tags.");
|
||||
assertAlignmentCue(allCues.get(10), Cue.ANCHOR_TYPE_END, Cue.ANCHOR_TYPE_START); // {/an1}
|
||||
assertAlignmentCue(allCues.get(12), Cue.ANCHOR_TYPE_END, Cue.ANCHOR_TYPE_MIDDLE); // {/an2}
|
||||
assertAlignmentCue(allCues.get(14), Cue.ANCHOR_TYPE_END, Cue.ANCHOR_TYPE_END); // {/an3}
|
||||
assertAlignmentCue(allCues.get(16), Cue.ANCHOR_TYPE_MIDDLE, Cue.ANCHOR_TYPE_START); // {/an4}
|
||||
assertAlignmentCue(allCues.get(18), Cue.ANCHOR_TYPE_MIDDLE, Cue.ANCHOR_TYPE_MIDDLE); // {/an5}
|
||||
assertAlignmentCue(allCues.get(20), Cue.ANCHOR_TYPE_MIDDLE, Cue.ANCHOR_TYPE_END); // {/an6}
|
||||
assertAlignmentCue(allCues.get(22), Cue.ANCHOR_TYPE_START, Cue.ANCHOR_TYPE_START); // {/an7}
|
||||
assertAlignmentCue(allCues.get(24), Cue.ANCHOR_TYPE_START, Cue.ANCHOR_TYPE_MIDDLE); // {/an8}
|
||||
assertAlignmentCue(allCues.get(26), Cue.ANCHOR_TYPE_START, Cue.ANCHOR_TYPE_END); // {/an9}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseTypicalNoHoursAndMillis() throws IOException {
|
||||
SubripParser parser = new SubripParser();
|
||||
byte[] bytes =
|
||||
TestUtil.getByteArray(
|
||||
ApplicationProvider.getApplicationContext(), TYPICAL_NO_HOURS_AND_MILLIS);
|
||||
|
||||
List<CuesWithTiming> allCues = parser.parse(bytes);
|
||||
|
||||
assertThat(allCues).hasSize(6);
|
||||
assertTypicalCue1(allCues.get(0));
|
||||
assertThat(allCues.get(2).startTimeUs).isEqualTo(2_000_000);
|
||||
assertThat(allCues.get(3).startTimeUs).isEqualTo(3_000_000);
|
||||
assertTypicalCue3(allCues.get(4));
|
||||
}
|
||||
|
||||
private static void assertTypicalCue1(CuesWithTiming cuesWithTiming) {
|
||||
assertThat(cuesWithTiming.startTimeUs).isEqualTo(0);
|
||||
assertThat(cuesWithTiming.cues.get(0).text.toString()).isEqualTo("This is the first subtitle.");
|
||||
assertThat(cuesWithTiming.durationUs).isEqualTo(1234000);
|
||||
}
|
||||
|
||||
private static void assertTypicalCue2(CuesWithTiming cuesWithTiming) {
|
||||
assertThat(cuesWithTiming.startTimeUs).isEqualTo(2345000);
|
||||
assertThat(cuesWithTiming.cues.get(0).text.toString())
|
||||
.isEqualTo("This is the second subtitle.\nSecond subtitle with second line.");
|
||||
assertThat(cuesWithTiming.durationUs).isEqualTo(3456000 - 2345000);
|
||||
}
|
||||
|
||||
private static void assertTypicalCue3(CuesWithTiming cuesWithTiming) {
|
||||
long expectedStartTimeUs = (((2L * 60L * 60L) + 4L) * 1000L + 567L) * 1000L;
|
||||
assertThat(cuesWithTiming.startTimeUs).isEqualTo(expectedStartTimeUs);
|
||||
assertThat(cuesWithTiming.cues.get(0).text.toString()).isEqualTo("This is the third subtitle.");
|
||||
long expectedEndTimeUs = (((2L * 60L * 60L) + 8L) * 1000L + 901L) * 1000L;
|
||||
assertThat(cuesWithTiming.durationUs).isEqualTo(expectedEndTimeUs - expectedStartTimeUs);
|
||||
}
|
||||
|
||||
private static void assertAlignmentCue(
|
||||
CuesWithTiming cuesWithTiming,
|
||||
@Cue.AnchorType int lineAnchor,
|
||||
@Cue.AnchorType int positionAnchor) {
|
||||
Cue cue = cuesWithTiming.cues.get(0);
|
||||
assertThat(cue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION);
|
||||
assertThat(cue.lineAnchor).isEqualTo(lineAnchor);
|
||||
assertThat(cue.line).isEqualTo(SubripParser.getFractionalPositionForAnchorType(lineAnchor));
|
||||
assertThat(cue.positionAnchor).isEqualTo(positionAnchor);
|
||||
assertThat(cue.position)
|
||||
.isEqualTo(SubripParser.getFractionalPositionForAnchorType(positionAnchor));
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user