WebvttParser
implementation - moved from WebvttDecoder
`WebvttDecoder` which used to be `SimpleSubtitleDecoder` will now be called `WebvttParser` and implement `SubtitleParser` interface. For backwards compatibility, we will have the same functionality provided by `DelegatingSubtitleDecoder` backed-up by a new `WebvttParser` instance. `WebvttSubtitle` will still be used behind the scenes to handle overlapping `Cues`. PiperOrigin-RevId: 549298733
This commit is contained in:
parent
5d453fcf37
commit
f0f24aa0d4
@ -15,7 +15,10 @@
|
||||
*/
|
||||
package androidx.media3.exoplayer.text;
|
||||
|
||||
import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.media3.extractor.text.CuesWithTiming;
|
||||
import androidx.media3.extractor.text.SimpleSubtitleDecoder;
|
||||
import androidx.media3.extractor.text.Subtitle;
|
||||
@ -43,17 +46,19 @@ import java.util.List;
|
||||
* <li>DelegatingSubtitleDecoder("XXX", new XXXParser(initializationData))
|
||||
* <li>XXXDecoder(initializationData)
|
||||
* </ul>
|
||||
*
|
||||
* <p>TODO(b/289983417): this will only be used in the old decoding flow (Decoder after SampleQueue)
|
||||
* while we maintain dual architecture. Once we fully migrate to the pre-SampleQueue flow, it can be
|
||||
* deprecated and later deleted.
|
||||
*/
|
||||
/* package */ final class DelegatingSubtitleDecoder extends SimpleSubtitleDecoder {
|
||||
// TODO(b/289983417): this will only be used in the old decoding flow (Decoder after SampleQueue)
|
||||
// while we maintain dual architecture. Once we fully migrate to the pre-SampleQueue flow, it can be
|
||||
// deprecated and later deleted.
|
||||
// TODO: remove VisibleForTesting annotation once SubtitleExtractor uses SubtitleParser (rather than
|
||||
// SubtitleDecoder) and SubtitleExtractorTest is refactored
|
||||
@VisibleForTesting(otherwise = PACKAGE_PRIVATE)
|
||||
public final class DelegatingSubtitleDecoder extends SimpleSubtitleDecoder {
|
||||
|
||||
private static final Subtitle EMPTY_SUBTITLE = new CuesWithTimingSubtitle(ImmutableList.of());
|
||||
private final SubtitleParser subtitleParser;
|
||||
|
||||
/* package */ DelegatingSubtitleDecoder(String name, SubtitleParser subtitleParser) {
|
||||
public DelegatingSubtitleDecoder(String name, SubtitleParser subtitleParser) {
|
||||
super(name);
|
||||
this.subtitleParser = subtitleParser;
|
||||
}
|
||||
@ -63,7 +68,8 @@ import java.util.List;
|
||||
if (reset) {
|
||||
subtitleParser.reset();
|
||||
}
|
||||
@Nullable List<CuesWithTiming> cuesWithTiming = subtitleParser.parse(data);
|
||||
@Nullable
|
||||
List<CuesWithTiming> cuesWithTiming = subtitleParser.parse(data, /* offset= */ 0, length);
|
||||
if (cuesWithTiming == null) {
|
||||
return EMPTY_SUBTITLE;
|
||||
}
|
||||
|
@ -25,7 +25,6 @@ import androidx.media3.extractor.text.SubtitleParser;
|
||||
import androidx.media3.extractor.text.cea.Cea608Decoder;
|
||||
import androidx.media3.extractor.text.cea.Cea708Decoder;
|
||||
import androidx.media3.extractor.text.ttml.TtmlDecoder;
|
||||
import androidx.media3.extractor.text.webvtt.WebvttDecoder;
|
||||
import java.util.Objects;
|
||||
|
||||
/** A factory for {@link SubtitleDecoder} instances. */
|
||||
@ -56,7 +55,6 @@ public interface SubtitleDecoderFactory {
|
||||
* <p>Supports formats supported by {@link DefaultSubtitleParserFactory} as well as the following:
|
||||
*
|
||||
* <ul>
|
||||
* <li>WebVTT ({@link WebvttDecoder})
|
||||
* <li>TTML ({@link TtmlDecoder})
|
||||
* <li>Cea608 ({@link Cea608Decoder})
|
||||
* <li>Cea708 ({@link Cea708Decoder})
|
||||
@ -72,7 +70,6 @@ public interface SubtitleDecoderFactory {
|
||||
public boolean supportsFormat(Format format) {
|
||||
@Nullable String mimeType = format.sampleMimeType;
|
||||
return delegate.supportsFormat(format)
|
||||
|| Objects.equals(mimeType, MimeTypes.TEXT_VTT)
|
||||
|| Objects.equals(mimeType, MimeTypes.APPLICATION_TTML)
|
||||
|| Objects.equals(mimeType, MimeTypes.APPLICATION_CEA608)
|
||||
|| Objects.equals(mimeType, MimeTypes.APPLICATION_MP4CEA608)
|
||||
@ -90,8 +87,6 @@ public interface SubtitleDecoderFactory {
|
||||
@Nullable String mimeType = format.sampleMimeType;
|
||||
if (mimeType != null) {
|
||||
switch (mimeType) {
|
||||
case MimeTypes.TEXT_VTT:
|
||||
return new WebvttDecoder();
|
||||
case MimeTypes.APPLICATION_TTML:
|
||||
return new TtmlDecoder();
|
||||
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,11 +13,11 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.extractor.text.webvtt;
|
||||
package androidx.media3.exoplayer.text;
|
||||
|
||||
import static androidx.media3.test.utils.truth.SpannedSubject.assertThat;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
import android.text.Layout.Alignment;
|
||||
import android.text.Spanned;
|
||||
@ -25,7 +25,8 @@ import androidx.media3.common.text.Cue;
|
||||
import androidx.media3.common.text.TextAnnotation;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.ColorParser;
|
||||
import androidx.media3.extractor.text.SubtitleDecoderException;
|
||||
import androidx.media3.extractor.text.Subtitle;
|
||||
import androidx.media3.extractor.text.webvtt.WebvttParser;
|
||||
import androidx.media3.test.utils.TestUtil;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
@ -37,10 +38,9 @@ import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Unit test for {@link WebvttDecoder}. */
|
||||
/** Unit test for a {@link DelegatingSubtitleDecoder} backed by {@link WebvttParser}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class WebvttDecoderTest {
|
||||
|
||||
public final class DelegatingSubtitleDecoderWithWebvttParserTest {
|
||||
private static final String TYPICAL_FILE = "media/webvtt/typical";
|
||||
private static final String TYPICAL_WITH_BAD_TIMESTAMPS =
|
||||
"media/webvtt/typical_with_bad_timestamps";
|
||||
@ -61,32 +61,27 @@ public class WebvttDecoderTest {
|
||||
"media/webvtt/with_css_text_combine_upright";
|
||||
private static final String WITH_BOM = "media/webvtt/with_bom";
|
||||
private static final String EMPTY_FILE = "media/webvtt/empty";
|
||||
|
||||
@Rule public final Expect expect = Expect.create();
|
||||
|
||||
@Test
|
||||
public void decodeEmpty() throws IOException {
|
||||
WebvttDecoder decoder = new WebvttDecoder();
|
||||
DelegatingSubtitleDecoder decoder =
|
||||
new DelegatingSubtitleDecoder(
|
||||
"DelegatingSubtitleDecoderWithWebvttParser", new WebvttParser());
|
||||
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), EMPTY_FILE);
|
||||
try {
|
||||
decoder.decode(bytes, bytes.length, /* reset= */ false);
|
||||
fail();
|
||||
} catch (SubtitleDecoderException expected) {
|
||||
// Do nothing.
|
||||
}
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> decoder.decode(bytes, bytes.length, /* reset= */ false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeTypical() throws Exception {
|
||||
WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_FILE);
|
||||
|
||||
Subtitle subtitle = getSubtitleForTestAsset(TYPICAL_FILE);
|
||||
assertThat(subtitle.getEventTimeCount()).isEqualTo(4);
|
||||
|
||||
assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
|
||||
assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L);
|
||||
Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
|
||||
assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle.");
|
||||
|
||||
assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L);
|
||||
assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L);
|
||||
Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
|
||||
@ -95,15 +90,12 @@ public class WebvttDecoderTest {
|
||||
|
||||
@Test
|
||||
public void decodeWithBom() throws Exception {
|
||||
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_BOM);
|
||||
|
||||
Subtitle subtitle = getSubtitleForTestAsset(WITH_BOM);
|
||||
assertThat(subtitle.getEventTimeCount()).isEqualTo(4);
|
||||
|
||||
assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
|
||||
assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L);
|
||||
Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
|
||||
assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle.");
|
||||
|
||||
assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L);
|
||||
assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L);
|
||||
Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
|
||||
@ -112,15 +104,12 @@ public class WebvttDecoderTest {
|
||||
|
||||
@Test
|
||||
public void decodeTypicalWithBadTimestamps() throws Exception {
|
||||
WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_BAD_TIMESTAMPS);
|
||||
|
||||
Subtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_BAD_TIMESTAMPS);
|
||||
assertThat(subtitle.getEventTimeCount()).isEqualTo(4);
|
||||
|
||||
assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
|
||||
assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L);
|
||||
Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
|
||||
assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle.");
|
||||
|
||||
assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L);
|
||||
assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L);
|
||||
Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
|
||||
@ -129,15 +118,12 @@ public class WebvttDecoderTest {
|
||||
|
||||
@Test
|
||||
public void decodeTypicalWithIds() throws Exception {
|
||||
WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_IDS_FILE);
|
||||
|
||||
Subtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_IDS_FILE);
|
||||
assertThat(subtitle.getEventTimeCount()).isEqualTo(4);
|
||||
|
||||
assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
|
||||
assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L);
|
||||
Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
|
||||
assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle.");
|
||||
|
||||
assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L);
|
||||
assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L);
|
||||
Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
|
||||
@ -146,15 +132,12 @@ public class WebvttDecoderTest {
|
||||
|
||||
@Test
|
||||
public void decodeTypicalWithComments() throws Exception {
|
||||
WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_COMMENTS_FILE);
|
||||
|
||||
Subtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_COMMENTS_FILE);
|
||||
assertThat(subtitle.getEventTimeCount()).isEqualTo(4);
|
||||
|
||||
assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
|
||||
assertThat(subtitle.getEventTime(0 + 1)).isEqualTo(1_234_000L);
|
||||
Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
|
||||
assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle.");
|
||||
|
||||
assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L);
|
||||
assertThat(subtitle.getEventTime(2 + 1)).isEqualTo(3_456_000L);
|
||||
Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
|
||||
@ -163,25 +146,20 @@ public class WebvttDecoderTest {
|
||||
|
||||
@Test
|
||||
public void decodeWithTags() throws Exception {
|
||||
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_TAGS_FILE);
|
||||
|
||||
Subtitle subtitle = getSubtitleForTestAsset(WITH_TAGS_FILE);
|
||||
assertThat(subtitle.getEventTimeCount()).isEqualTo(8);
|
||||
|
||||
assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
|
||||
assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L);
|
||||
Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
|
||||
assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle.");
|
||||
|
||||
assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L);
|
||||
assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L);
|
||||
Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
|
||||
assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle.");
|
||||
|
||||
assertThat(subtitle.getEventTime(4)).isEqualTo(4_000_000L);
|
||||
assertThat(subtitle.getEventTime(5)).isEqualTo(5_000_000L);
|
||||
Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4)));
|
||||
assertThat(thirdCue.text.toString()).isEqualTo("This is the third subtitle.");
|
||||
|
||||
assertThat(subtitle.getEventTime(6)).isEqualTo(6_000_000L);
|
||||
assertThat(subtitle.getEventTime(7)).isEqualTo(7_000_000L);
|
||||
Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6)));
|
||||
@ -190,10 +168,8 @@ public class WebvttDecoderTest {
|
||||
|
||||
@Test
|
||||
public void decodeWithPositioning() throws Exception {
|
||||
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_POSITIONING_FILE);
|
||||
|
||||
Subtitle subtitle = getSubtitleForTestAsset(WITH_POSITIONING_FILE);
|
||||
assertThat(subtitle.getEventTimeCount()).isEqualTo(16);
|
||||
|
||||
assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
|
||||
assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L);
|
||||
Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
|
||||
@ -202,12 +178,10 @@ public class WebvttDecoderTest {
|
||||
assertThat(firstCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_END);
|
||||
assertThat(firstCue.textAlignment).isEqualTo(Alignment.ALIGN_NORMAL);
|
||||
assertThat(firstCue.size).isEqualTo(0.35f);
|
||||
|
||||
// Unspecified values should use WebVTT defaults
|
||||
assertThat(firstCue.line).isEqualTo(-1f);
|
||||
assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_NUMBER);
|
||||
assertThat(firstCue.verticalType).isEqualTo(Cue.TYPE_UNSET);
|
||||
|
||||
assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L);
|
||||
assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L);
|
||||
Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
|
||||
@ -215,7 +189,6 @@ public class WebvttDecoderTest {
|
||||
// Position is invalid so defaults to 0.5
|
||||
assertThat(secondCue.position).isEqualTo(0.5f);
|
||||
assertThat(secondCue.textAlignment).isEqualTo(Alignment.ALIGN_OPPOSITE);
|
||||
|
||||
assertThat(subtitle.getEventTime(4)).isEqualTo(4_000_000L);
|
||||
assertThat(subtitle.getEventTime(5)).isEqualTo(5_000_000L);
|
||||
Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4)));
|
||||
@ -226,7 +199,6 @@ public class WebvttDecoderTest {
|
||||
assertThat(thirdCue.textAlignment).isEqualTo(Alignment.ALIGN_CENTER);
|
||||
// Derived from `align:middle`:
|
||||
assertThat(thirdCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE);
|
||||
|
||||
assertThat(subtitle.getEventTime(6)).isEqualTo(6_000_000L);
|
||||
assertThat(subtitle.getEventTime(7)).isEqualTo(7_000_000L);
|
||||
Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6)));
|
||||
@ -237,7 +209,6 @@ public class WebvttDecoderTest {
|
||||
// Derived from `align:middle`:
|
||||
assertThat(fourthCue.position).isEqualTo(0.5f);
|
||||
assertThat(fourthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE);
|
||||
|
||||
assertThat(subtitle.getEventTime(8)).isEqualTo(8_000_000L);
|
||||
assertThat(subtitle.getEventTime(9)).isEqualTo(9_000_000L);
|
||||
Cue fifthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(8)));
|
||||
@ -246,7 +217,6 @@ public class WebvttDecoderTest {
|
||||
// Derived from `align:right`:
|
||||
assertThat(fifthCue.position).isEqualTo(1.0f);
|
||||
assertThat(fifthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_END);
|
||||
|
||||
assertThat(subtitle.getEventTime(10)).isEqualTo(10_000_000L);
|
||||
assertThat(subtitle.getEventTime(11)).isEqualTo(11_000_000L);
|
||||
Cue sixthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(10)));
|
||||
@ -255,13 +225,11 @@ public class WebvttDecoderTest {
|
||||
// Derived from `align:center`:
|
||||
assertThat(sixthCue.position).isEqualTo(0.5f);
|
||||
assertThat(sixthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE);
|
||||
|
||||
assertThat(subtitle.getEventTime(12)).isEqualTo(12_000_000L);
|
||||
assertThat(subtitle.getEventTime(13)).isEqualTo(13_000_000L);
|
||||
Cue seventhCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(12)));
|
||||
assertThat(seventhCue.text.toString()).isEqualTo("This is the seventh subtitle.");
|
||||
assertThat(seventhCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_START);
|
||||
|
||||
assertThat(subtitle.getEventTime(14)).isEqualTo(14_000_000L);
|
||||
assertThat(subtitle.getEventTime(15)).isEqualTo(15_000_000L);
|
||||
Cue eighthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(14)));
|
||||
@ -271,15 +239,12 @@ public class WebvttDecoderTest {
|
||||
|
||||
@Test
|
||||
public void decodeWithOverlappingTimestamps() throws Exception {
|
||||
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_OVERLAPPING_TIMESTAMPS_FILE);
|
||||
|
||||
Subtitle subtitle = getSubtitleForTestAsset(WITH_OVERLAPPING_TIMESTAMPS_FILE);
|
||||
assertThat(subtitle.getEventTimeCount()).isEqualTo(8);
|
||||
|
||||
Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
|
||||
assertThat(firstCue.text.toString()).isEqualTo("Displayed at the bottom for 3 seconds.");
|
||||
assertThat(firstCue.line).isEqualTo(-1f);
|
||||
assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_NUMBER);
|
||||
|
||||
List<Cue> firstAndSecondCue = subtitle.getCues(subtitle.getEventTime(1));
|
||||
assertThat(firstAndSecondCue).hasSize(2);
|
||||
assertThat(firstAndSecondCue.get(0).text.toString())
|
||||
@ -290,12 +255,10 @@ public class WebvttDecoderTest {
|
||||
.isEqualTo("Appears directly above for 1 second.");
|
||||
assertThat(firstAndSecondCue.get(1).line).isEqualTo(-2f);
|
||||
assertThat(firstAndSecondCue.get(1).lineType).isEqualTo(Cue.LINE_TYPE_NUMBER);
|
||||
|
||||
Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4)));
|
||||
assertThat(thirdCue.text.toString()).isEqualTo("Displayed at the bottom for 2 seconds.");
|
||||
assertThat(thirdCue.line).isEqualTo(-1f);
|
||||
assertThat(thirdCue.lineType).isEqualTo(Cue.LINE_TYPE_NUMBER);
|
||||
|
||||
List<Cue> thirdAndFourthCue = subtitle.getCues(subtitle.getEventTime(5));
|
||||
assertThat(thirdAndFourthCue).hasSize(2);
|
||||
assertThat(thirdAndFourthCue.get(0).text.toString())
|
||||
@ -306,7 +269,6 @@ public class WebvttDecoderTest {
|
||||
.isEqualTo("Appears directly above the previous cue, then replaces it after 1 second.");
|
||||
assertThat(thirdAndFourthCue.get(1).line).isEqualTo(-2f);
|
||||
assertThat(thirdAndFourthCue.get(1).lineType).isEqualTo(Cue.LINE_TYPE_NUMBER);
|
||||
|
||||
Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6)));
|
||||
assertThat(fourthCue.text.toString())
|
||||
.isEqualTo("Appears directly above the previous cue, then replaces it after 1 second.");
|
||||
@ -316,22 +278,18 @@ public class WebvttDecoderTest {
|
||||
|
||||
@Test
|
||||
public void decodeWithVertical() throws Exception {
|
||||
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_VERTICAL_FILE);
|
||||
|
||||
Subtitle subtitle = getSubtitleForTestAsset(WITH_VERTICAL_FILE);
|
||||
assertThat(subtitle.getEventTimeCount()).isEqualTo(6);
|
||||
|
||||
assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
|
||||
assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L);
|
||||
Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
|
||||
assertThat(firstCue.text.toString()).isEqualTo("Vertical right-to-left (e.g. Japanese)");
|
||||
assertThat(firstCue.verticalType).isEqualTo(Cue.VERTICAL_TYPE_RL);
|
||||
|
||||
assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L);
|
||||
assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L);
|
||||
Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
|
||||
assertThat(secondCue.text.toString()).isEqualTo("Vertical left-to-right (e.g. Mongolian)");
|
||||
assertThat(secondCue.verticalType).isEqualTo(Cue.VERTICAL_TYPE_LR);
|
||||
|
||||
assertThat(subtitle.getEventTime(4)).isEqualTo(4_000_000L);
|
||||
assertThat(subtitle.getEventTime(5)).isEqualTo(5_000_000L);
|
||||
Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4)));
|
||||
@ -341,17 +299,14 @@ public class WebvttDecoderTest {
|
||||
|
||||
@Test
|
||||
public void decodeWithRubies() throws Exception {
|
||||
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_RUBIES_FILE);
|
||||
|
||||
Subtitle subtitle = getSubtitleForTestAsset(WITH_RUBIES_FILE);
|
||||
assertThat(subtitle.getEventTimeCount()).isEqualTo(8);
|
||||
|
||||
// Check that an explicit `over` position is read from CSS.
|
||||
Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
|
||||
assertThat(firstCue.text.toString()).isEqualTo("Some text with over-ruby.");
|
||||
assertThat((Spanned) firstCue.text)
|
||||
.hasRubySpanBetween("Some ".length(), "Some text with over-ruby".length())
|
||||
.withTextAndPosition("over", TextAnnotation.POSITION_BEFORE);
|
||||
|
||||
// Check that `under` is read from CSS and unspecified defaults to `over`.
|
||||
Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
|
||||
assertThat(secondCue.text.toString())
|
||||
@ -364,7 +319,6 @@ public class WebvttDecoderTest {
|
||||
"Some text with under-ruby and ".length(),
|
||||
"Some text with under-ruby and over-ruby (default)".length())
|
||||
.withTextAndPosition("over", TextAnnotation.POSITION_BEFORE);
|
||||
|
||||
// Check many <rt> tags with different positions nested in a single <ruby> span.
|
||||
Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4)));
|
||||
assertThat(thirdCue.text.toString()).isEqualTo("base1base2base3.");
|
||||
@ -377,7 +331,6 @@ public class WebvttDecoderTest {
|
||||
assertThat((Spanned) thirdCue.text)
|
||||
.hasRubySpanBetween("base1base2".length(), "base1base2base3".length())
|
||||
.withTextAndPosition("under3", TextAnnotation.POSITION_AFTER);
|
||||
|
||||
// Check a <ruby> span with no <rt> tags.
|
||||
Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6)));
|
||||
assertThat(fourthCue.text.toString()).isEqualTo("Some text with no ruby text.");
|
||||
@ -386,15 +339,12 @@ public class WebvttDecoderTest {
|
||||
|
||||
@Test
|
||||
public void decodeWithBadCueHeader() throws Exception {
|
||||
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_BAD_CUE_HEADER_FILE);
|
||||
|
||||
Subtitle subtitle = getSubtitleForTestAsset(WITH_BAD_CUE_HEADER_FILE);
|
||||
assertThat(subtitle.getEventTimeCount()).isEqualTo(4);
|
||||
|
||||
assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
|
||||
assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L);
|
||||
Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
|
||||
assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle.");
|
||||
|
||||
assertThat(subtitle.getEventTime(2)).isEqualTo(4_000_000L);
|
||||
assertThat(subtitle.getEventTime(3)).isEqualTo(5_000_000L);
|
||||
Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
|
||||
@ -403,10 +353,8 @@ public class WebvttDecoderTest {
|
||||
|
||||
@Test
|
||||
public void decodeWithCssFontSizeStyle() throws Exception {
|
||||
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_FONT_SIZE);
|
||||
|
||||
Subtitle subtitle = getSubtitleForTestAsset(WITH_FONT_SIZE);
|
||||
assertThat(subtitle.getEventTimeCount()).isEqualTo(12);
|
||||
|
||||
assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
|
||||
assertThat(subtitle.getEventTime(1)).isEqualTo(2_000_000L);
|
||||
Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
|
||||
@ -414,13 +362,11 @@ public class WebvttDecoderTest {
|
||||
assertThat((Spanned) firstCue.text)
|
||||
.hasRelativeSizeSpanBetween(0, "Sentence with font-size set to 4.4em.".length())
|
||||
.withSizeChange(4.4f);
|
||||
|
||||
assertThat(subtitle.getEventTime(2)).isEqualTo(2_100_000L);
|
||||
assertThat(subtitle.getEventTime(3)).isEqualTo(2_400_000L);
|
||||
Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
|
||||
assertThat(secondCue.text.toString()).isEqualTo("Sentence with bad font-size unit.");
|
||||
assertThat((Spanned) secondCue.text).hasNoSpans();
|
||||
|
||||
assertThat(subtitle.getEventTime(4)).isEqualTo(2_500_000L);
|
||||
assertThat(subtitle.getEventTime(5)).isEqualTo(4_000_000L);
|
||||
Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4)));
|
||||
@ -428,7 +374,6 @@ public class WebvttDecoderTest {
|
||||
assertThat((Spanned) thirdCue.text)
|
||||
.hasAbsoluteSizeSpanBetween(0, "Absolute font-size expressed in px unit!".length())
|
||||
.withAbsoluteSize(2);
|
||||
|
||||
assertThat(subtitle.getEventTime(6)).isEqualTo(4_500_000L);
|
||||
assertThat(subtitle.getEventTime(7)).isEqualTo(6_000_000L);
|
||||
Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6)));
|
||||
@ -436,13 +381,11 @@ public class WebvttDecoderTest {
|
||||
assertThat((Spanned) fourthCue.text)
|
||||
.hasRelativeSizeSpanBetween(0, "Relative font-size expressed in % unit!".length())
|
||||
.withSizeChange(0.035f);
|
||||
|
||||
assertThat(subtitle.getEventTime(8)).isEqualTo(6_100_000L);
|
||||
assertThat(subtitle.getEventTime(9)).isEqualTo(6_400_000L);
|
||||
Cue fifthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(8)));
|
||||
assertThat(fifthCue.text.toString()).isEqualTo("Sentence with bad font-size value.");
|
||||
assertThat((Spanned) secondCue.text).hasNoSpans();
|
||||
|
||||
assertThat(subtitle.getEventTime(10)).isEqualTo(6_500_000L);
|
||||
assertThat(subtitle.getEventTime(11)).isEqualTo(8_000_000L);
|
||||
Cue sixthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(10)));
|
||||
@ -455,8 +398,7 @@ public class WebvttDecoderTest {
|
||||
|
||||
@Test
|
||||
public void webvttWithCssStyle() throws Exception {
|
||||
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_STYLES);
|
||||
|
||||
Subtitle subtitle = getSubtitleForTestAsset(WITH_CSS_STYLES);
|
||||
Spanned firstCueText = getUniqueSpanTextAt(subtitle, 0);
|
||||
assertThat(firstCueText.toString()).isEqualTo("This is the first subtitle.");
|
||||
assertThat(firstCueText)
|
||||
@ -465,17 +407,14 @@ public class WebvttDecoderTest {
|
||||
assertThat(firstCueText)
|
||||
.hasBackgroundColorSpanBetween(0, firstCueText.length())
|
||||
.withColor(ColorParser.parseCssColor("green"));
|
||||
|
||||
Spanned secondCueText = getUniqueSpanTextAt(subtitle, 2_345_000);
|
||||
assertThat(secondCueText.toString()).isEqualTo("This is the second subtitle.");
|
||||
assertThat(secondCueText)
|
||||
.hasForegroundColorSpanBetween(0, secondCueText.length())
|
||||
.withColor(ColorParser.parseCssColor("peachpuff"));
|
||||
|
||||
Spanned thirdCueText = getUniqueSpanTextAt(subtitle, 20_000_000);
|
||||
assertThat(thirdCueText.toString()).isEqualTo("This is a reference by element");
|
||||
assertThat(thirdCueText).hasUnderlineSpanBetween("This is a ".length(), thirdCueText.length());
|
||||
|
||||
Spanned fourthCueText = getUniqueSpanTextAt(subtitle, 25_000_000);
|
||||
assertThat(fourthCueText.toString()).isEqualTo("You are an idiot\nYou don't have the guts");
|
||||
assertThat(fourthCueText)
|
||||
@ -487,7 +426,7 @@ public class WebvttDecoderTest {
|
||||
|
||||
@Test
|
||||
public void withComplexCssSelectors() throws Exception {
|
||||
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_COMPLEX_SELECTORS);
|
||||
Subtitle subtitle = getSubtitleForTestAsset(WITH_CSS_COMPLEX_SELECTORS);
|
||||
Spanned firstCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 0);
|
||||
assertThat(firstCueText).hasUnderlineSpanBetween(0, firstCueText.length());
|
||||
assertThat(firstCueText)
|
||||
@ -497,26 +436,22 @@ public class WebvttDecoderTest {
|
||||
assertThat(firstCueText)
|
||||
.hasTypefaceSpanBetween("This should be underlined and ".length(), firstCueText.length())
|
||||
.withFamily("courier");
|
||||
|
||||
Spanned secondCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2_000_000);
|
||||
assertThat(secondCueText)
|
||||
.hasTypefaceSpanBetween("This ".length(), secondCueText.length())
|
||||
.withFamily("courier");
|
||||
assertThat(secondCueText)
|
||||
.hasNoForegroundColorSpanBetween("This ".length(), secondCueText.length());
|
||||
|
||||
Spanned thirdCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2_500_000);
|
||||
assertThat(thirdCueText).hasBoldSpanBetween("This ".length(), thirdCueText.length());
|
||||
assertThat(thirdCueText)
|
||||
.hasTypefaceSpanBetween("This ".length(), thirdCueText.length())
|
||||
.withFamily("courier");
|
||||
|
||||
Spanned fourthCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 4_000_000);
|
||||
assertThat(fourthCueText)
|
||||
.hasNoStyleSpanBetween("This ".length(), "shouldn't be bold.".length());
|
||||
assertThat(fourthCueText)
|
||||
.hasBoldSpanBetween("This shouldn't be bold.\nThis ".length(), fourthCueText.length());
|
||||
|
||||
Spanned fifthCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 5_000_000);
|
||||
assertThat(fifthCueText)
|
||||
.hasNoStyleSpanBetween("This is ".length(), "This is specific".length());
|
||||
@ -526,26 +461,25 @@ public class WebvttDecoderTest {
|
||||
|
||||
@Test
|
||||
public void webvttWithCssTextCombineUpright() throws Exception {
|
||||
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_TEXT_COMBINE_UPRIGHT);
|
||||
|
||||
Subtitle subtitle = getSubtitleForTestAsset(WITH_CSS_TEXT_COMBINE_UPRIGHT);
|
||||
Spanned firstCueText = getUniqueSpanTextAt(subtitle, 500_000);
|
||||
assertThat(firstCueText)
|
||||
.hasHorizontalTextInVerticalContextSpanBetween("Combine ".length(), "Combine all".length());
|
||||
|
||||
Spanned secondCueText = getUniqueSpanTextAt(subtitle, 3_500_000);
|
||||
assertThat(secondCueText)
|
||||
.hasHorizontalTextInVerticalContextSpanBetween(
|
||||
"Combine ".length(), "Combine 0004".length());
|
||||
}
|
||||
|
||||
private WebvttSubtitle getSubtitleForTestAsset(String asset)
|
||||
throws IOException, SubtitleDecoderException {
|
||||
WebvttDecoder decoder = new WebvttDecoder();
|
||||
private Subtitle getSubtitleForTestAsset(String asset) throws IOException {
|
||||
DelegatingSubtitleDecoder decoder =
|
||||
new DelegatingSubtitleDecoder(
|
||||
"DelegatingSubtitleDecoderWithWebvttParser", new WebvttParser());
|
||||
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), asset);
|
||||
return (WebvttSubtitle) decoder.decode(bytes, bytes.length, /* reset= */ false);
|
||||
return decoder.decode(bytes, bytes.length, /* reset= */ false);
|
||||
}
|
||||
|
||||
private Spanned getUniqueSpanTextAt(WebvttSubtitle sub, long timeUs) {
|
||||
private Spanned getUniqueSpanTextAt(Subtitle sub, long timeUs) {
|
||||
return (Spanned) Assertions.checkNotNull(sub.getCues(timeUs).get(0).text);
|
||||
}
|
||||
}
|
@ -25,6 +25,7 @@ import androidx.media3.extractor.text.ssa.SsaParser;
|
||||
import androidx.media3.extractor.text.subrip.SubripParser;
|
||||
import androidx.media3.extractor.text.tx3g.Tx3gParser;
|
||||
import androidx.media3.extractor.text.webvtt.Mp4WebvttParser;
|
||||
import androidx.media3.extractor.text.webvtt.WebvttParser;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
@ -33,9 +34,10 @@ import java.util.Objects;
|
||||
* <p>The formats supported by this factory are:
|
||||
*
|
||||
* <ul>
|
||||
* <li>SSA/ASS ({@link SsaParser})
|
||||
* <li>WebVTT ({@link WebvttParser})
|
||||
* <li>WebVTT (MP4) ({@link Mp4WebvttParser})
|
||||
* <li>SubRip ({@link SubripParser})
|
||||
* <li>SSA/ASS ({@link SsaParser})
|
||||
* <li>TX3G ({@link Tx3gParser})
|
||||
* <li>PGS ({@link PgsParser})
|
||||
* <li>DVB ({@link DvbParser})
|
||||
@ -48,6 +50,7 @@ public final class DefaultSubtitleParserFactory implements SubtitleParser.Factor
|
||||
public boolean supportsFormat(Format format) {
|
||||
@Nullable String mimeType = format.sampleMimeType;
|
||||
return Objects.equals(mimeType, MimeTypes.TEXT_SSA)
|
||||
|| Objects.equals(mimeType, MimeTypes.TEXT_VTT)
|
||||
|| Objects.equals(mimeType, MimeTypes.APPLICATION_MP4VTT)
|
||||
|| Objects.equals(mimeType, MimeTypes.APPLICATION_SUBRIP)
|
||||
|| Objects.equals(mimeType, MimeTypes.APPLICATION_TX3G)
|
||||
@ -62,6 +65,8 @@ public final class DefaultSubtitleParserFactory implements SubtitleParser.Factor
|
||||
switch (mimeType) {
|
||||
case MimeTypes.TEXT_SSA:
|
||||
return new SsaParser(format.initializationData);
|
||||
case MimeTypes.TEXT_VTT:
|
||||
return new WebvttParser();
|
||||
case MimeTypes.APPLICATION_MP4VTT:
|
||||
return new Mp4WebvttParser();
|
||||
case MimeTypes.APPLICATION_SUBRIP:
|
||||
|
@ -20,19 +20,19 @@ import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.ParserException;
|
||||
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.extractor.text.SubtitleDecoderException;
|
||||
import androidx.media3.extractor.text.CuesWithTiming;
|
||||
import androidx.media3.extractor.text.SubtitleParser;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A {@link SimpleSubtitleDecoder} for WebVTT.
|
||||
* A {@link SubtitleParser} for WebVTT.
|
||||
*
|
||||
* <p>See the <a href="http://dev.w3.org/html5/webvtt">WebVTT specification</a>.
|
||||
*/
|
||||
@UnstableApi
|
||||
public final class WebvttDecoder extends SimpleSubtitleDecoder {
|
||||
public final class WebvttParser implements SubtitleParser {
|
||||
|
||||
private static final int EVENT_NONE = -1;
|
||||
private static final int EVENT_END_OF_FILE = 0;
|
||||
@ -46,23 +46,22 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder {
|
||||
private final ParsableByteArray parsableWebvttData;
|
||||
private final WebvttCssParser cssParser;
|
||||
|
||||
public WebvttDecoder() {
|
||||
super("WebvttDecoder");
|
||||
public WebvttParser() {
|
||||
parsableWebvttData = new ParsableByteArray();
|
||||
cssParser = new WebvttCssParser();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Subtitle decode(byte[] data, int length, boolean reset)
|
||||
throws SubtitleDecoderException {
|
||||
public ImmutableList<CuesWithTiming> parse(byte[] data, int offset, int length) {
|
||||
parsableWebvttData.reset(data, length);
|
||||
parsableWebvttData.setPosition(offset);
|
||||
List<WebvttCssStyle> definedStyles = new ArrayList<>();
|
||||
|
||||
// Validate the first line of the header, and skip the remainder.
|
||||
try {
|
||||
WebvttParserUtil.validateWebvttHeaderLine(parsableWebvttData);
|
||||
} catch (ParserException e) {
|
||||
throw new SubtitleDecoderException(e);
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
while (!TextUtils.isEmpty(parsableWebvttData.readLine())) {}
|
||||
|
||||
@ -73,7 +72,7 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder {
|
||||
skipComment(parsableWebvttData);
|
||||
} else if (event == EVENT_STYLE_BLOCK) {
|
||||
if (!cueInfos.isEmpty()) {
|
||||
throw new SubtitleDecoderException("A style block was found after the first cue.");
|
||||
throw new IllegalArgumentException("A style block was found after the first cue.");
|
||||
}
|
||||
parsableWebvttData.readLine(); // Consume the "STYLE" header.
|
||||
definedStyles.addAll(cssParser.parseBlock(parsableWebvttData));
|
||||
@ -85,9 +84,13 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder {
|
||||
}
|
||||
}
|
||||
}
|
||||
return new WebvttSubtitle(cueInfos);
|
||||
WebvttSubtitle subtitle = new WebvttSubtitle(cueInfos);
|
||||
return subtitle.toCuesWithTimingList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {}
|
||||
|
||||
/**
|
||||
* Positions the input right before the next event, and returns the kind of event found. Does not
|
||||
* consume any data from such event, if any.
|
@ -21,8 +21,9 @@ import static org.junit.Assert.assertThrows;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.common.MimeTypes;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.exoplayer.text.DelegatingSubtitleDecoder;
|
||||
import androidx.media3.extractor.Extractor;
|
||||
import androidx.media3.extractor.text.webvtt.WebvttDecoder;
|
||||
import androidx.media3.extractor.text.webvtt.WebvttParser;
|
||||
import androidx.media3.test.utils.FakeExtractorInput;
|
||||
import androidx.media3.test.utils.FakeExtractorOutput;
|
||||
import androidx.media3.test.utils.FakeTrackOutput;
|
||||
@ -64,7 +65,8 @@ public class SubtitleExtractorTest {
|
||||
.build();
|
||||
SubtitleExtractor extractor =
|
||||
new SubtitleExtractor(
|
||||
new WebvttDecoder(),
|
||||
new DelegatingSubtitleDecoder(
|
||||
"DelegatingSubtitleDecoderWithWebvttParser", new WebvttParser()),
|
||||
new Format.Builder().setSampleMimeType(MimeTypes.TEXT_VTT).build());
|
||||
extractor.init(output);
|
||||
|
||||
@ -107,7 +109,8 @@ public class SubtitleExtractorTest {
|
||||
.build();
|
||||
SubtitleExtractor extractor =
|
||||
new SubtitleExtractor(
|
||||
new WebvttDecoder(),
|
||||
new DelegatingSubtitleDecoder(
|
||||
"DelegatingSubtitleDecoderWithWebvttParser", new WebvttParser()),
|
||||
new Format.Builder().setSampleMimeType(MimeTypes.TEXT_VTT).build());
|
||||
extractor.init(output);
|
||||
FakeTrackOutput trackOutput = output.trackOutputs.get(0);
|
||||
@ -149,7 +152,8 @@ public class SubtitleExtractorTest {
|
||||
.build();
|
||||
SubtitleExtractor extractor =
|
||||
new SubtitleExtractor(
|
||||
new WebvttDecoder(),
|
||||
new DelegatingSubtitleDecoder(
|
||||
"DelegatingSubtitleDecoderWithWebvttParser", new WebvttParser()),
|
||||
new Format.Builder().setSampleMimeType(MimeTypes.TEXT_VTT).build());
|
||||
extractor.init(output);
|
||||
FakeTrackOutput trackOutput = output.trackOutputs.get(0);
|
||||
@ -185,7 +189,10 @@ public class SubtitleExtractorTest {
|
||||
public void read_withoutInit_fails() {
|
||||
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(new byte[0]).build();
|
||||
SubtitleExtractor extractor =
|
||||
new SubtitleExtractor(new WebvttDecoder(), new Format.Builder().build());
|
||||
new SubtitleExtractor(
|
||||
new DelegatingSubtitleDecoder(
|
||||
"DelegatingSubtitleDecoderWithWebvttParser", new WebvttParser()),
|
||||
new Format.Builder().build());
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> extractor.read(input, null));
|
||||
}
|
||||
@ -194,7 +201,10 @@ public class SubtitleExtractorTest {
|
||||
public void read_afterRelease_fails() {
|
||||
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(new byte[0]).build();
|
||||
SubtitleExtractor extractor =
|
||||
new SubtitleExtractor(new WebvttDecoder(), new Format.Builder().build());
|
||||
new SubtitleExtractor(
|
||||
new DelegatingSubtitleDecoder(
|
||||
"DelegatingSubtitleDecoderWithWebvttParser", new WebvttParser()),
|
||||
new Format.Builder().build());
|
||||
FakeExtractorOutput output = new FakeExtractorOutput();
|
||||
|
||||
extractor.init(output);
|
||||
@ -206,7 +216,10 @@ public class SubtitleExtractorTest {
|
||||
@Test
|
||||
public void seek_withoutInit_fails() {
|
||||
SubtitleExtractor extractor =
|
||||
new SubtitleExtractor(new WebvttDecoder(), new Format.Builder().build());
|
||||
new SubtitleExtractor(
|
||||
new DelegatingSubtitleDecoder(
|
||||
"DelegatingSubtitleDecoderWithWebvttParser", new WebvttParser()),
|
||||
new Format.Builder().build());
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> extractor.seek(0, 0));
|
||||
}
|
||||
@ -214,7 +227,10 @@ public class SubtitleExtractorTest {
|
||||
@Test
|
||||
public void seek_afterRelease_fails() {
|
||||
SubtitleExtractor extractor =
|
||||
new SubtitleExtractor(new WebvttDecoder(), new Format.Builder().build());
|
||||
new SubtitleExtractor(
|
||||
new DelegatingSubtitleDecoder(
|
||||
"DelegatingSubtitleDecoderWithWebvttParser", new WebvttParser()),
|
||||
new Format.Builder().build());
|
||||
FakeExtractorOutput output = new FakeExtractorOutput();
|
||||
|
||||
extractor.init(output);
|
||||
@ -226,7 +242,10 @@ public class SubtitleExtractorTest {
|
||||
@Test
|
||||
public void released_calledTwice() {
|
||||
SubtitleExtractor extractor =
|
||||
new SubtitleExtractor(new WebvttDecoder(), new Format.Builder().build());
|
||||
new SubtitleExtractor(
|
||||
new DelegatingSubtitleDecoder(
|
||||
"DelegatingSubtitleDecoderWithWebvttParser", new WebvttParser()),
|
||||
new Format.Builder().build());
|
||||
FakeExtractorOutput output = new FakeExtractorOutput();
|
||||
|
||||
extractor.init(output);
|
||||
|
@ -0,0 +1,563 @@
|
||||
/*
|
||||
* 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.webvtt;
|
||||
|
||||
import static androidx.media3.test.utils.truth.SpannedSubject.assertThat;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
import android.text.Layout.Alignment;
|
||||
import android.text.Spanned;
|
||||
import androidx.media3.common.text.Cue;
|
||||
import androidx.media3.common.text.TextAnnotation;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.ColorParser;
|
||||
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 com.google.common.collect.Iterables;
|
||||
import com.google.common.truth.Expect;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Unit test for {@link WebvttParser}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class WebvttParserTest {
|
||||
|
||||
private static final String TYPICAL_FILE = "media/webvtt/typical";
|
||||
private static final String TYPICAL_WITH_BAD_TIMESTAMPS =
|
||||
"media/webvtt/typical_with_bad_timestamps";
|
||||
private static final String TYPICAL_WITH_IDS_FILE = "media/webvtt/typical_with_identifiers";
|
||||
private static final String TYPICAL_WITH_COMMENTS_FILE = "media/webvtt/typical_with_comments";
|
||||
private static final String WITH_POSITIONING_FILE = "media/webvtt/with_positioning";
|
||||
private static final String WITH_OVERLAPPING_TIMESTAMPS_FILE =
|
||||
"media/webvtt/with_overlapping_timestamps";
|
||||
private static final String WITH_VERTICAL_FILE = "media/webvtt/with_vertical";
|
||||
private static final String WITH_RUBIES_FILE = "media/webvtt/with_rubies";
|
||||
private static final String WITH_BAD_CUE_HEADER_FILE = "media/webvtt/with_bad_cue_header";
|
||||
private static final String WITH_TAGS_FILE = "media/webvtt/with_tags";
|
||||
private static final String WITH_CSS_STYLES = "media/webvtt/with_css_styles";
|
||||
private static final String WITH_FONT_SIZE = "media/webvtt/with_font_size";
|
||||
private static final String WITH_CSS_COMPLEX_SELECTORS =
|
||||
"media/webvtt/with_css_complex_selectors";
|
||||
private static final String WITH_CSS_TEXT_COMBINE_UPRIGHT =
|
||||
"media/webvtt/with_css_text_combine_upright";
|
||||
private static final String WITH_BOM = "media/webvtt/with_bom";
|
||||
private static final String EMPTY_FILE = "media/webvtt/empty";
|
||||
|
||||
@Rule public final Expect expect = Expect.create();
|
||||
|
||||
@Test
|
||||
public void parseEmpty() throws IOException {
|
||||
WebvttParser parser = new WebvttParser();
|
||||
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), EMPTY_FILE);
|
||||
assertThrows(IllegalArgumentException.class, () -> parser.parse(bytes));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseTypical() throws Exception {
|
||||
List<CuesWithTiming> allCues = getCuesForTestAsset(TYPICAL_FILE);
|
||||
|
||||
assertThat(allCues).hasSize(2);
|
||||
|
||||
assertThat(allCues.get(0).startTimeUs).isEqualTo(0L);
|
||||
assertThat(allCues.get(0).durationUs).isEqualTo(1_234_000L);
|
||||
Cue firstCue = Iterables.getOnlyElement(allCues.get(0).cues);
|
||||
assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle.");
|
||||
|
||||
assertThat(allCues.get(1).startTimeUs).isEqualTo(2_345_000L);
|
||||
assertThat(allCues.get(1).durationUs).isEqualTo(3_456_000L - 2_345_000L);
|
||||
Cue secondCue = Iterables.getOnlyElement(allCues.get(1).cues);
|
||||
assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseWithBom() throws Exception {
|
||||
List<CuesWithTiming> allCues = getCuesForTestAsset(WITH_BOM);
|
||||
|
||||
assertThat(allCues).hasSize(2);
|
||||
|
||||
assertThat(allCues.get(0).startTimeUs).isEqualTo(0L);
|
||||
assertThat(allCues.get(0).durationUs).isEqualTo(1_234_000L);
|
||||
Cue firstCue = Iterables.getOnlyElement(allCues.get(0).cues);
|
||||
assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle.");
|
||||
|
||||
assertThat(allCues.get(1).startTimeUs).isEqualTo(2_345_000L);
|
||||
assertThat(allCues.get(1).durationUs).isEqualTo(3_456_000L - 2_345_000L);
|
||||
Cue secondCue = Iterables.getOnlyElement(allCues.get(1).cues);
|
||||
assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseTypicalWithBadTimestamps() throws Exception {
|
||||
List<CuesWithTiming> allCues = getCuesForTestAsset(TYPICAL_WITH_BAD_TIMESTAMPS);
|
||||
|
||||
assertThat(allCues).hasSize(2);
|
||||
|
||||
assertThat(allCues.get(0).startTimeUs).isEqualTo(0L);
|
||||
assertThat(allCues.get(0).durationUs).isEqualTo(1_234_000L);
|
||||
Cue firstCue = Iterables.getOnlyElement(allCues.get(0).cues);
|
||||
assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle.");
|
||||
|
||||
assertThat(allCues.get(1).startTimeUs).isEqualTo(2_345_000L);
|
||||
assertThat(allCues.get(1).durationUs).isEqualTo(3_456_000L - 2_345_000L);
|
||||
Cue secondCue = Iterables.getOnlyElement(allCues.get(1).cues);
|
||||
assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseTypicalWithIds() throws Exception {
|
||||
List<CuesWithTiming> allCues = getCuesForTestAsset(TYPICAL_WITH_IDS_FILE);
|
||||
|
||||
assertThat(allCues).hasSize(2);
|
||||
|
||||
assertThat(allCues.get(0).startTimeUs).isEqualTo(0L);
|
||||
assertThat(allCues.get(0).durationUs).isEqualTo(1_234_000L);
|
||||
Cue firstCue = Iterables.getOnlyElement(allCues.get(0).cues);
|
||||
assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle.");
|
||||
|
||||
assertThat(allCues.get(1).startTimeUs).isEqualTo(2_345_000L);
|
||||
assertThat(allCues.get(1).durationUs).isEqualTo(3_456_000L - 2_345_000L);
|
||||
Cue secondCue = Iterables.getOnlyElement(allCues.get(1).cues);
|
||||
assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseTypicalWithComments() throws Exception {
|
||||
List<CuesWithTiming> allCues = getCuesForTestAsset(TYPICAL_WITH_COMMENTS_FILE);
|
||||
|
||||
assertThat(allCues).hasSize(2);
|
||||
|
||||
assertThat(allCues.get(0).startTimeUs).isEqualTo(0L);
|
||||
assertThat(allCues.get(0).durationUs).isEqualTo(1_234_000L);
|
||||
Cue firstCue = Iterables.getOnlyElement(allCues.get(0).cues);
|
||||
assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle.");
|
||||
|
||||
assertThat(allCues.get(1).startTimeUs).isEqualTo(2_345_000L);
|
||||
assertThat(allCues.get(1).durationUs).isEqualTo(3_456_000L - 2_345_000L);
|
||||
Cue secondCue = Iterables.getOnlyElement(allCues.get(1).cues);
|
||||
assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseWithTags() throws Exception {
|
||||
List<CuesWithTiming> allCues = getCuesForTestAsset(WITH_TAGS_FILE);
|
||||
|
||||
assertThat(allCues).hasSize(4);
|
||||
|
||||
assertThat(allCues.get(0).startTimeUs).isEqualTo(0L);
|
||||
assertThat(allCues.get(0).durationUs).isEqualTo(1_234_000L);
|
||||
Cue firstCue = Iterables.getOnlyElement(allCues.get(0).cues);
|
||||
assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle.");
|
||||
|
||||
assertThat(allCues.get(1).startTimeUs).isEqualTo(2_345_000L);
|
||||
assertThat(allCues.get(1).durationUs).isEqualTo(3_456_000L - 2_345_000L);
|
||||
Cue secondCue = Iterables.getOnlyElement(allCues.get(1).cues);
|
||||
assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle.");
|
||||
|
||||
assertThat(allCues.get(2).startTimeUs).isEqualTo(4_000_000L);
|
||||
assertThat(allCues.get(2).durationUs).isEqualTo(5_000_000L - 4_000_000L);
|
||||
Cue thirdCue = Iterables.getOnlyElement(allCues.get(2).cues);
|
||||
assertThat(thirdCue.text.toString()).isEqualTo("This is the third subtitle.");
|
||||
|
||||
assertThat(allCues.get(3).startTimeUs).isEqualTo(6_000_000L);
|
||||
assertThat(allCues.get(3).durationUs).isEqualTo(7_000_000L - 6_000_000L);
|
||||
Cue fourthCue = Iterables.getOnlyElement(allCues.get(3).cues);
|
||||
assertThat(fourthCue.text.toString()).isEqualTo("This is the <fourth> &subtitle.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseWithPositioning() throws Exception {
|
||||
List<CuesWithTiming> allCues = getCuesForTestAsset(WITH_POSITIONING_FILE);
|
||||
|
||||
assertThat(allCues).hasSize(8);
|
||||
|
||||
assertThat(allCues.get(0).startTimeUs).isEqualTo(0L);
|
||||
assertThat(allCues.get(0).durationUs).isEqualTo(1_234_000L);
|
||||
Cue firstCue = Iterables.getOnlyElement(allCues.get(0).cues);
|
||||
assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle.");
|
||||
assertThat(firstCue.position).isEqualTo(0.6f);
|
||||
assertThat(firstCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_END);
|
||||
assertThat(firstCue.textAlignment).isEqualTo(Alignment.ALIGN_NORMAL);
|
||||
assertThat(firstCue.size).isEqualTo(0.35f);
|
||||
|
||||
// Unspecified values should use WebVTT defaults
|
||||
assertThat(firstCue.line).isEqualTo(-1f);
|
||||
assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_NUMBER);
|
||||
assertThat(firstCue.verticalType).isEqualTo(Cue.TYPE_UNSET);
|
||||
|
||||
assertThat(allCues.get(1).startTimeUs).isEqualTo(2_345_000L);
|
||||
assertThat(allCues.get(1).durationUs).isEqualTo(3_456_000L - 2_345_000L);
|
||||
Cue secondCue = Iterables.getOnlyElement(allCues.get(1).cues);
|
||||
assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle.");
|
||||
// Position is invalid so defaults to 0.5
|
||||
assertThat(secondCue.position).isEqualTo(0.5f);
|
||||
assertThat(secondCue.textAlignment).isEqualTo(Alignment.ALIGN_OPPOSITE);
|
||||
|
||||
assertThat(allCues.get(2).startTimeUs).isEqualTo(4_000_000L);
|
||||
assertThat(allCues.get(2).durationUs).isEqualTo(5_000_000L - 4_000_000L);
|
||||
Cue thirdCue = Iterables.getOnlyElement(allCues.get(2).cues);
|
||||
assertThat(thirdCue.text.toString()).isEqualTo("This is the third subtitle.");
|
||||
assertThat(thirdCue.line).isEqualTo(0.45f);
|
||||
assertThat(thirdCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION);
|
||||
assertThat(thirdCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END);
|
||||
assertThat(thirdCue.textAlignment).isEqualTo(Alignment.ALIGN_CENTER);
|
||||
// Derived from `align:middle`:
|
||||
assertThat(thirdCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE);
|
||||
|
||||
assertThat(allCues.get(3).startTimeUs).isEqualTo(6_000_000L);
|
||||
assertThat(allCues.get(3).durationUs).isEqualTo(7_000_000L - 6_000_000L);
|
||||
Cue fourthCue = Iterables.getOnlyElement(allCues.get(3).cues);
|
||||
assertThat(fourthCue.text.toString()).isEqualTo("This is the fourth subtitle.");
|
||||
assertThat(fourthCue.line).isEqualTo(-10f);
|
||||
assertThat(fourthCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_START);
|
||||
assertThat(fourthCue.textAlignment).isEqualTo(Alignment.ALIGN_CENTER);
|
||||
// Derived from `align:middle`:
|
||||
assertThat(fourthCue.position).isEqualTo(0.5f);
|
||||
assertThat(fourthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE);
|
||||
|
||||
assertThat(allCues.get(4).startTimeUs).isEqualTo(8_000_000L);
|
||||
assertThat(allCues.get(4).durationUs).isEqualTo(9_000_000L - 8_000_000L);
|
||||
Cue fifthCue = Iterables.getOnlyElement(allCues.get(4).cues);
|
||||
assertThat(fifthCue.text.toString()).isEqualTo("This is the fifth subtitle.");
|
||||
assertThat(fifthCue.textAlignment).isEqualTo(Alignment.ALIGN_OPPOSITE);
|
||||
// Derived from `align:right`:
|
||||
assertThat(fifthCue.position).isEqualTo(1.0f);
|
||||
assertThat(fifthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_END);
|
||||
|
||||
assertThat(allCues.get(5).startTimeUs).isEqualTo(10_000_000L);
|
||||
assertThat(allCues.get(5).durationUs).isEqualTo(11_000_000L - 10_000_000L);
|
||||
Cue sixthCue = Iterables.getOnlyElement(allCues.get(5).cues);
|
||||
assertThat(sixthCue.text.toString()).isEqualTo("This is the sixth subtitle.");
|
||||
assertThat(sixthCue.textAlignment).isEqualTo(Alignment.ALIGN_CENTER);
|
||||
// Derived from `align:center`:
|
||||
assertThat(sixthCue.position).isEqualTo(0.5f);
|
||||
assertThat(sixthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE);
|
||||
|
||||
assertThat(allCues.get(6).startTimeUs).isEqualTo(12_000_000L);
|
||||
assertThat(allCues.get(6).durationUs).isEqualTo(13_000_000L - 12_000_000L);
|
||||
Cue seventhCue = Iterables.getOnlyElement(allCues.get(6).cues);
|
||||
assertThat(seventhCue.text.toString()).isEqualTo("This is the seventh subtitle.");
|
||||
assertThat(seventhCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_START);
|
||||
|
||||
assertThat(allCues.get(7).startTimeUs).isEqualTo(14_000_000L);
|
||||
assertThat(allCues.get(7).durationUs).isEqualTo(15_000_000L - 14_000_000L);
|
||||
Cue eighthCue = Iterables.getOnlyElement(allCues.get(7).cues);
|
||||
assertThat(eighthCue.text.toString()).isEqualTo("This is the eighth subtitle.");
|
||||
assertThat(eighthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_END);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseWithOverlappingTimestamps() throws Exception {
|
||||
List<CuesWithTiming> allCues = getCuesForTestAsset(WITH_OVERLAPPING_TIMESTAMPS_FILE);
|
||||
|
||||
assertThat(allCues).hasSize(6);
|
||||
|
||||
assertThat(allCues.get(0).startTimeUs).isEqualTo(0);
|
||||
assertThat(allCues.get(0).durationUs).isEqualTo(1_000_000);
|
||||
Cue firstCue = Iterables.getOnlyElement(allCues.get(0).cues);
|
||||
assertThat(firstCue.text.toString()).isEqualTo("Displayed at the bottom for 3 seconds.");
|
||||
assertThat(firstCue.line).isEqualTo(-1f);
|
||||
assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_NUMBER);
|
||||
|
||||
assertThat(allCues.get(1).startTimeUs).isEqualTo(1_000_000);
|
||||
assertThat(allCues.get(1).durationUs).isEqualTo(1_000_000);
|
||||
ImmutableList<Cue> firstAndSecondCue = allCues.get(1).cues;
|
||||
assertThat(firstAndSecondCue).hasSize(2);
|
||||
assertThat(firstAndSecondCue.get(0).text.toString())
|
||||
.isEqualTo("Displayed at the bottom for 3 seconds.");
|
||||
assertThat(firstAndSecondCue.get(0).line).isEqualTo(-1f);
|
||||
assertThat(firstAndSecondCue.get(0).lineType).isEqualTo(Cue.LINE_TYPE_NUMBER);
|
||||
assertThat(firstAndSecondCue.get(1).text.toString())
|
||||
.isEqualTo("Appears directly above for 1 second.");
|
||||
assertThat(firstAndSecondCue.get(1).line).isEqualTo(-2f);
|
||||
assertThat(firstAndSecondCue.get(1).lineType).isEqualTo(Cue.LINE_TYPE_NUMBER);
|
||||
|
||||
assertThat(allCues.get(2).startTimeUs).isEqualTo(2_000_000);
|
||||
assertThat(allCues.get(2).durationUs).isEqualTo(1_000_000);
|
||||
Cue thirdCue = Iterables.getOnlyElement(allCues.get(2).cues);
|
||||
assertThat(thirdCue.text.toString()).isEqualTo("Displayed at the bottom for 3 seconds.");
|
||||
assertThat(thirdCue.line).isEqualTo(-1f);
|
||||
assertThat(thirdCue.lineType).isEqualTo(Cue.LINE_TYPE_NUMBER);
|
||||
|
||||
assertThat(allCues.get(3).startTimeUs).isEqualTo(4_000_000);
|
||||
assertThat(allCues.get(3).durationUs).isEqualTo(1_000_000);
|
||||
Cue fourthCue = Iterables.getOnlyElement(allCues.get(3).cues);
|
||||
assertThat(fourthCue.text.toString()).isEqualTo("Displayed at the bottom for 2 seconds.");
|
||||
assertThat(fourthCue.line).isEqualTo(-1f);
|
||||
assertThat(fourthCue.lineType).isEqualTo(Cue.LINE_TYPE_NUMBER);
|
||||
|
||||
assertThat(allCues.get(4).startTimeUs).isEqualTo(5_000_000);
|
||||
assertThat(allCues.get(4).durationUs).isEqualTo(1_000_000);
|
||||
ImmutableList<Cue> fourthAndFifth = allCues.get(4).cues;
|
||||
assertThat(fourthAndFifth).hasSize(2);
|
||||
assertThat(fourthAndFifth.get(0).text.toString())
|
||||
.isEqualTo("Displayed at the bottom for 2 seconds.");
|
||||
assertThat(fourthAndFifth.get(0).line).isEqualTo(-1f);
|
||||
assertThat(fourthAndFifth.get(0).lineType).isEqualTo(Cue.LINE_TYPE_NUMBER);
|
||||
assertThat(fourthAndFifth.get(1).text.toString())
|
||||
.isEqualTo("Appears directly above the previous cue, then replaces it after 1 second.");
|
||||
assertThat(fourthAndFifth.get(1).line).isEqualTo(-2f);
|
||||
assertThat(fourthAndFifth.get(1).lineType).isEqualTo(Cue.LINE_TYPE_NUMBER);
|
||||
|
||||
assertThat(allCues.get(5).startTimeUs).isEqualTo(6_000_000);
|
||||
assertThat(allCues.get(5).durationUs).isEqualTo(1_000_000);
|
||||
Cue sixthCue = Iterables.getOnlyElement(allCues.get(5).cues);
|
||||
assertThat(sixthCue.text.toString())
|
||||
.isEqualTo("Appears directly above the previous cue, then replaces it after 1 second.");
|
||||
assertThat(sixthCue.line).isEqualTo(-1f);
|
||||
assertThat(sixthCue.lineType).isEqualTo(Cue.LINE_TYPE_NUMBER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseWithVertical() throws Exception {
|
||||
List<CuesWithTiming> allCues = getCuesForTestAsset(WITH_VERTICAL_FILE);
|
||||
|
||||
assertThat(allCues).hasSize(3);
|
||||
|
||||
assertThat(allCues.get(0).startTimeUs).isEqualTo(0L);
|
||||
assertThat(allCues.get(0).durationUs).isEqualTo(1_234_000L);
|
||||
Cue firstCue = Iterables.getOnlyElement(allCues.get(0).cues);
|
||||
assertThat(firstCue.text.toString()).isEqualTo("Vertical right-to-left (e.g. Japanese)");
|
||||
assertThat(firstCue.verticalType).isEqualTo(Cue.VERTICAL_TYPE_RL);
|
||||
|
||||
assertThat(allCues.get(1).startTimeUs).isEqualTo(2_345_000L);
|
||||
assertThat(allCues.get(1).durationUs).isEqualTo(3_456_000L - 2_345_000L);
|
||||
Cue secondCue = Iterables.getOnlyElement(allCues.get(1).cues);
|
||||
assertThat(secondCue.text.toString()).isEqualTo("Vertical left-to-right (e.g. Mongolian)");
|
||||
assertThat(secondCue.verticalType).isEqualTo(Cue.VERTICAL_TYPE_LR);
|
||||
|
||||
assertThat(allCues.get(2).startTimeUs).isEqualTo(4_000_000L);
|
||||
assertThat(allCues.get(2).durationUs).isEqualTo(5_000_000L - 4_000_000L);
|
||||
Cue thirdCue = Iterables.getOnlyElement(allCues.get(2).cues);
|
||||
assertThat(thirdCue.text.toString()).isEqualTo("No vertical setting (i.e. horizontal)");
|
||||
assertThat(thirdCue.verticalType).isEqualTo(Cue.TYPE_UNSET);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseWithRubies() throws Exception {
|
||||
List<CuesWithTiming> allCues = getCuesForTestAsset(WITH_RUBIES_FILE);
|
||||
|
||||
assertThat(allCues).hasSize(4);
|
||||
|
||||
// Check that an explicit `over` position is read from CSS.
|
||||
Cue firstCue = Iterables.getOnlyElement(allCues.get(0).cues);
|
||||
assertThat(firstCue.text.toString()).isEqualTo("Some text with over-ruby.");
|
||||
assertThat((Spanned) firstCue.text)
|
||||
.hasRubySpanBetween("Some ".length(), "Some text with over-ruby".length())
|
||||
.withTextAndPosition("over", TextAnnotation.POSITION_BEFORE);
|
||||
|
||||
// Check that `under` is read from CSS and unspecified defaults to `over`.
|
||||
Cue secondCue = Iterables.getOnlyElement(allCues.get(1).cues);
|
||||
assertThat(secondCue.text.toString())
|
||||
.isEqualTo("Some text with under-ruby and over-ruby (default).");
|
||||
assertThat((Spanned) secondCue.text)
|
||||
.hasRubySpanBetween("Some ".length(), "Some text with under-ruby".length())
|
||||
.withTextAndPosition("under", TextAnnotation.POSITION_AFTER);
|
||||
assertThat((Spanned) secondCue.text)
|
||||
.hasRubySpanBetween(
|
||||
"Some text with under-ruby and ".length(),
|
||||
"Some text with under-ruby and over-ruby (default)".length())
|
||||
.withTextAndPosition("over", TextAnnotation.POSITION_BEFORE);
|
||||
|
||||
// Check many <rt> tags with different positions nested in a single <ruby> span.
|
||||
Cue thirdCue = Iterables.getOnlyElement(allCues.get(2).cues);
|
||||
assertThat(thirdCue.text.toString()).isEqualTo("base1base2base3.");
|
||||
assertThat((Spanned) thirdCue.text)
|
||||
.hasRubySpanBetween(/* start= */ 0, "base1".length())
|
||||
.withTextAndPosition("over1", TextAnnotation.POSITION_BEFORE);
|
||||
assertThat((Spanned) thirdCue.text)
|
||||
.hasRubySpanBetween("base1".length(), "base1base2".length())
|
||||
.withTextAndPosition("under2", TextAnnotation.POSITION_AFTER);
|
||||
assertThat((Spanned) thirdCue.text)
|
||||
.hasRubySpanBetween("base1base2".length(), "base1base2base3".length())
|
||||
.withTextAndPosition("under3", TextAnnotation.POSITION_AFTER);
|
||||
|
||||
// Check a <ruby> span with no <rt> tags.
|
||||
Cue fourthCue = Iterables.getOnlyElement(allCues.get(3).cues);
|
||||
assertThat(fourthCue.text.toString()).isEqualTo("Some text with no ruby text.");
|
||||
assertThat((Spanned) fourthCue.text).hasNoSpans();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseWithBadCueHeader() throws Exception {
|
||||
List<CuesWithTiming> allCues = getCuesForTestAsset(WITH_BAD_CUE_HEADER_FILE);
|
||||
|
||||
assertThat(allCues).hasSize(2);
|
||||
|
||||
assertThat(allCues.get(0).startTimeUs).isEqualTo(0L);
|
||||
assertThat(allCues.get(0).durationUs).isEqualTo(1_234_000L);
|
||||
Cue firstCue = Iterables.getOnlyElement(allCues.get(0).cues);
|
||||
assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle.");
|
||||
|
||||
assertThat(allCues.get(1).startTimeUs).isEqualTo(4_000_000L);
|
||||
assertThat(allCues.get(1).durationUs).isEqualTo(5_000_000L - 4_000_000L);
|
||||
Cue secondCue = Iterables.getOnlyElement(allCues.get(1).cues);
|
||||
assertThat(secondCue.text.toString()).isEqualTo("This is the third subtitle.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseWithCssFontSizeStyle() throws Exception {
|
||||
List<CuesWithTiming> allCues = getCuesForTestAsset(WITH_FONT_SIZE);
|
||||
|
||||
assertThat(allCues).hasSize(6);
|
||||
|
||||
assertThat(allCues.get(0).startTimeUs).isEqualTo(0L);
|
||||
assertThat(allCues.get(0).durationUs).isEqualTo(2_000_000L);
|
||||
Cue firstCue = Iterables.getOnlyElement(allCues.get(0).cues);
|
||||
assertThat(firstCue.text.toString()).isEqualTo("Sentence with font-size set to 4.4em.");
|
||||
assertThat((Spanned) firstCue.text)
|
||||
.hasRelativeSizeSpanBetween(0, "Sentence with font-size set to 4.4em.".length())
|
||||
.withSizeChange(4.4f);
|
||||
|
||||
assertThat(allCues.get(1).startTimeUs).isEqualTo(2_100_000L);
|
||||
assertThat(allCues.get(1).durationUs).isEqualTo(2_400_000L - 2_100_000L);
|
||||
Cue secondCue = Iterables.getOnlyElement(allCues.get(1).cues);
|
||||
assertThat(secondCue.text.toString()).isEqualTo("Sentence with bad font-size unit.");
|
||||
assertThat((Spanned) secondCue.text).hasNoSpans();
|
||||
|
||||
assertThat(allCues.get(2).startTimeUs).isEqualTo(2_500_000L);
|
||||
assertThat(allCues.get(2).durationUs).isEqualTo(4_000_000L - 2_500_000L);
|
||||
Cue thirdCue = Iterables.getOnlyElement(allCues.get(2).cues);
|
||||
assertThat(thirdCue.text.toString()).isEqualTo("Absolute font-size expressed in px unit!");
|
||||
assertThat((Spanned) thirdCue.text)
|
||||
.hasAbsoluteSizeSpanBetween(0, "Absolute font-size expressed in px unit!".length())
|
||||
.withAbsoluteSize(2);
|
||||
|
||||
assertThat(allCues.get(3).startTimeUs).isEqualTo(4_500_000L);
|
||||
assertThat(allCues.get(3).durationUs).isEqualTo(6_000_000L - 4_500_000L);
|
||||
Cue fourthCue = Iterables.getOnlyElement(allCues.get(3).cues);
|
||||
assertThat(fourthCue.text.toString()).isEqualTo("Relative font-size expressed in % unit!");
|
||||
assertThat((Spanned) fourthCue.text)
|
||||
.hasRelativeSizeSpanBetween(0, "Relative font-size expressed in % unit!".length())
|
||||
.withSizeChange(0.035f);
|
||||
|
||||
assertThat(allCues.get(4).startTimeUs).isEqualTo(6_100_000L);
|
||||
assertThat(allCues.get(4).durationUs).isEqualTo(6_400_000L - 6_100_000L);
|
||||
Cue fifthCue = Iterables.getOnlyElement(allCues.get(4).cues);
|
||||
assertThat(fifthCue.text.toString()).isEqualTo("Sentence with bad font-size value.");
|
||||
assertThat((Spanned) secondCue.text).hasNoSpans();
|
||||
|
||||
assertThat(allCues.get(5).startTimeUs).isEqualTo(6_500_000L);
|
||||
assertThat(allCues.get(5).durationUs).isEqualTo(8_000_000L - 6_500_000L);
|
||||
Cue sixthCue = Iterables.getOnlyElement(allCues.get(5).cues);
|
||||
assertThat(sixthCue.text.toString())
|
||||
.isEqualTo("Upper and lower case letters in font-size unit.");
|
||||
assertThat((Spanned) sixthCue.text)
|
||||
.hasAbsoluteSizeSpanBetween(0, "Upper and lower case letters in font-size unit.".length())
|
||||
.withAbsoluteSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void webvttWithCssStyle() throws Exception {
|
||||
List<CuesWithTiming> allCues = getCuesForTestAsset(WITH_CSS_STYLES);
|
||||
|
||||
Spanned firstCueText = getUniqueSpanTextAt(allCues.get(0));
|
||||
assertThat(firstCueText.toString()).isEqualTo("This is the first subtitle.");
|
||||
assertThat(firstCueText)
|
||||
.hasForegroundColorSpanBetween(0, firstCueText.length())
|
||||
.withColor(ColorParser.parseCssColor("papayawhip"));
|
||||
assertThat(firstCueText)
|
||||
.hasBackgroundColorSpanBetween(0, firstCueText.length())
|
||||
.withColor(ColorParser.parseCssColor("green"));
|
||||
|
||||
Spanned secondCueText = getUniqueSpanTextAt(allCues.get(1));
|
||||
assertThat(secondCueText.toString()).isEqualTo("This is the second subtitle.");
|
||||
assertThat(secondCueText)
|
||||
.hasForegroundColorSpanBetween(0, secondCueText.length())
|
||||
.withColor(ColorParser.parseCssColor("peachpuff"));
|
||||
|
||||
Spanned thirdCueText = getUniqueSpanTextAt(allCues.get(2));
|
||||
assertThat(thirdCueText.toString()).isEqualTo("This is a reference by element");
|
||||
assertThat(thirdCueText).hasUnderlineSpanBetween("This is a ".length(), thirdCueText.length());
|
||||
|
||||
Spanned fourthCueText = getUniqueSpanTextAt(allCues.get(3));
|
||||
assertThat(fourthCueText.toString()).isEqualTo("You are an idiot\nYou don't have the guts");
|
||||
assertThat(fourthCueText)
|
||||
.hasBackgroundColorSpanBetween(0, "You are an idiot".length())
|
||||
.withColor(ColorParser.parseCssColor("lime"));
|
||||
assertThat(fourthCueText)
|
||||
.hasBoldSpanBetween("You are an idiot\n".length(), fourthCueText.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withComplexCssSelectors() throws Exception {
|
||||
List<CuesWithTiming> allCues = getCuesForTestAsset(WITH_CSS_COMPLEX_SELECTORS);
|
||||
Spanned firstCueText = getUniqueSpanTextAt(allCues.get(0));
|
||||
assertThat(firstCueText).hasUnderlineSpanBetween(0, firstCueText.length());
|
||||
assertThat(firstCueText)
|
||||
.hasForegroundColorSpanBetween(
|
||||
"This should be underlined and ".length(), firstCueText.length())
|
||||
.withColor(ColorParser.parseCssColor("violet"));
|
||||
assertThat(firstCueText)
|
||||
.hasTypefaceSpanBetween("This should be underlined and ".length(), firstCueText.length())
|
||||
.withFamily("courier");
|
||||
|
||||
Spanned secondCueText = getUniqueSpanTextAt(allCues.get(1));
|
||||
assertThat(secondCueText)
|
||||
.hasTypefaceSpanBetween("This ".length(), secondCueText.length())
|
||||
.withFamily("courier");
|
||||
assertThat(secondCueText)
|
||||
.hasNoForegroundColorSpanBetween("This ".length(), secondCueText.length());
|
||||
|
||||
Spanned thirdCueText = getUniqueSpanTextAt(allCues.get(2));
|
||||
assertThat(thirdCueText).hasBoldSpanBetween("This ".length(), thirdCueText.length());
|
||||
assertThat(thirdCueText)
|
||||
.hasTypefaceSpanBetween("This ".length(), thirdCueText.length())
|
||||
.withFamily("courier");
|
||||
|
||||
Spanned fourthCueText = getUniqueSpanTextAt(allCues.get(3));
|
||||
assertThat(fourthCueText)
|
||||
.hasNoStyleSpanBetween("This ".length(), "shouldn't be bold.".length());
|
||||
assertThat(fourthCueText)
|
||||
.hasBoldSpanBetween("This shouldn't be bold.\nThis ".length(), fourthCueText.length());
|
||||
|
||||
Spanned fifthCueText = getUniqueSpanTextAt(allCues.get(4));
|
||||
assertThat(fifthCueText)
|
||||
.hasNoStyleSpanBetween("This is ".length(), "This is specific".length());
|
||||
assertThat(fifthCueText)
|
||||
.hasItalicSpanBetween("This is specific\n".length(), fifthCueText.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void webvttWithCssTextCombineUpright() throws Exception {
|
||||
List<CuesWithTiming> allCues = getCuesForTestAsset(WITH_CSS_TEXT_COMBINE_UPRIGHT);
|
||||
|
||||
Spanned firstCueText = getUniqueSpanTextAt(allCues.get(0));
|
||||
assertThat(firstCueText)
|
||||
.hasHorizontalTextInVerticalContextSpanBetween("Combine ".length(), "Combine all".length());
|
||||
|
||||
Spanned secondCueText = getUniqueSpanTextAt(allCues.get(1));
|
||||
assertThat(secondCueText)
|
||||
.hasHorizontalTextInVerticalContextSpanBetween(
|
||||
"Combine ".length(), "Combine 0004".length());
|
||||
}
|
||||
|
||||
private List<CuesWithTiming> getCuesForTestAsset(String asset) throws IOException {
|
||||
WebvttParser parser = new WebvttParser();
|
||||
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), asset);
|
||||
return parser.parse(bytes);
|
||||
}
|
||||
|
||||
private Spanned getUniqueSpanTextAt(CuesWithTiming cuesWithTiming) {
|
||||
return (Spanned) Assertions.checkNotNull(cuesWithTiming.cues.get(0).text);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user