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:
jbibik 2023-07-19 14:31:22 +01:00 committed by Ian Baker
parent 5d453fcf37
commit f0f24aa0d4
7 changed files with 660 additions and 135 deletions

View File

@ -15,7 +15,10 @@
*/ */
package androidx.media3.exoplayer.text; package androidx.media3.exoplayer.text;
import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.extractor.text.CuesWithTiming; import androidx.media3.extractor.text.CuesWithTiming;
import androidx.media3.extractor.text.SimpleSubtitleDecoder; import androidx.media3.extractor.text.SimpleSubtitleDecoder;
import androidx.media3.extractor.text.Subtitle; import androidx.media3.extractor.text.Subtitle;
@ -43,17 +46,19 @@ import java.util.List;
* <li>DelegatingSubtitleDecoder("XXX", new XXXParser(initializationData)) * <li>DelegatingSubtitleDecoder("XXX", new XXXParser(initializationData))
* <li>XXXDecoder(initializationData) * <li>XXXDecoder(initializationData)
* </ul> * </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 static final Subtitle EMPTY_SUBTITLE = new CuesWithTimingSubtitle(ImmutableList.of());
private final SubtitleParser subtitleParser; private final SubtitleParser subtitleParser;
/* package */ DelegatingSubtitleDecoder(String name, SubtitleParser subtitleParser) { public DelegatingSubtitleDecoder(String name, SubtitleParser subtitleParser) {
super(name); super(name);
this.subtitleParser = subtitleParser; this.subtitleParser = subtitleParser;
} }
@ -63,7 +68,8 @@ import java.util.List;
if (reset) { if (reset) {
subtitleParser.reset(); subtitleParser.reset();
} }
@Nullable List<CuesWithTiming> cuesWithTiming = subtitleParser.parse(data); @Nullable
List<CuesWithTiming> cuesWithTiming = subtitleParser.parse(data, /* offset= */ 0, length);
if (cuesWithTiming == null) { if (cuesWithTiming == null) {
return EMPTY_SUBTITLE; return EMPTY_SUBTITLE;
} }

View File

