From f9ece88a25b449ab1e59ec0f6a67b71d7a2dc8ce Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 13 Oct 2023 02:17:57 -0700 Subject: [PATCH] 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 --- .../extractor/text/LegacySubtitleUtil.java | 52 ++++- .../media3/extractor/text/SubtitleParser.java | 3 +- .../extractor/text/ttml/TtmlParserTest.java | 188 +++++++++++++++++- .../webvtt/LegacySubtitleUtilWebvttTest.java | 104 ++++++++-- .../assets/media/ttml/overlapping_times.xml | 31 +++ .../src/test/assets/media/ttml/simple.xml | 31 +++ 6 files changed, 374 insertions(+), 35 deletions(-) create mode 100644 libraries/test_data/src/test/assets/media/ttml/overlapping_times.xml create mode 100644 libraries/test_data/src/test/assets/media/ttml/simple.xml diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/LegacySubtitleUtil.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/LegacySubtitleUtil.java index 158c2958de..2d9ebe21fb 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/LegacySubtitleUtil.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/LegacySubtitleUtil.java @@ -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,23 +36,56 @@ public class LegacySubtitleUtil { * and the last event is an empty cue list. */ public static void toCuesWithTiming( - Subtitle subtitle, - SubtitleParser.OutputOptions outputOptions, - Consumer output) { - int startIndex = - outputOptions.startTimeUs != C.TIME_UNSET - ? max(subtitle.getNextEventTimeIndex(outputOptions.startTimeUs) - 1, 0) - : 0; + Subtitle subtitle, OutputOptions outputOptions, Consumer output) { + int startIndex = getStartIndex(subtitle, outputOptions); + boolean startedInMiddleOfCue = false; + if (outputOptions.startTimeUs != C.TIME_UNSET) { + List 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 output) { long startTimeUs = subtitle.getEventTime(eventIndex); diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/SubtitleParser.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/SubtitleParser.java index 125a538022..7ff9fcf28e 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/SubtitleParser.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/SubtitleParser.java @@ -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; diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/text/ttml/TtmlParserTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/text/ttml/TtmlParserTest.java index c66d9165ef..beaf46bca6 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/text/ttml/TtmlParserTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/text/ttml/TtmlParserTest.java @@ -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 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 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 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 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 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 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 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 allCues, int index) { + private static Spanned getOnlyCueTextAtIndex(List 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 allCues, int index) { + private static Cue getOnlyCueAtIndex(List allCues, int index) { ImmutableList cues = allCues.get(index).cues; assertThat(cues).hasSize(1); return cues.get(0); } - private static ImmutableList getAllCues(String file) throws Exception { + private static ImmutableList 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 allCues = ImmutableList.builder(); + ttmlParser.parse(bytes, OutputOptions.allCues(), allCues::add); + return allCues.build(); } } diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/text/webvtt/LegacySubtitleUtilWebvttTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/text/webvtt/LegacySubtitleUtilWebvttTest.java index fec89e5211..efa0cc8ba7 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/text/webvtt/LegacySubtitleUtilWebvttTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/text/webvtt/LegacySubtitleUtilWebvttTest.java @@ -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 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 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 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 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 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 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 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 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 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 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 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 toCuesWithTimingList( diff --git a/libraries/test_data/src/test/assets/media/ttml/overlapping_times.xml b/libraries/test_data/src/test/assets/media/ttml/overlapping_times.xml new file mode 100644 index 0000000000..5747cbcfaf --- /dev/null +++ b/libraries/test_data/src/test/assets/media/ttml/overlapping_times.xml @@ -0,0 +1,31 @@ + + + +
+

cue 1

+
+
+

cue 2: nested inside cue 1

+
+
+

cue 3: overlaps with cue 2

+
+ +
diff --git a/libraries/test_data/src/test/assets/media/ttml/simple.xml b/libraries/test_data/src/test/assets/media/ttml/simple.xml new file mode 100644 index 0000000000..f390981ff9 --- /dev/null +++ b/libraries/test_data/src/test/assets/media/ttml/simple.xml @@ -0,0 +1,31 @@ + + + +
+

cue 1

+
+
+

cue 2

+
+
+

cue 3

+
+ +