Change LegacySubtitleUtil handling of SubtitleParser.OutputOptions

If the `Subtitle` has 'active' cues at `OutputOptions.startTimeUs`, this
change ensures these are emitted in a `CuesWithTiming` with
`CuesWithTiming.startTimeUs = OutputOptions.startTimeUs`. If
`OutputOptions.outputAllCues` is also set, then another `CuesWithTiming`
is emitted at the end that covers the 'first part' of the active cues,
and  ends at `OutputOptions.startTimeUs`.

As well as adding some more tests to `LegacySubtitleUtilWebvttTest`,
this change also adds more tests for `TtmlParser` handling of
`OutputOptions`, which transitively tests the behaviour of
`LegacySubtitleUtil`.

#minor-release

PiperOrigin-RevId: 573151016
This commit is contained in:
ibaker 2023-10-13 02:17:57 -07:00 committed by Copybara-Service
parent 2421ba4d8f
commit f9ece88a25
6 changed files with 374 additions and 35 deletions

View File

@ -15,12 +15,11 @@
*/
package androidx.media3.extractor.text;
import static java.lang.Math.max;
import androidx.media3.common.C;
import androidx.media3.common.text.Cue;
import androidx.media3.common.util.Consumer;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.extractor.text.SubtitleParser.OutputOptions;
import java.util.List;
/** Utility methods for working with legacy {@link Subtitle} objects. */
@ -37,22 +36,55 @@ public class LegacySubtitleUtil {
* and the last event is an empty cue list.
*/
public static void toCuesWithTiming(
Subtitle subtitle,
SubtitleParser.OutputOptions outputOptions,
Consumer<CuesWithTiming> output) {
int startIndex =
outputOptions.startTimeUs != C.TIME_UNSET
? max(subtitle.getNextEventTimeIndex(outputOptions.startTimeUs) - 1, 0)
: 0;
Subtitle subtitle, OutputOptions outputOptions, Consumer<CuesWithTiming> output) {
int startIndex = getStartIndex(subtitle, outputOptions);
boolean startedInMiddleOfCue = false;
if (outputOptions.startTimeUs != C.TIME_UNSET) {
List<Cue> cuesAtStartTime = subtitle.getCues(outputOptions.startTimeUs);
long firstEventTimeUs = subtitle.getEventTime(startIndex);
if (!cuesAtStartTime.isEmpty()
&& startIndex < subtitle.getEventTimeCount()
&& outputOptions.startTimeUs < firstEventTimeUs) {
output.accept(
new CuesWithTiming(
cuesAtStartTime,
outputOptions.startTimeUs,
firstEventTimeUs - outputOptions.startTimeUs));
startedInMiddleOfCue = true;
}
}
for (int i = startIndex; i < subtitle.getEventTimeCount(); i++) {
outputSubtitleEvent(subtitle, i, output);
}
if (outputOptions.outputAllCues) {
for (int i = 0; i < startIndex; i++) {
int endIndex = startedInMiddleOfCue ? startIndex - 1 : startIndex;
for (int i = 0; i < endIndex; i++) {
outputSubtitleEvent(subtitle, i, output);
}
if (startedInMiddleOfCue) {
output.accept(
new CuesWithTiming(
subtitle.getCues(outputOptions.startTimeUs),
subtitle.getEventTime(endIndex),
outputOptions.startTimeUs - subtitle.getEventTime(endIndex)));
}
}
}
private static int getStartIndex(Subtitle subtitle, OutputOptions outputOptions) {
if (outputOptions.startTimeUs == C.TIME_UNSET) {
return 0;
}
int nextEventTimeIndex = subtitle.getNextEventTimeIndex(outputOptions.startTimeUs);
if (nextEventTimeIndex == C.INDEX_UNSET) {
return subtitle.getEventTimeCount();
}
if (nextEventTimeIndex > 0
&& subtitle.getEventTime(nextEventTimeIndex - 1) == outputOptions.startTimeUs) {
nextEventTimeIndex--;
}
return nextEventTimeIndex;
}
private static void outputSubtitleEvent(
Subtitle subtitle, int eventIndex, Consumer<CuesWithTiming> output) {

View File

@ -81,7 +81,8 @@ public interface SubtitleParser {
/**
* Cues after this time (inclusive) will be emitted first. Cues before this time might be
* emitted later, depending on {@link #outputAllCues}.
* emitted later, depending on {@link #outputAllCues}. Can be {@link C#TIME_UNSET} to emit all
* cues.
*/
public final long startTimeUs;

View File

@ -26,11 +26,16 @@ import androidx.media3.common.text.TextEmphasisSpan;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.ColorParser;
import androidx.media3.extractor.text.CuesWithTiming;
import androidx.media3.extractor.text.SubtitleParser.OutputOptions;
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.collect.Lists;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -38,6 +43,8 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public final class TtmlParserTest {
private static final String SIMPLE_TTML_FILE = "media/ttml/simple.xml";
private static final String OVERLAPPING_TIMES_TTML_FILE = "media/ttml/overlapping_times.xml";
private static final String INLINE_ATTRIBUTES_TTML_FILE =
"media/ttml/inline_style_attributes.xml";
private static final String INHERIT_STYLE_TTML_FILE = "media/ttml/inherit_style.xml";
@ -69,6 +76,177 @@ public final class TtmlParserTest {
private static final String TEXT_EMPHASIS_FILE = "media/ttml/text_emphasis.xml";
private static final String SHEAR_FILE = "media/ttml/shear.xml";
@Test
public void simple_allCues() throws Exception {
ImmutableList<CuesWithTiming> allCues = getAllCues(SIMPLE_TTML_FILE);
assertThat(allCues).hasSize(3);
CuesWithTiming firstCue = allCues.get(0);
assertThat(firstCue.startTimeUs).isEqualTo(10_000_000);
assertThat(firstCue.durationUs).isEqualTo(8_000_000);
assertThat(firstCue.endTimeUs).isEqualTo(18_000_000);
assertThat(Lists.transform(firstCue.cues, c -> c.text.toString())).containsExactly("cue 1");
CuesWithTiming secondCue = allCues.get(1);
assertThat(secondCue.startTimeUs).isEqualTo(20_000_000);
assertThat(secondCue.durationUs).isEqualTo(8_000_000);
assertThat(secondCue.endTimeUs).isEqualTo(28_000_000);
assertThat(Lists.transform(secondCue.cues, c -> c.text.toString())).containsExactly("cue 2");
CuesWithTiming thirdCue = allCues.get(2);
assertThat(thirdCue.startTimeUs).isEqualTo(30_000_000);
assertThat(thirdCue.durationUs).isEqualTo(8_000_000);
assertThat(thirdCue.endTimeUs).isEqualTo(38_000_000);
assertThat(Lists.transform(thirdCue.cues, c -> c.text.toString())).containsExactly("cue 3");
}
@Test
public void simple_onlyCuesAfterTime() throws Exception {
TtmlParser parser = new TtmlParser();
byte[] bytes =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SIMPLE_TTML_FILE);
List<CuesWithTiming> cues = new ArrayList<>();
parser.parse(bytes, OutputOptions.onlyCuesAfter(/* startTimeUs= */ 11_000_000), cues::add);
assertThat(cues).hasSize(3);
CuesWithTiming firstCue = cues.get(0);
// First cue is truncated to OutputOptions.startTimeUs
assertThat(firstCue.startTimeUs).isEqualTo(11_000_000);
assertThat(Lists.transform(firstCue.cues, c -> c.text.toString())).containsExactly("cue 1");
assertThat(getOnlyCueTextAtIndex(cues, 1).toString()).isEqualTo("cue 2");
assertThat(getOnlyCueTextAtIndex(cues, 2).toString()).isEqualTo("cue 3");
}
@Test
public void simple_cuesAfterTimeThenCuesBefore() throws Exception {
TtmlParser parser = new TtmlParser();
byte[] bytes =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SIMPLE_TTML_FILE);
List<CuesWithTiming> cues = new ArrayList<>();
parser.parse(bytes, OutputOptions.cuesAfterThenRemainingCuesBefore(11_000_000), cues::add);
assertThat(cues).hasSize(4);
CuesWithTiming firstCue = cues.get(0);
// First cue is truncated to OutputOptions.startTimeUs
assertThat(firstCue.startTimeUs).isEqualTo(11_000_000);
assertThat(Lists.transform(firstCue.cues, c -> c.text.toString())).containsExactly("cue 1");
assertThat(getOnlyCueTextAtIndex(cues, 1).toString()).isEqualTo("cue 2");
assertThat(getOnlyCueTextAtIndex(cues, 2).toString()).isEqualTo("cue 3");
CuesWithTiming fourthCue = cues.get(3);
// Last cue is the part of firstCue before OutputOptions.startTimeUs
assertThat(fourthCue.startTimeUs).isEqualTo(10_000_000);
assertThat(fourthCue.endTimeUs).isEqualTo(11_000_000);
assertThat(Lists.transform(fourthCue.cues, c -> c.text.toString())).containsExactly("cue 1");
}
@Test
public void overlappingTimes_allCues() throws Exception {
ImmutableList<CuesWithTiming> allCues = getAllCues(OVERLAPPING_TIMES_TTML_FILE);
assertThat(allCues).hasSize(5);
CuesWithTiming firstCue = allCues.get(0);
assertThat(firstCue.startTimeUs).isEqualTo(10_000_000);
assertThat(firstCue.durationUs).isEqualTo(5_000_000);
assertThat(firstCue.endTimeUs).isEqualTo(15_000_000);
assertThat(Lists.transform(firstCue.cues, c -> c.text.toString())).containsExactly("cue 1");
CuesWithTiming secondCue = allCues.get(1);
assertThat(secondCue.startTimeUs).isEqualTo(15_000_000);
assertThat(secondCue.durationUs).isEqualTo(1_000_000);
assertThat(secondCue.endTimeUs).isEqualTo(16_000_000);
assertThat(Lists.transform(secondCue.cues, c -> c.text.toString()))
.containsExactly("cue 1\ncue 2: nested inside cue 1");
CuesWithTiming thirdCue = allCues.get(2);
assertThat(thirdCue.startTimeUs).isEqualTo(16_000_000);
assertThat(thirdCue.durationUs).isEqualTo(4_000_000);
assertThat(thirdCue.endTimeUs).isEqualTo(20_000_000);
assertThat(Lists.transform(thirdCue.cues, c -> c.text.toString()))
.containsExactly("cue 1\ncue 2: nested inside cue 1\ncue 3: overlaps with cue 2");
CuesWithTiming fourthCue = allCues.get(3);
assertThat(fourthCue.startTimeUs).isEqualTo(20_000_000);
assertThat(fourthCue.durationUs).isEqualTo(5_000_000);
assertThat(fourthCue.endTimeUs).isEqualTo(25_000_000);
assertThat(Lists.transform(fourthCue.cues, c -> c.text.toString()))
.containsExactly("cue 1\ncue 3: overlaps with cue 2");
CuesWithTiming fifthCue = allCues.get(4);
assertThat(fifthCue.startTimeUs).isEqualTo(25_000_000);
assertThat(fifthCue.durationUs).isEqualTo(3_000_000);
assertThat(fifthCue.endTimeUs).isEqualTo(28_000_000);
assertThat(Lists.transform(fifthCue.cues, c -> c.text.toString()))
.containsExactly("cue 3: overlaps with cue 2");
}
@Test
public void overlappingTimes_onlyCuesAfterTime() throws Exception {
TtmlParser parser = new TtmlParser();
byte[] bytes =
TestUtil.getByteArray(
ApplicationProvider.getApplicationContext(), OVERLAPPING_TIMES_TTML_FILE);
List<CuesWithTiming> cues = new ArrayList<>();
parser.parse(bytes, OutputOptions.onlyCuesAfter(/* startTimeUs= */ 11_000_000), cues::add);
assertThat(cues).hasSize(5);
CuesWithTiming firstCue = cues.get(0);
// First cue is truncated to OutputOptions.startTimeUs
assertThat(firstCue.startTimeUs).isEqualTo(11_000_000);
assertThat(Lists.transform(firstCue.cues, c -> c.text.toString())).containsExactly("cue 1");
assertThat(getOnlyCueTextAtIndex(cues, 1).toString())
.isEqualTo("cue 1\ncue 2: nested inside cue 1");
assertThat(getOnlyCueTextAtIndex(cues, 2).toString())
.isEqualTo("cue 1\ncue 2: nested inside cue 1\ncue 3: overlaps with cue 2");
assertThat(getOnlyCueTextAtIndex(cues, 3).toString())
.isEqualTo("cue 1\ncue 3: overlaps with cue 2");
assertThat(getOnlyCueTextAtIndex(cues, 4).toString()).isEqualTo("cue 3: overlaps with cue 2");
}
@Test
public void overlappingTimes_cuesAfterTimeThenCuesBefore() throws Exception {
TtmlParser parser = new TtmlParser();
byte[] bytes =
TestUtil.getByteArray(
ApplicationProvider.getApplicationContext(), OVERLAPPING_TIMES_TTML_FILE);
List<CuesWithTiming> cues = new ArrayList<>();
parser.parse(
bytes,
OutputOptions.cuesAfterThenRemainingCuesBefore(/* startTimeUs= */ 11_000_000),
cues::add);
assertThat(cues).hasSize(6);
CuesWithTiming firstCue = cues.get(0);
// First cue is truncated to OutputOptions.startTimeUs
assertThat(firstCue.startTimeUs).isEqualTo(11_000_000);
assertThat(Lists.transform(firstCue.cues, c -> c.text.toString())).containsExactly("cue 1");
assertThat(getOnlyCueTextAtIndex(cues, 1).toString())
.isEqualTo("cue 1\ncue 2: nested inside cue 1");
assertThat(getOnlyCueTextAtIndex(cues, 2).toString())
.isEqualTo("cue 1\ncue 2: nested inside cue 1\ncue 3: overlaps with cue 2");
assertThat(getOnlyCueTextAtIndex(cues, 3).toString())
.isEqualTo("cue 1\ncue 3: overlaps with cue 2");
assertThat(getOnlyCueTextAtIndex(cues, 4).toString()).isEqualTo("cue 3: overlaps with cue 2");
CuesWithTiming sixthCue = cues.get(5);
// Last cue is truncated to end at OutputOptions.startTimeUs
assertThat(sixthCue.startTimeUs).isEqualTo(10_000_000);
assertThat(sixthCue.endTimeUs).isEqualTo(11_000_000);
assertThat(Lists.transform(sixthCue.cues, c -> c.text.toString())).containsExactly("cue 1");
}
@Test
public void inlineAttributes() throws Exception {
ImmutableList<CuesWithTiming> allCues = getAllCues(INLINE_ATTRIBUTES_TTML_FILE);
@ -904,21 +1082,23 @@ public final class TtmlParserTest {
assertThat(eighthCue.shearDegrees).isWithin(0.01f).of(90f);
}
private static Spanned getOnlyCueTextAtIndex(ImmutableList<CuesWithTiming> allCues, int index) {
private static Spanned getOnlyCueTextAtIndex(List<CuesWithTiming> allCues, int index) {
Cue cue = getOnlyCueAtIndex(allCues, index);
assertThat(cue.text).isInstanceOf(Spanned.class);
return (Spanned) Assertions.checkNotNull(cue.text);
}
private static Cue getOnlyCueAtIndex(ImmutableList<CuesWithTiming> allCues, int index) {
private static Cue getOnlyCueAtIndex(List<CuesWithTiming> allCues, int index) {
ImmutableList<Cue> cues = allCues.get(index).cues;
assertThat(cues).hasSize(1);
return cues.get(0);
}
private static ImmutableList<CuesWithTiming> getAllCues(String file) throws Exception {
private static ImmutableList<CuesWithTiming> getAllCues(String file) throws IOException {
TtmlParser ttmlParser = new TtmlParser();
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), file);
return ttmlParser.parse(bytes, 0, bytes.length);
ImmutableList.Builder<CuesWithTiming> allCues = ImmutableList.builder();
ttmlParser.parse(bytes, OutputOptions.allCues(), allCues::add);
return allCues.build();
}
}

View File

@ -40,7 +40,7 @@ public class LegacySubtitleUtilWebvttTest {
private static final String FIRST_SUBTITLE_STRING = "This is the first subtitle.";
private static final String SECOND_SUBTITLE_STRING = "This is the second subtitle.";
private static final WebvttSubtitle simpleSubtitle =
private static final WebvttSubtitle SIMPLE_SUBTITLE =
new WebvttSubtitle(
Arrays.asList(
new WebvttCueInfo(
@ -52,7 +52,7 @@ public class LegacySubtitleUtilWebvttTest {
/* startTimeUs= */ 3_000_000,
/* endTimeUs= */ 4_000_000)));
private static final WebvttSubtitle overlappingSubtitle =
private static final WebvttSubtitle OVERLAPPING_SUBTITLE =
new WebvttSubtitle(
Arrays.asList(
new WebvttCueInfo(
@ -67,7 +67,7 @@ public class LegacySubtitleUtilWebvttTest {
@Test
public void toCuesWithTiming_allCues_simpleSubtitle() {
ImmutableList<CuesWithTiming> cuesWithTimingsList =
toCuesWithTimingList(simpleSubtitle, SubtitleParser.OutputOptions.allCues());
toCuesWithTimingList(SIMPLE_SUBTITLE, SubtitleParser.OutputOptions.allCues());
assertThat(cuesWithTimingsList).hasSize(2);
assertThat(cuesWithTimingsList.get(0).startTimeUs).isEqualTo(1_000_000);
@ -85,7 +85,7 @@ public class LegacySubtitleUtilWebvttTest {
@Test
public void toCuesWithTiming_allCues_overlappingSubtitle() {
ImmutableList<CuesWithTiming> cuesWithTimingsList =
toCuesWithTimingList(overlappingSubtitle, SubtitleParser.OutputOptions.allCues());
toCuesWithTimingList(OVERLAPPING_SUBTITLE, SubtitleParser.OutputOptions.allCues());
assertThat(cuesWithTimingsList).hasSize(3);
assertThat(cuesWithTimingsList.get(0).startTimeUs).isEqualTo(1_000_000);
@ -109,7 +109,8 @@ public class LegacySubtitleUtilWebvttTest {
@Test
public void toCuesWithTiming_onlyEmitCuesAfterStartTime_startBetweenCues_simpleSubtitle() {
ImmutableList<CuesWithTiming> cuesWithTimingsList =
toCuesWithTimingList(simpleSubtitle, SubtitleParser.OutputOptions.onlyCuesAfter(2_500_000));
toCuesWithTimingList(
SIMPLE_SUBTITLE, SubtitleParser.OutputOptions.onlyCuesAfter(2_500_000));
assertThat(cuesWithTimingsList).hasSize(1);
assertThat(cuesWithTimingsList.get(0).startTimeUs).isEqualTo(3_000_000);
@ -122,7 +123,8 @@ public class LegacySubtitleUtilWebvttTest {
@Test
public void toCuesWithTiming_onlyEmitCuesAfterStartTime_startAtCueEnd_simpleSubtitle() {
ImmutableList<CuesWithTiming> cuesWithTimingsList =
toCuesWithTimingList(simpleSubtitle, SubtitleParser.OutputOptions.onlyCuesAfter(2_000_000));
toCuesWithTimingList(
SIMPLE_SUBTITLE, SubtitleParser.OutputOptions.onlyCuesAfter(2_000_000));
assertThat(cuesWithTimingsList).hasSize(1);
assertThat(cuesWithTimingsList.get(0).startTimeUs).isEqualTo(3_000_000);
@ -135,7 +137,8 @@ public class LegacySubtitleUtilWebvttTest {
@Test
public void toCuesWithTiming_onlyEmitCuesAfterStartTime_startAtCueStart_simpleSubtitle() {
ImmutableList<CuesWithTiming> cuesWithTimingsList =
toCuesWithTimingList(simpleSubtitle, SubtitleParser.OutputOptions.onlyCuesAfter(3_000_000));
toCuesWithTimingList(
SIMPLE_SUBTITLE, SubtitleParser.OutputOptions.onlyCuesAfter(3_000_000));
assertThat(cuesWithTimingsList).hasSize(1);
assertThat(cuesWithTimingsList.get(0).startTimeUs).isEqualTo(3_000_000);
@ -148,11 +151,13 @@ public class LegacySubtitleUtilWebvttTest {
@Test
public void toCuesWithTiming_onlyEmitCuesAfterStartTime_startInMiddleOfCue_simpleSubtitle() {
ImmutableList<CuesWithTiming> cuesWithTimingsList =
toCuesWithTimingList(simpleSubtitle, SubtitleParser.OutputOptions.onlyCuesAfter(1_500_000));
toCuesWithTimingList(
SIMPLE_SUBTITLE, SubtitleParser.OutputOptions.onlyCuesAfter(1_500_000));
assertThat(cuesWithTimingsList).hasSize(2);
assertThat(cuesWithTimingsList.get(0).startTimeUs).isEqualTo(1_000_000);
assertThat(cuesWithTimingsList.get(0).durationUs).isEqualTo(1_000_000);
// First cue is truncated to start at OutputOptions.startTimeUs
assertThat(cuesWithTimingsList.get(0).startTimeUs).isEqualTo(1_500_000);
assertThat(cuesWithTimingsList.get(0).durationUs).isEqualTo(500_000);
assertThat(cuesWithTimingsList.get(0).endTimeUs).isEqualTo(2_000_000);
assertThat(Lists.transform(cuesWithTimingsList.get(0).cues, c -> c.text))
.containsExactly(FIRST_SUBTITLE_STRING);
@ -167,11 +172,12 @@ public class LegacySubtitleUtilWebvttTest {
public void toCuesWithTiming_onlyEmitCuesAfterStartTime_overlappingSubtitle() {
ImmutableList<CuesWithTiming> cuesWithTimingsList =
toCuesWithTimingList(
overlappingSubtitle, SubtitleParser.OutputOptions.onlyCuesAfter(2_500_000));
OVERLAPPING_SUBTITLE, SubtitleParser.OutputOptions.onlyCuesAfter(2_500_000));
assertThat(cuesWithTimingsList).hasSize(2);
assertThat(cuesWithTimingsList.get(0).startTimeUs).isEqualTo(2_000_000);
assertThat(cuesWithTimingsList.get(0).durationUs).isEqualTo(1_000_000);
// First event is truncated to start at OutputOptions.startTimeUs.
assertThat(cuesWithTimingsList.get(0).startTimeUs).isEqualTo(2_500_000);
assertThat(cuesWithTimingsList.get(0).durationUs).isEqualTo(500_000);
assertThat(cuesWithTimingsList.get(0).endTimeUs).isEqualTo(3_000_000);
assertThat(Lists.transform(cuesWithTimingsList.get(0).cues, c -> c.text))
.containsExactly(FIRST_SUBTITLE_STRING, SECOND_SUBTITLE_STRING)
@ -184,11 +190,61 @@ public class LegacySubtitleUtilWebvttTest {
}
@Test
public void toCuesWithTiming_emitCuesAfterStartTimeThenThoseBefore_simpleSubtitle() {
public void
toCuesWithTiming_emitCuesAfterStartTimeThenThoseBefore_startAtStartOfCue_simpleSubtitle() {
ImmutableList<CuesWithTiming> cuesWithTimingsList =
toCuesWithTimingList(
simpleSubtitle,
SubtitleParser.OutputOptions.cuesAfterThenRemainingCuesBefore(2_500_000));
SIMPLE_SUBTITLE,
SubtitleParser.OutputOptions.cuesAfterThenRemainingCuesBefore(3_000_000));
assertThat(cuesWithTimingsList).hasSize(2);
assertThat(cuesWithTimingsList.get(0).startTimeUs).isEqualTo(3_000_000);
assertThat(cuesWithTimingsList.get(0).durationUs).isEqualTo(1_000_000);
assertThat(cuesWithTimingsList.get(0).endTimeUs).isEqualTo(4_000_000);
assertThat(Lists.transform(cuesWithTimingsList.get(0).cues, c -> c.text))
.containsExactly(SECOND_SUBTITLE_STRING);
assertThat(cuesWithTimingsList.get(1).startTimeUs).isEqualTo(1_000_000);
assertThat(cuesWithTimingsList.get(1).durationUs).isEqualTo(1_000_000);
assertThat(cuesWithTimingsList.get(1).endTimeUs).isEqualTo(2_000_000);
assertThat(Lists.transform(cuesWithTimingsList.get(1).cues, c -> c.text))
.containsExactly(FIRST_SUBTITLE_STRING);
}
@Test
public void
toCuesWithTiming_emitCuesAfterStartTimeThenThoseBefore_startInMiddleOfCue_simpleSubtitle() {
ImmutableList<CuesWithTiming> cuesWithTimingsList =
toCuesWithTimingList(
SIMPLE_SUBTITLE,
SubtitleParser.OutputOptions.cuesAfterThenRemainingCuesBefore(1_500_000));
assertThat(cuesWithTimingsList).hasSize(3);
// First event is truncated to start at OutputOptions.startTimeUs.
assertThat(cuesWithTimingsList.get(0).startTimeUs).isEqualTo(1_500_000);
assertThat(cuesWithTimingsList.get(0).durationUs).isEqualTo(500_000);
assertThat(cuesWithTimingsList.get(0).endTimeUs).isEqualTo(2_000_000);
assertThat(Lists.transform(cuesWithTimingsList.get(0).cues, c -> c.text))
.containsExactly(FIRST_SUBTITLE_STRING);
assertThat(cuesWithTimingsList.get(1).startTimeUs).isEqualTo(3_000_000);
assertThat(cuesWithTimingsList.get(1).durationUs).isEqualTo(1_000_000);
assertThat(cuesWithTimingsList.get(1).endTimeUs).isEqualTo(4_000_000);
assertThat(Lists.transform(cuesWithTimingsList.get(1).cues, c -> c.text))
.containsExactly(SECOND_SUBTITLE_STRING);
// Final event is the part of the 'first event' that is before OutputOptions.startTimeUs
assertThat(cuesWithTimingsList.get(2).startTimeUs).isEqualTo(1_000_000);
assertThat(cuesWithTimingsList.get(2).durationUs).isEqualTo(500_000);
assertThat(cuesWithTimingsList.get(2).endTimeUs).isEqualTo(1_500_000);
assertThat(Lists.transform(cuesWithTimingsList.get(2).cues, c -> c.text))
.containsExactly(FIRST_SUBTITLE_STRING);
}
@Test
public void
toCuesWithTiming_emitCuesAfterStartTimeThenThoseBefore_startAtEndOfCue_simpleSubtitle() {
ImmutableList<CuesWithTiming> cuesWithTimingsList =
toCuesWithTimingList(
SIMPLE_SUBTITLE,
SubtitleParser.OutputOptions.cuesAfterThenRemainingCuesBefore(2_000_000));
assertThat(cuesWithTimingsList).hasSize(2);
assertThat(cuesWithTimingsList.get(0).startTimeUs).isEqualTo(3_000_000);
@ -207,12 +263,13 @@ public class LegacySubtitleUtilWebvttTest {
public void toCuesWithTiming_emitCuesAfterStartTimeThenThoseBefore_overlappingSubtitle() {
ImmutableList<CuesWithTiming> cuesWithTimingsList =
toCuesWithTimingList(
overlappingSubtitle,
OVERLAPPING_SUBTITLE,
SubtitleParser.OutputOptions.cuesAfterThenRemainingCuesBefore(2_500_000));
assertThat(cuesWithTimingsList).hasSize(3);
assertThat(cuesWithTimingsList.get(0).startTimeUs).isEqualTo(2_000_000);
assertThat(cuesWithTimingsList.get(0).durationUs).isEqualTo(1_000_000);
assertThat(cuesWithTimingsList).hasSize(4);
// First event is truncated to start at OutputOptions.startTimeUs.
assertThat(cuesWithTimingsList.get(0).startTimeUs).isEqualTo(2_500_000);
assertThat(cuesWithTimingsList.get(0).durationUs).isEqualTo(500_000);
assertThat(cuesWithTimingsList.get(0).endTimeUs).isEqualTo(3_000_000);
assertThat(Lists.transform(cuesWithTimingsList.get(0).cues, c -> c.text))
.containsExactly(FIRST_SUBTITLE_STRING, SECOND_SUBTITLE_STRING)
@ -227,6 +284,13 @@ public class LegacySubtitleUtilWebvttTest {
assertThat(cuesWithTimingsList.get(2).endTimeUs).isEqualTo(2_000_000);
assertThat(Lists.transform(cuesWithTimingsList.get(2).cues, c -> c.text))
.containsExactly(FIRST_SUBTITLE_STRING);
// Final event is the part of the 'first event' that is before OutputOptions.startTimeUs
assertThat(cuesWithTimingsList.get(3).startTimeUs).isEqualTo(2_000_000);
assertThat(cuesWithTimingsList.get(3).durationUs).isEqualTo(500_000);
assertThat(cuesWithTimingsList.get(3).endTimeUs).isEqualTo(2_500_000);
assertThat(Lists.transform(cuesWithTimingsList.get(3).cues, c -> c.text))
.containsExactly(FIRST_SUBTITLE_STRING, SECOND_SUBTITLE_STRING)
.inOrder();
}
private static ImmutableList<CuesWithTiming> toCuesWithTimingList(

View File

@ -0,0 +1,31 @@
<!--
~ Copyright 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ 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.
-->
<tt xmlns:ttm="http://www.w3.org/2006/10/ttaf1#metadata"
xmlns:ttp="http://www.w3.org/2006/10/ttaf1#parameter"
xmlns:tts="http://www.w3.org/2006/10/ttaf1#style"
xmlns="http://www.w3.org/ns/ttml">
<body>
<div>
<p begin="10s" end="25s">cue 1</p>
</div>
<div>
<p begin="15s" end="20s">cue 2: nested inside cue 1</p>
</div>
<div>
<p begin="16s" end="28s">cue 3: overlaps with cue 2</p>
</div>
</body>
</tt>

View File

@ -0,0 +1,31 @@
<!--
~ Copyright 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ 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.
-->
<tt xmlns:ttm="http://www.w3.org/2006/10/ttaf1#metadata"
xmlns:ttp="http://www.w3.org/2006/10/ttaf1#parameter"
xmlns:tts="http://www.w3.org/2006/10/ttaf1#style"
xmlns="http://www.w3.org/ns/ttml">
<body>
<div>
<p begin="10s" end="18s">cue 1</p>
</div>
<div>
<p begin="20s" end="28s">cue 2</p>
</div>
<div>
<p begin="30s" end="38s">cue 3</p>
</div>
</body>
</tt>