@ -25,7 +25,6 @@ import androidx.media3.extractor.text.SubtitleParser;
import androidx.media3.extractor.text.cea.Cea608Decoder; import androidx.media3.extractor.text.cea.Cea608Decoder;
import androidx.media3.extractor.text.cea.Cea708Decoder; import androidx.media3.extractor.text.cea.Cea708Decoder;
import androidx.media3.extractor.text.ttml.TtmlDecoder; import androidx.media3.extractor.text.ttml.TtmlDecoder;
import androidx.media3.extractor.text.webvtt.WebvttDecoder;
import java.util.Objects; import java.util.Objects;
/** A factory for {@link SubtitleDecoder} instances. */ /** 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: * <p>Supports formats supported by {@link DefaultSubtitleParserFactory} as well as the following:
* *
* <ul> * <ul>
* <li>WebVTT ({@link WebvttDecoder})
* <li>TTML ({@link TtmlDecoder}) * <li>TTML ({@link TtmlDecoder})
* <li>Cea608 ({@link Cea608Decoder}) * <li>Cea608 ({@link Cea608Decoder})
* <li>Cea708 ({@link Cea708Decoder}) * <li>Cea708 ({@link Cea708Decoder})
@ -72,7 +70,6 @@ public interface SubtitleDecoderFactory {
public boolean supportsFormat(Format format) { public boolean supportsFormat(Format format) {
@Nullable String mimeType = format.sampleMimeType; @Nullable String mimeType = format.sampleMimeType;
return delegate.supportsFormat(format) return delegate.supportsFormat(format)
|| Objects.equals(mimeType, MimeTypes.TEXT_VTT)
|| Objects.equals(mimeType, MimeTypes.APPLICATION_TTML) || Objects.equals(mimeType, MimeTypes.APPLICATION_TTML)
|| Objects.equals(mimeType, MimeTypes.APPLICATION_CEA608) || Objects.equals(mimeType, MimeTypes.APPLICATION_CEA608)
|| Objects.equals(mimeType, MimeTypes.APPLICATION_MP4CEA608) || Objects.equals(mimeType, MimeTypes.APPLICATION_MP4CEA608)
@ -90,8 +87,6 @@ public interface SubtitleDecoderFactory {
@Nullable String mimeType = format.sampleMimeType; @Nullable String mimeType = format.sampleMimeType;
if (mimeType != null) { if (mimeType != null) {
switch (mimeType) { switch (mimeType) {
case MimeTypes.TEXT_VTT:
return new WebvttDecoder();
case MimeTypes.APPLICATION_TTML: case MimeTypes.APPLICATION_TTML:
return new TtmlDecoder(); return new TtmlDecoder();
case MimeTypes.APPLICATION_CEA608: case MimeTypes.APPLICATION_CEA608:

View File

@ -1,11 +1,11 @@
/* /*
* Copyright (C) 2016 The Android Open Source Project * Copyright 2023 The Android Open Source Project
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package androidx.media3.extractor.text.webvtt; package androidx.media3.exoplayer.text;
import static androidx.media3.test.utils.truth.SpannedSubject.assertThat; import static androidx.media3.test.utils.truth.SpannedSubject.assertThat;
import static com.google.common.truth.Truth.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.Layout.Alignment;
import android.text.Spanned; import android.text.Spanned;
@ -25,7 +25,8 @@ import androidx.media3.common.text.Cue;
import androidx.media3.common.text.TextAnnotation; import androidx.media3.common.text.TextAnnotation;
import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.ColorParser; 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.media3.test.utils.TestUtil;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
@ -37,10 +38,9 @@ import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
/** Unit test for {@link WebvttDecoder}. */ /** Unit test for a {@link DelegatingSubtitleDecoder} backed by {@link WebvttParser}. */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class WebvttDecoderTest { public final class DelegatingSubtitleDecoderWithWebvttParserTest {
private static final String TYPICAL_FILE = "media/webvtt/typical"; private static final String TYPICAL_FILE = "media/webvtt/typical";
private static final String TYPICAL_WITH_BAD_TIMESTAMPS = private static final String TYPICAL_WITH_BAD_TIMESTAMPS =
"media/webvtt/typical_with_bad_timestamps"; "media/webvtt/typical_with_bad_timestamps";
@ -61,32 +61,27 @@ public class WebvttDecoderTest {
"media/webvtt/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 WITH_BOM = "media/webvtt/with_bom";
private static final String EMPTY_FILE = "media/webvtt/empty"; private static final String EMPTY_FILE = "media/webvtt/empty";
@Rule public final Expect expect = Expect.create(); @Rule public final Expect expect = Expect.create();
@Test @Test
public void decodeEmpty() throws IOException { public void decodeEmpty() throws IOException {
WebvttDecoder decoder = new WebvttDecoder(); DelegatingSubtitleDecoder decoder =
new DelegatingSubtitleDecoder(
"DelegatingSubtitleDecoderWithWebvttParser", new WebvttParser());
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), EMPTY_FILE); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), EMPTY_FILE);
try { assertThrows(
decoder.decode(bytes, bytes.length, /* reset= */ false); IllegalArgumentException.class,
fail(); () -> decoder.decode(bytes, bytes.length, /* reset= */ false));
} catch (SubtitleDecoderException expected) {
// Do nothing.
}
} }
@Test @Test
public void decodeTypical() throws Exception { public void decodeTypical() throws Exception {
WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_FILE); Subtitle subtitle = getSubtitleForTestAsset(TYPICAL_FILE);
assertThat(subtitle.getEventTimeCount()).isEqualTo(4); assertThat(subtitle.getEventTimeCount()).isEqualTo(4);
assertThat(subtitle.getEventTime(0)).isEqualTo(0L); assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L);
Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle.");
assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L);
assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L);
Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
@ -95,15 +90,12 @@ public class WebvttDecoderTest {
@Test @Test
public void decodeWithBom() throws Exception { public void decodeWithBom() throws Exception {
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_BOM); Subtitle subtitle = getSubtitleForTestAsset(WITH_BOM);
assertThat(subtitle.getEventTimeCount()).isEqualTo(4); assertThat(subtitle.getEventTimeCount()).isEqualTo(4);
assertThat(subtitle.getEventTime(0)).isEqualTo(0L); assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L);
Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle.");
assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L);
assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L);
Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
@ -112,15 +104,12 @@ public class WebvttDecoderTest {
@Test @Test
public void decodeTypicalWithBadTimestamps() throws Exception { 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.getEventTimeCount()).isEqualTo(4);
assertThat(subtitle.getEventTime(0)).isEqualTo(0L); assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L);
Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle.");
assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L);
assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L);
Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
@ -129,15 +118,12 @@ public class WebvttDecoderTest {
@Test @Test
public void decodeTypicalWithIds() throws Exception { 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.getEventTimeCount()).isEqualTo(4);
assertThat(subtitle.getEventTime(0)).isEqualTo(0L); assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L);
Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle.");
assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L);
assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L);
Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
@ -146,15 +132,12 @@ public class WebvttDecoderTest {
@Test @Test
public void decodeTypicalWithComments() throws Exception { 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.getEventTimeCount()).isEqualTo(4);
assertThat(subtitle.getEventTime(0)).isEqualTo(0L); assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
assertThat(subtitle.getEventTime(0 + 1)).isEqualTo(1_234_000L); assertThat(subtitle.getEventTime(0 + 1)).isEqualTo(1_234_000L);
Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle.");
assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L);
assertThat(subtitle.getEventTime(2 + 1)).isEqualTo(3_456_000L); assertThat(subtitle.getEventTime(2 + 1)).isEqualTo(3_456_000L);
Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
@ -163,25 +146,20 @@ public class WebvttDecoderTest {
@Test @Test
public void decodeWithTags() throws Exception { public void decodeWithTags() throws Exception {
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_TAGS_FILE); Subtitle subtitle = getSubtitleForTestAsset(WITH_TAGS_FILE);
assertThat(subtitle.getEventTimeCount()).isEqualTo(8); assertThat(subtitle.getEventTimeCount()).isEqualTo(8);
assertThat(subtitle.getEventTime(0)).isEqualTo(0L); assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L);
Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle.");
assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L);
assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L);
Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle."); assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle.");
assertThat(subtitle.getEventTime(4)).isEqualTo(4_000_000L); assertThat(subtitle.getEventTime(4)).isEqualTo(4_000_000L);
assertThat(subtitle.getEventTime(5)).isEqualTo(5_000_000L); assertThat(subtitle.getEventTime(5)).isEqualTo(5_000_000L);
Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4)));
assertThat(thirdCue.text.toString()).isEqualTo("This is the third subtitle."); assertThat(thirdCue.text.toString()).isEqualTo("This is the third subtitle.");
assertThat(subtitle.getEventTime(6)).isEqualTo(6_000_000L); assertThat(subtitle.getEventTime(6)).isEqualTo(6_000_000L);
assertThat(subtitle.getEventTime(7)).isEqualTo(7_000_000L); assertThat(subtitle.getEventTime(7)).isEqualTo(7_000_000L);
Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))); Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6)));
@ -190,10 +168,8 @@ public class WebvttDecoderTest {
@Test @Test
public void decodeWithPositioning() throws Exception { public void decodeWithPositioning() throws Exception {
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_POSITIONING_FILE); Subtitle subtitle = getSubtitleForTestAsset(WITH_POSITIONING_FILE);
assertThat(subtitle.getEventTimeCount()).isEqualTo(16); assertThat(subtitle.getEventTimeCount()).isEqualTo(16);
assertThat(subtitle.getEventTime(0)).isEqualTo(0L); assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L);
Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); 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.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_END);
assertThat(firstCue.textAlignment).isEqualTo(Alignment.ALIGN_NORMAL); assertThat(firstCue.textAlignment).isEqualTo(Alignment.ALIGN_NORMAL);
assertThat(firstCue.size).isEqualTo(0.35f); assertThat(firstCue.size).isEqualTo(0.35f);
// Unspecified values should use WebVTT defaults // Unspecified values should use WebVTT defaults
assertThat(firstCue.line).isEqualTo(-1f); assertThat(firstCue.line).isEqualTo(-1f);
assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_NUMBER); assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_NUMBER);
assertThat(firstCue.verticalType).isEqualTo(Cue.TYPE_UNSET); assertThat(firstCue.verticalType).isEqualTo(Cue.TYPE_UNSET);
assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L);
assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L);
Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
@ -215,7 +189,6 @@ public class WebvttDecoderTest {
// Position is invalid so defaults to 0.5 // Position is invalid so defaults to 0.5
assertThat(secondCue.position).isEqualTo(0.5f); assertThat(secondCue.position).isEqualTo(0.5f);
assertThat(secondCue.textAlignment).isEqualTo(Alignment.ALIGN_OPPOSITE); assertThat(secondCue.textAlignment).isEqualTo(Alignment.ALIGN_OPPOSITE);
assertThat(subtitle.getEventTime(4)).isEqualTo(4_000_000L); assertThat(subtitle.getEventTime(4)).isEqualTo(4_000_000L);
assertThat(subtitle.getEventTime(5)).isEqualTo(5_000_000L); assertThat(subtitle.getEventTime(5)).isEqualTo(5_000_000L);
Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4)));
@ -226,7 +199,6 @@ public class WebvttDecoderTest {
assertThat(thirdCue.textAlignment).isEqualTo(Alignment.ALIGN_CENTER); assertThat(thirdCue.textAlignment).isEqualTo(Alignment.ALIGN_CENTER);
// Derived from `align:middle`: // Derived from `align:middle`:
assertThat(thirdCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); assertThat(thirdCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE);
assertThat(subtitle.getEventTime(6)).isEqualTo(6_000_000L); assertThat(subtitle.getEventTime(6)).isEqualTo(6_000_000L);
assertThat(subtitle.getEventTime(7)).isEqualTo(7_000_000L); assertThat(subtitle.getEventTime(7)).isEqualTo(7_000_000L);
Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))); Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6)));
@ -237,7 +209,6 @@ public class WebvttDecoderTest {
// Derived from `align:middle`: // Derived from `align:middle`:
assertThat(fourthCue.position).isEqualTo(0.5f); assertThat(fourthCue.position).isEqualTo(0.5f);
assertThat(fourthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); assertThat(fourthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE);
assertThat(subtitle.getEventTime(8)).isEqualTo(8_000_000L); assertThat(subtitle.getEventTime(8)).isEqualTo(8_000_000L);
assertThat(subtitle.getEventTime(9)).isEqualTo(9_000_000L); assertThat(subtitle.getEventTime(9)).isEqualTo(9_000_000L);
Cue fifthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(8))); Cue fifthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(8)));
@ -246,7 +217,6 @@ public class WebvttDecoderTest {
// Derived from `align:right`: // Derived from `align:right`:
assertThat(fifthCue.position).isEqualTo(1.0f); assertThat(fifthCue.position).isEqualTo(1.0f);
assertThat(fifthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); assertThat(fifthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_END);
assertThat(subtitle.getEventTime(10)).isEqualTo(10_000_000L); assertThat(subtitle.getEventTime(10)).isEqualTo(10_000_000L);
assertThat(subtitle.getEventTime(11)).isEqualTo(11_000_000L); assertThat(subtitle.getEventTime(11)).isEqualTo(11_000_000L);
Cue sixthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(10))); Cue sixthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(10)));
@ -255,13 +225,11 @@ public class WebvttDecoderTest {
// Derived from `align:center`: // Derived from `align:center`:
assertThat(sixthCue.position).isEqualTo(0.5f); assertThat(sixthCue.position).isEqualTo(0.5f);
assertThat(sixthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); assertThat(sixthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE);
assertThat(subtitle.getEventTime(12)).isEqualTo(12_000_000L); assertThat(subtitle.getEventTime(12)).isEqualTo(12_000_000L);
assertThat(subtitle.getEventTime(13)).isEqualTo(13_000_000L); assertThat(subtitle.getEventTime(13)).isEqualTo(13_000_000L);
Cue seventhCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(12))); Cue seventhCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(12)));
assertThat(seventhCue.text.toString()).isEqualTo("This is the seventh subtitle."); assertThat(seventhCue.text.toString()).isEqualTo("This is the seventh subtitle.");
assertThat(seventhCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_START); assertThat(seventhCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_START);
assertThat(subtitle.getEventTime(14)).isEqualTo(14_000_000L); assertThat(subtitle.getEventTime(14)).isEqualTo(14_000_000L);
assertThat(subtitle.getEventTime(15)).isEqualTo(15_000_000L); assertThat(subtitle.getEventTime(15)).isEqualTo(15_000_000L);
Cue eighthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(14))); Cue eighthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(14)));
@ -271,15 +239,12 @@ public class WebvttDecoderTest {
@Test @Test
public void decodeWithOverlappingTimestamps() throws Exception { public void decodeWithOverlappingTimestamps() throws Exception {
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_OVERLAPPING_TIMESTAMPS_FILE); Subtitle subtitle = getSubtitleForTestAsset(WITH_OVERLAPPING_TIMESTAMPS_FILE);
assertThat(subtitle.getEventTimeCount()).isEqualTo(8); assertThat(subtitle.getEventTimeCount()).isEqualTo(8);
Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
assertThat(firstCue.text.toString()).isEqualTo("Displayed at the bottom for 3 seconds."); assertThat(firstCue.text.toString()).isEqualTo("Displayed at the bottom for 3 seconds.");
assertThat(firstCue.line).isEqualTo(-1f); assertThat(firstCue.line).isEqualTo(-1f);
assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_NUMBER); assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_NUMBER);
List<Cue> firstAndSecondCue = subtitle.getCues(subtitle.getEventTime(1)); List<Cue> firstAndSecondCue = subtitle.getCues(subtitle.getEventTime(1));
assertThat(firstAndSecondCue).hasSize(2); assertThat(firstAndSecondCue).hasSize(2);
assertThat(firstAndSecondCue.get(0).text.toString()) assertThat(firstAndSecondCue.get(0).text.toString())
@ -290,12 +255,10 @@ public class WebvttDecoderTest {
.isEqualTo("Appears directly above for 1 second."); .isEqualTo("Appears directly above for 1 second.");
assertThat(firstAndSecondCue.get(1).line).isEqualTo(-2f); assertThat(firstAndSecondCue.get(1).line).isEqualTo(-2f);
assertThat(firstAndSecondCue.get(1).lineType).isEqualTo(Cue.LINE_TYPE_NUMBER); assertThat(firstAndSecondCue.get(1).lineType).isEqualTo(Cue.LINE_TYPE_NUMBER);
Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4)));
assertThat(thirdCue.text.toString()).isEqualTo("Displayed at the bottom for 2 seconds."); assertThat(thirdCue.text.toString()).isEqualTo("Displayed at the bottom for 2 seconds.");
assertThat(thirdCue.line).isEqualTo(-1f); assertThat(thirdCue.line).isEqualTo(-1f);
assertThat(thirdCue.lineType).isEqualTo(Cue.LINE_TYPE_NUMBER); assertThat(thirdCue.lineType).isEqualTo(Cue.LINE_TYPE_NUMBER);
List<Cue> thirdAndFourthCue = subtitle.getCues(subtitle.getEventTime(5)); List<Cue> thirdAndFourthCue = subtitle.getCues(subtitle.getEventTime(5));
assertThat(thirdAndFourthCue).hasSize(2); assertThat(thirdAndFourthCue).hasSize(2);
assertThat(thirdAndFourthCue.get(0).text.toString()) 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."); .isEqualTo("Appears directly above the previous cue, then replaces it after 1 second.");
assertThat(thirdAndFourthCue.get(1).line).isEqualTo(-2f); assertThat(thirdAndFourthCue.get(1).line).isEqualTo(-2f);
assertThat(thirdAndFourthCue.get(1).lineType).isEqualTo(Cue.LINE_TYPE_NUMBER); assertThat(thirdAndFourthCue.get(1).lineType).isEqualTo(Cue.LINE_TYPE_NUMBER);
Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))); Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6)));
assertThat(fourthCue.text.toString()) assertThat(fourthCue.text.toString())
.isEqualTo("Appears directly above the previous cue, then replaces it after 1 second."); .isEqualTo("Appears directly above the previous cue, then replaces it after 1 second.");
@ -316,22 +278,18 @@ public class WebvttDecoderTest {
@Test @Test
public void decodeWithVertical() throws Exception { public void decodeWithVertical() throws Exception {
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_VERTICAL_FILE); Subtitle subtitle = getSubtitleForTestAsset(WITH_VERTICAL_FILE);
assertThat(subtitle.getEventTimeCount()).isEqualTo(6); assertThat(subtitle.getEventTimeCount()).isEqualTo(6);
assertThat(subtitle.getEventTime(0)).isEqualTo(0L); assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L);
Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
assertThat(firstCue.text.toString()).isEqualTo("Vertical right-to-left (e.g. Japanese)"); assertThat(firstCue.text.toString()).isEqualTo("Vertical right-to-left (e.g. Japanese)");
assertThat(firstCue.verticalType).isEqualTo(Cue.VERTICAL_TYPE_RL); assertThat(firstCue.verticalType).isEqualTo(Cue.VERTICAL_TYPE_RL);
assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L);
assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L);
Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
assertThat(secondCue.text.toString()).isEqualTo("Vertical left-to-right (e.g. Mongolian)"); assertThat(secondCue.text.toString()).isEqualTo("Vertical left-to-right (e.g. Mongolian)");
assertThat(secondCue.verticalType).isEqualTo(Cue.VERTICAL_TYPE_LR); assertThat(secondCue.verticalType).isEqualTo(Cue.VERTICAL_TYPE_LR);
assertThat(subtitle.getEventTime(4)).isEqualTo(4_000_000L); assertThat(subtitle.getEventTime(4)).isEqualTo(4_000_000L);
assertThat(subtitle.getEventTime(5)).isEqualTo(5_000_000L); assertThat(subtitle.getEventTime(5)).isEqualTo(5_000_000L);
Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4)));
@ -341,17 +299,14 @@ public class WebvttDecoderTest {
@Test @Test
public void decodeWithRubies() throws Exception { public void decodeWithRubies() throws Exception {
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_RUBIES_FILE); Subtitle subtitle = getSubtitleForTestAsset(WITH_RUBIES_FILE);
assertThat(subtitle.getEventTimeCount()).isEqualTo(8); assertThat(subtitle.getEventTimeCount()).isEqualTo(8);
// Check that an explicit `over` position is read from CSS. // Check that an explicit `over` position is read from CSS.
Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
assertThat(firstCue.text.toString()).isEqualTo("Some text with over-ruby."); assertThat(firstCue.text.toString()).isEqualTo("Some text with over-ruby.");
assertThat((Spanned) firstCue.text) assertThat((Spanned) firstCue.text)
.hasRubySpanBetween("Some ".length(), "Some text with over-ruby".length()) .hasRubySpanBetween("Some ".length(), "Some text with over-ruby".length())
.withTextAndPosition("over", TextAnnotation.POSITION_BEFORE); .withTextAndPosition("over", TextAnnotation.POSITION_BEFORE);
// Check that `under` is read from CSS and unspecified defaults to `over`. // Check that `under` is read from CSS and unspecified defaults to `over`.
Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
assertThat(secondCue.text.toString()) assertThat(secondCue.text.toString())
@ -364,7 +319,6 @@ public class WebvttDecoderTest {
"Some text with under-ruby and ".length(), "Some text with under-ruby and ".length(),
"Some text with under-ruby and over-ruby (default)".length()) "Some text with under-ruby and over-ruby (default)".length())
.withTextAndPosition("over", TextAnnotation.POSITION_BEFORE); .withTextAndPosition("over", TextAnnotation.POSITION_BEFORE);
// Check many <rt> tags with different positions nested in a single <ruby> span. // Check many <rt> tags with different positions nested in a single <ruby> span.
Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4)));
assertThat(thirdCue.text.toString()).isEqualTo("base1base2base3."); assertThat(thirdCue.text.toString()).isEqualTo("base1base2base3.");
@ -377,7 +331,6 @@ public class WebvttDecoderTest {
assertThat((Spanned) thirdCue.text) assertThat((Spanned) thirdCue.text)
.hasRubySpanBetween("base1base2".length(), "base1base2base3".length()) .hasRubySpanBetween("base1base2".length(), "base1base2base3".length())
.withTextAndPosition("under3", TextAnnotation.POSITION_AFTER); .withTextAndPosition("under3", TextAnnotation.POSITION_AFTER);
// Check a <ruby> span with no <rt> tags. // Check a <ruby> span with no <rt> tags.
Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))); Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6)));
assertThat(fourthCue.text.toString()).isEqualTo("Some text with no ruby text."); assertThat(fourthCue.text.toString()).isEqualTo("Some text with no ruby text.");
@ -386,15 +339,12 @@ public class WebvttDecoderTest {
@Test @Test
public void decodeWithBadCueHeader() throws Exception { 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.getEventTimeCount()).isEqualTo(4);
assertThat(subtitle.getEventTime(0)).isEqualTo(0L); assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L);
Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle.");
assertThat(subtitle.getEventTime(2)).isEqualTo(4_000_000L); assertThat(subtitle.getEventTime(2)).isEqualTo(4_000_000L);
assertThat(subtitle.getEventTime(3)).isEqualTo(5_000_000L); assertThat(subtitle.getEventTime(3)).isEqualTo(5_000_000L);
Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
@ -403,10 +353,8 @@ public class WebvttDecoderTest {
@Test @Test
public void decodeWithCssFontSizeStyle() throws Exception { public void decodeWithCssFontSizeStyle() throws Exception {
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_FONT_SIZE); Subtitle subtitle = getSubtitleForTestAsset(WITH_FONT_SIZE);
assertThat(subtitle.getEventTimeCount()).isEqualTo(12); assertThat(subtitle.getEventTimeCount()).isEqualTo(12);
assertThat(subtitle.getEventTime(0)).isEqualTo(0L); assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
assertThat(subtitle.getEventTime(1)).isEqualTo(2_000_000L); assertThat(subtitle.getEventTime(1)).isEqualTo(2_000_000L);
Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
@ -414,13 +362,11 @@ public class WebvttDecoderTest {
assertThat((Spanned) firstCue.text) assertThat((Spanned) firstCue.text)
.hasRelativeSizeSpanBetween(0, "Sentence with font-size set to 4.4em.".length()) .hasRelativeSizeSpanBetween(0, "Sentence with font-size set to 4.4em.".length())
.withSizeChange(4.4f); .withSizeChange(4.4f);
assertThat(subtitle.getEventTime(2)).isEqualTo(2_100_000L); assertThat(subtitle.getEventTime(2)).isEqualTo(2_100_000L);
assertThat(subtitle.getEventTime(3)).isEqualTo(2_400_000L); assertThat(subtitle.getEventTime(3)).isEqualTo(2_400_000L);
Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
assertThat(secondCue.text.toString()).isEqualTo("Sentence with bad font-size unit."); assertThat(secondCue.text.toString()).isEqualTo("Sentence with bad font-size unit.");
assertThat((Spanned) secondCue.text).hasNoSpans(); assertThat((Spanned) secondCue.text).hasNoSpans();
assertThat(subtitle.getEventTime(4)).isEqualTo(2_500_000L); assertThat(subtitle.getEventTime(4)).isEqualTo(2_500_000L);
assertThat(subtitle.getEventTime(5)).isEqualTo(4_000_000L); assertThat(subtitle.getEventTime(5)).isEqualTo(4_000_000L);
Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4)));
@ -428,7 +374,6 @@ public class WebvttDecoderTest {
assertThat((Spanned) thirdCue.text) assertThat((Spanned) thirdCue.text)
.hasAbsoluteSizeSpanBetween(0, "Absolute font-size expressed in px unit!".length()) .hasAbsoluteSizeSpanBetween(0, "Absolute font-size expressed in px unit!".length())
.withAbsoluteSize(2); .withAbsoluteSize(2);
assertThat(subtitle.getEventTime(6)).isEqualTo(4_500_000L); assertThat(subtitle.getEventTime(6)).isEqualTo(4_500_000L);
assertThat(subtitle.getEventTime(7)).isEqualTo(6_000_000L); assertThat(subtitle.getEventTime(7)).isEqualTo(6_000_000L);
Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))); Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6)));
@ -436,13 +381,11 @@ public class WebvttDecoderTest {
assertThat((Spanned) fourthCue.text) assertThat((Spanned) fourthCue.text)
.hasRelativeSizeSpanBetween(0, "Relative font-size expressed in % unit!".length()) .hasRelativeSizeSpanBetween(0, "Relative font-size expressed in % unit!".length())
.withSizeChange(0.035f); .withSizeChange(0.035f);
assertThat(subtitle.getEventTime(8)).isEqualTo(6_100_000L); assertThat(subtitle.getEventTime(8)).isEqualTo(6_100_000L);
assertThat(subtitle.getEventTime(9)).isEqualTo(6_400_000L); assertThat(subtitle.getEventTime(9)).isEqualTo(6_400_000L);
Cue fifthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(8))); Cue fifthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(8)));
assertThat(fifthCue.text.toString()).isEqualTo("Sentence with bad font-size value."); assertThat(fifthCue.text.toString()).isEqualTo("Sentence with bad font-size value.");
assertThat((Spanned) secondCue.text).hasNoSpans(); assertThat((Spanned) secondCue.text).hasNoSpans();
assertThat(subtitle.getEventTime(10)).isEqualTo(6_500_000L); assertThat(subtitle.getEventTime(10)).isEqualTo(6_500_000L);
assertThat(subtitle.getEventTime(11)).isEqualTo(8_000_000L); assertThat(subtitle.getEventTime(11)).isEqualTo(8_000_000L);
Cue sixthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(10))); Cue sixthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(10)));
@ -455,8 +398,7 @@ public class WebvttDecoderTest {
@Test @Test
public void webvttWithCssStyle() throws Exception { public void webvttWithCssStyle() throws Exception {
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_STYLES); Subtitle subtitle = getSubtitleForTestAsset(WITH_CSS_STYLES);
Spanned firstCueText = getUniqueSpanTextAt(subtitle, 0); Spanned firstCueText = getUniqueSpanTextAt(subtitle, 0);
assertThat(firstCueText.toString()).isEqualTo("This is the first subtitle."); assertThat(firstCueText.toString()).isEqualTo("This is the first subtitle.");
assertThat(firstCueText) assertThat(firstCueText)
@ -465,17 +407,14 @@ public class WebvttDecoderTest {
assertThat(firstCueText) assertThat(firstCueText)
.hasBackgroundColorSpanBetween(0, firstCueText.length()) .hasBackgroundColorSpanBetween(0, firstCueText.length())
.withColor(ColorParser.parseCssColor("green")); .withColor(ColorParser.parseCssColor("green"));
Spanned secondCueText = getUniqueSpanTextAt(subtitle, 2_345_000); Spanned secondCueText = getUniqueSpanTextAt(subtitle, 2_345_000);
assertThat(secondCueText.toString()).isEqualTo("This is the second subtitle."); assertThat(secondCueText.toString()).isEqualTo("This is the second subtitle.");
assertThat(secondCueText) assertThat(secondCueText)
.hasForegroundColorSpanBetween(0, secondCueText.length()) .hasForegroundColorSpanBetween(0, secondCueText.length())
.withColor(ColorParser.parseCssColor("peachpuff")); .withColor(ColorParser.parseCssColor("peachpuff"));
Spanned thirdCueText = getUniqueSpanTextAt(subtitle, 20_000_000); Spanned thirdCueText = getUniqueSpanTextAt(subtitle, 20_000_000);
assertThat(thirdCueText.toString()).isEqualTo("This is a reference by element"); assertThat(thirdCueText.toString()).isEqualTo("This is a reference by element");
assertThat(thirdCueText).hasUnderlineSpanBetween("This is a ".length(), thirdCueText.length()); assertThat(thirdCueText).hasUnderlineSpanBetween("This is a ".length(), thirdCueText.length());
Spanned fourthCueText = getUniqueSpanTextAt(subtitle, 25_000_000); Spanned fourthCueText = getUniqueSpanTextAt(subtitle, 25_000_000);
assertThat(fourthCueText.toString()).isEqualTo("You are an idiot\nYou don't have the guts"); assertThat(fourthCueText.toString()).isEqualTo("You are an idiot\nYou don't have the guts");
assertThat(fourthCueText) assertThat(fourthCueText)
@ -487,7 +426,7 @@ public class WebvttDecoderTest {
@Test @Test
public void withComplexCssSelectors() throws Exception { 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); Spanned firstCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 0);
assertThat(firstCueText).hasUnderlineSpanBetween(0, firstCueText.length()); assertThat(firstCueText).hasUnderlineSpanBetween(0, firstCueText.length());
assertThat(firstCueText) assertThat(firstCueText)
@ -497,26 +436,22 @@ public class WebvttDecoderTest {
assertThat(firstCueText) assertThat(firstCueText)
.hasTypefaceSpanBetween("This should be underlined and ".length(), firstCueText.length()) .hasTypefaceSpanBetween("This should be underlined and ".length(), firstCueText.length())
.withFamily("courier"); .withFamily("courier");
Spanned secondCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2_000_000); Spanned secondCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2_000_000);
assertThat(secondCueText) assertThat(secondCueText)
.hasTypefaceSpanBetween("This ".length(), secondCueText.length()) .hasTypefaceSpanBetween("This ".length(), secondCueText.length())
.withFamily("courier"); .withFamily("courier");
assertThat(secondCueText) assertThat(secondCueText)
.hasNoForegroundColorSpanBetween("This ".length(), secondCueText.length()); .hasNoForegroundColorSpanBetween("This ".length(), secondCueText.length());
Spanned thirdCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2_500_000); Spanned thirdCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2_500_000);
assertThat(thirdCueText).hasBoldSpanBetween("This ".length(), thirdCueText.length()); assertThat(thirdCueText).hasBoldSpanBetween("This ".length(), thirdCueText.length());
assertThat(thirdCueText) assertThat(thirdCueText)
.hasTypefaceSpanBetween("This ".length(), thirdCueText.length()) .hasTypefaceSpanBetween("This ".length(), thirdCueText.length())
.withFamily("courier"); .withFamily("courier");
Spanned fourthCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 4_000_000); Spanned fourthCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 4_000_000);
assertThat(fourthCueText) assertThat(fourthCueText)
.hasNoStyleSpanBetween("This ".length(), "shouldn't be bold.".length()); .hasNoStyleSpanBetween("This ".length(), "shouldn't be bold.".length());
assertThat(fourthCueText) assertThat(fourthCueText)
.hasBoldSpanBetween("This shouldn't be bold.\nThis ".length(), fourthCueText.length()); .hasBoldSpanBetween("This shouldn't be bold.\nThis ".length(), fourthCueText.length());
Spanned fifthCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 5_000_000); Spanned fifthCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 5_000_000);
assertThat(fifthCueText) assertThat(fifthCueText)
.hasNoStyleSpanBetween("This is ".length(), "This is specific".length()); .hasNoStyleSpanBetween("This is ".length(), "This is specific".length());
@ -526,26 +461,25 @@ public class WebvttDecoderTest {
@Test @Test
public void webvttWithCssTextCombineUpright() throws Exception { 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); Spanned firstCueText = getUniqueSpanTextAt(subtitle, 500_000);
assertThat(firstCueText) assertThat(firstCueText)
.hasHorizontalTextInVerticalContextSpanBetween("Combine ".length(), "Combine all".length()); .hasHorizontalTextInVerticalContextSpanBetween("Combine ".length(), "Combine all".length());
Spanned secondCueText = getUniqueSpanTextAt(subtitle, 3_500_000); Spanned secondCueText = getUniqueSpanTextAt(subtitle, 3_500_000);
assertThat(secondCueText) assertThat(secondCueText)
.hasHorizontalTextInVerticalContextSpanBetween( .hasHorizontalTextInVerticalContextSpanBetween(
"Combine ".length(), "Combine 0004".length()); "Combine ".length(), "Combine 0004".length());
} }
private WebvttSubtitle getSubtitleForTestAsset(String asset) private Subtitle getSubtitleForTestAsset(String asset) throws IOException {
throws IOException, SubtitleDecoderException { DelegatingSubtitleDecoder decoder =
WebvttDecoder decoder = new WebvttDecoder(); new DelegatingSubtitleDecoder(
"DelegatingSubtitleDecoderWithWebvttParser", new WebvttParser());
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), asset); 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); return (Spanned) Assertions.checkNotNull(sub.getCues(timeUs).get(0).text);
} }
} }

