blob: 4bd26b8da216eac358d690cc77cdad5c16fab826 [file] [log] [blame]
/*
* 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 com.google.android.exoplayer2.text.webvtt;
import static com.google.android.exoplayer2.testutil.truth.SpannedSubject.assertThat;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import android.text.Layout.Alignment;
import android.text.Spanned;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.SubtitleDecoderException;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ColorParser;
import com.google.common.collect.Iterables;
import com.google.common.truth.Expect;
import java.io.IOException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link WebvttDecoder}. */
@RunWith(AndroidJUnit4.class)
public class WebvttDecoderTest {
private static final String TYPICAL_FILE = "webvtt/typical";
private static final String TYPICAL_WITH_BAD_TIMESTAMPS = "webvtt/typical_with_bad_timestamps";
private static final String TYPICAL_WITH_IDS_FILE = "webvtt/typical_with_identifiers";
private static final String TYPICAL_WITH_COMMENTS_FILE = "webvtt/typical_with_comments";
private static final String WITH_POSITIONING_FILE = "webvtt/with_positioning";
private static final String WITH_VERTICAL_FILE = "webvtt/with_vertical";
private static final String WITH_BAD_CUE_HEADER_FILE = "webvtt/with_bad_cue_header";
private static final String WITH_TAGS_FILE = "webvtt/with_tags";
private static final String WITH_CSS_STYLES = "webvtt/with_css_styles";
private static final String WITH_CSS_COMPLEX_SELECTORS = "webvtt/with_css_complex_selectors";
private static final String WITH_CSS_TEXT_COMBINE_UPRIGHT =
"webvtt/with_css_text_combine_upright";
private static final String WITH_BOM = "webvtt/with_bom";
private static final String EMPTY_FILE = "webvtt/empty";
@Rule public final Expect expect = Expect.create();
@Test
public void decodeEmpty() throws IOException {
WebvttDecoder decoder = new WebvttDecoder();
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), EMPTY_FILE);
try {
decoder.decode(bytes, bytes.length, /* reset= */ false);
fail();
} catch (SubtitleDecoderException expected) {
// Do nothing.
}
}
@Test
public void decodeTypical() throws Exception {
WebvttSubtitle 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)));
assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle.");
}
@Test
public void decodeWithBom() throws Exception {
WebvttSubtitle 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)));
assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle.");
}
@Test
public void decodeTypicalWithBadTimestamps() throws Exception {
WebvttSubtitle 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)));
assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle.");
}
@Test
public void decodeTypicalWithIds() throws Exception {
WebvttSubtitle 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)));
assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle.");
}
@Test
public void decodeTypicalWithComments() throws Exception {
WebvttSubtitle 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)));
assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle.");
}
@Test
public void decodeWithTags() throws Exception {
WebvttSubtitle 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)));
assertThat(fourthCue.text.toString()).isEqualTo("This is the <fourth> &subtitle.");
}
@Test
public void decodeWithPositioning() throws Exception {
WebvttSubtitle 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)));
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(Cue.DIMEN_UNSET);
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)));
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(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(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(subtitle.getEventTime(6)).isEqualTo(6_000_000L);
assertThat(subtitle.getEventTime(7)).isEqualTo(7_000_000L);
Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6)));
assertThat(fourthCue.text.toString()).isEqualTo("This is the fourth subtitle.");
assertThat(fourthCue.line).isEqualTo(-11f);
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(subtitle.getEventTime(8)).isEqualTo(8_000_000L);
assertThat(subtitle.getEventTime(9)).isEqualTo(9_000_000L);
Cue fifthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(8)));
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(subtitle.getEventTime(10)).isEqualTo(10_000_000L);
assertThat(subtitle.getEventTime(11)).isEqualTo(11_000_000L);
Cue sixthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(10)));
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(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)));
assertThat(eighthCue.text.toString()).isEqualTo("This is the eighth subtitle.");
assertThat(eighthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_END);
}
@Test
public void decodeWithVertical() throws Exception {
WebvttSubtitle 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)));
assertThat(thirdCue.text.toString()).isEqualTo("No vertical setting (i.e. horizontal)");
assertThat(thirdCue.verticalType).isEqualTo(Cue.TYPE_UNSET);
}
@Test
public void decodeWithBadCueHeader() throws Exception {
WebvttSubtitle 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)));
assertThat(secondCue.text.toString()).isEqualTo("This is the third subtitle.");
}
@Test
public void webvttWithCssStyle() throws Exception {
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_STYLES);
Spanned firstCueText = getUniqueSpanTextAt(subtitle, 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(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)
.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 {
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_COMPLEX_SELECTORS);
Spanned firstCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 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(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());
assertThat(fifthCueText)
.hasItalicSpanBetween("This is specific\n".length(), fifthCueText.length());
}
@Test
public void webvttWithCssTextCombineUpright() throws Exception {
WebvttSubtitle 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();
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), asset);
return (WebvttSubtitle) decoder.decode(bytes, bytes.length, /* reset= */ false);
}
private Spanned getUniqueSpanTextAt(WebvttSubtitle sub, long timeUs) {
return (Spanned) Assertions.checkNotNull(sub.getCues(timeUs).get(0).text);
}
}