View File

@ -25,6 +25,7 @@ import androidx.media3.extractor.text.ssa.SsaParser;
import androidx.media3.extractor.text.subrip.SubripParser; import androidx.media3.extractor.text.subrip.SubripParser;
import androidx.media3.extractor.text.tx3g.Tx3gParser; import androidx.media3.extractor.text.tx3g.Tx3gParser;
import androidx.media3.extractor.text.webvtt.Mp4WebvttParser; import androidx.media3.extractor.text.webvtt.Mp4WebvttParser;
import androidx.media3.extractor.text.webvtt.WebvttParser;
import java.util.Objects; import java.util.Objects;
/** /**
@ -33,9 +34,10 @@ import java.util.Objects;
* <p>The formats supported by this factory are: * <p>The formats supported by this factory are:
* *
* <ul> * <ul>
* <li>SSA/ASS ({@link SsaParser})
* <li>WebVTT ({@link WebvttParser})
* <li>WebVTT (MP4) ({@link Mp4WebvttParser}) * <li>WebVTT (MP4) ({@link Mp4WebvttParser})
* <li>SubRip ({@link SubripParser}) * <li>SubRip ({@link SubripParser})
* <li>SSA/ASS ({@link SsaParser})
* <li>TX3G ({@link Tx3gParser}) * <li>TX3G ({@link Tx3gParser})
* <li>PGS ({@link PgsParser}) * <li>PGS ({@link PgsParser})
* <li>DVB ({@link DvbParser}) * <li>DVB ({@link DvbParser})
@ -48,6 +50,7 @@ public final class DefaultSubtitleParserFactory implements SubtitleParser.Factor
public boolean supportsFormat(Format format) { public boolean supportsFormat(Format format) {
@Nullable String mimeType = format.sampleMimeType; @Nullable String mimeType = format.sampleMimeType;
return Objects.equals(mimeType, MimeTypes.TEXT_SSA) return Objects.equals(mimeType, MimeTypes.TEXT_SSA)
|| Objects.equals(mimeType, MimeTypes.TEXT_VTT)
|| Objects.equals(mimeType, MimeTypes.APPLICATION_MP4VTT) || Objects.equals(mimeType, MimeTypes.APPLICATION_MP4VTT)
|| Objects.equals(mimeType, MimeTypes.APPLICATION_SUBRIP) || Objects.equals(mimeType, MimeTypes.APPLICATION_SUBRIP)
|| Objects.equals(mimeType, MimeTypes.APPLICATION_TX3G) || Objects.equals(mimeType, MimeTypes.APPLICATION_TX3G)
@ -62,6 +65,8 @@ public final class DefaultSubtitleParserFactory implements SubtitleParser.Factor
switch (mimeType) { switch (mimeType) {
case MimeTypes.TEXT_SSA: case MimeTypes.TEXT_SSA:
return new SsaParser(format.initializationData); return new SsaParser(format.initializationData);
case MimeTypes.TEXT_VTT:
return new WebvttParser();
case MimeTypes.APPLICATION_MP4VTT: case MimeTypes.APPLICATION_MP4VTT:
return new Mp4WebvttParser(); return new Mp4WebvttParser();
case MimeTypes.APPLICATION_SUBRIP: case MimeTypes.APPLICATION_SUBRIP:

View File

@ -20,19 +20,19 @@ import androidx.annotation.Nullable;
import androidx.media3.common.ParserException; import androidx.media3.common.ParserException;
import androidx.media3.common.util.ParsableByteArray; import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.extractor.text.SimpleSubtitleDecoder; import androidx.media3.extractor.text.CuesWithTiming;
import androidx.media3.extractor.text.Subtitle; import androidx.media3.extractor.text.SubtitleParser;
import androidx.media3.extractor.text.SubtitleDecoderException; import com.google.common.collect.ImmutableList;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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>. * <p>See the <a href="http://dev.w3.org/html5/webvtt">WebVTT specification</a>.
*/ */
@UnstableApi @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_NONE = -1;
private static final int EVENT_END_OF_FILE = 0; 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 ParsableByteArray parsableWebvttData;
private final WebvttCssParser cssParser; private final WebvttCssParser cssParser;
public WebvttDecoder() { public WebvttParser() {
super("WebvttDecoder");
parsableWebvttData = new ParsableByteArray(); parsableWebvttData = new ParsableByteArray();
cssParser = new WebvttCssParser(); cssParser = new WebvttCssParser();
} }
@Override @Override
protected Subtitle decode(byte[] data, int length, boolean reset) public ImmutableList<CuesWithTiming> parse(byte[] data, int offset, int length) {
throws SubtitleDecoderException {
parsableWebvttData.reset(data, length); parsableWebvttData.reset(data, length);
parsableWebvttData.setPosition(offset);
List<WebvttCssStyle> definedStyles = new ArrayList<>(); List<WebvttCssStyle> definedStyles = new ArrayList<>();
// Validate the first line of the header, and skip the remainder. // Validate the first line of the header, and skip the remainder.
try { try {
WebvttParserUtil.validateWebvttHeaderLine(parsableWebvttData); WebvttParserUtil.validateWebvttHeaderLine(parsableWebvttData);
} catch (ParserException e) { } catch (ParserException e) {
throw new SubtitleDecoderException(e); throw new IllegalArgumentException(e);
} }
while (!TextUtils.isEmpty(parsableWebvttData.readLine())) {} while (!TextUtils.isEmpty(parsableWebvttData.readLine())) {}
@ -73,7 +72,7 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder {
skipComment(parsableWebvttData); skipComment(parsableWebvttData);
} else if (event == EVENT_STYLE_BLOCK) { } else if (event == EVENT_STYLE_BLOCK) {
if (!cueInfos.isEmpty()) { 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. parsableWebvttData.readLine(); // Consume the "STYLE" header.
definedStyles.addAll(cssParser.parseBlock(parsableWebvttData)); 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 * 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. * consume any data from such event, if any.

View File

@ -21,8 +21,9 @@ import static org.junit.Assert.assertThrows;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.text.DelegatingSubtitleDecoder;
import androidx.media3.extractor.Extractor; 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.FakeExtractorInput;
import androidx.media3.test.utils.FakeExtractorOutput; import androidx.media3.test.utils.FakeExtractorOutput;
import androidx.media3.test.utils.FakeTrackOutput; import androidx.media3.test.utils.FakeTrackOutput;
@ -64,7 +65,8 @@ public class SubtitleExtractorTest {
.build(); .build();
SubtitleExtractor extractor = SubtitleExtractor extractor =
new SubtitleExtractor( new SubtitleExtractor(
new WebvttDecoder(), new DelegatingSubtitleDecoder(
"DelegatingSubtitleDecoderWithWebvttParser", new WebvttParser()),
new Format.Builder().setSampleMimeType(MimeTypes.TEXT_VTT).build()); new Format.Builder().setSampleMimeType(MimeTypes.TEXT_VTT).build());
extractor.init(output); extractor.init(output);
@ -107,7 +109,8 @@ public class SubtitleExtractorTest {
.build(); .build();
SubtitleExtractor extractor = SubtitleExtractor extractor =
new SubtitleExtractor( new SubtitleExtractor(
new WebvttDecoder(), new DelegatingSubtitleDecoder(
"DelegatingSubtitleDecoderWithWebvttParser", new WebvttParser()),
new Format.Builder().setSampleMimeType(MimeTypes.TEXT_VTT).build()); new Format.Builder().setSampleMimeType(MimeTypes.TEXT_VTT).build());
extractor.init(output); extractor.init(output);
FakeTrackOutput trackOutput = output.trackOutputs.get(0); FakeTrackOutput trackOutput = output.trackOutputs.get(0);
@ -149,7 +152,8 @@ public class SubtitleExtractorTest {
.build(); .build();
SubtitleExtractor extractor = SubtitleExtractor extractor =
new SubtitleExtractor( new SubtitleExtractor(
new WebvttDecoder(), new DelegatingSubtitleDecoder(
"DelegatingSubtitleDecoderWithWebvttParser", new WebvttParser()),
new Format.Builder().setSampleMimeType(MimeTypes.TEXT_VTT).build()); new Format.Builder().setSampleMimeType(MimeTypes.TEXT_VTT).build());
extractor.init(output); extractor.init(output);
FakeTrackOutput trackOutput = output.trackOutputs.get(0); FakeTrackOutput trackOutput = output.trackOutputs.get(0);
@ -185,7 +189,10 @@ public class SubtitleExtractorTest {
public void read_withoutInit_fails() { public void read_withoutInit_fails() {
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(new byte[0]).build(); FakeExtractorInput input = new FakeExtractorInput.Builder().setData(new byte[0]).build();
SubtitleExtractor extractor = 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)); assertThrows(IllegalStateException.class, () -> extractor.read(input, null));
} }
@ -194,7 +201,10 @@ public class SubtitleExtractorTest {
public void read_afterRelease_fails() { public void read_afterRelease_fails() {
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(new byte[0]).build(); FakeExtractorInput input = new FakeExtractorInput.Builder().setData(new byte[0]).build();
SubtitleExtractor extractor = 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(); FakeExtractorOutput output = new FakeExtractorOutput();
extractor.init(output); extractor.init(output);
@ -206,7 +216,10 @@ public class SubtitleExtractorTest {
@Test @Test
public void seek_withoutInit_fails() { public void seek_withoutInit_fails() {
SubtitleExtractor extractor = 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)); assertThrows(IllegalStateException.class, () -> extractor.seek(0, 0));
} }
@ -214,7 +227,10 @@ public class SubtitleExtractorTest {
@Test @Test
public void seek_afterRelease_fails() { public void seek_afterRelease_fails() {
SubtitleExtractor extractor = 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(); FakeExtractorOutput output = new FakeExtractorOutput();
extractor.init(output); extractor.init(output);
@ -226,7 +242,10 @@ public class SubtitleExtractorTest {
@Test @Test
public void released_calledTwice() { public void released_calledTwice() {
SubtitleExtractor extractor = 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(); FakeExtractorOutput output = new FakeExtractorOutput();
extractor.init(output); extractor.init(output);

View File

@ -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);
}
}