From 0c5d47028342aa7bd88a3d171bc28e1bc5970f0f Mon Sep 17 00:00:00 2001 From: Arnold Szabo Date: Sun, 3 Nov 2019 13:59:28 +0200 Subject: [PATCH] Correct SSA overlapping subtitle decoding, add tests --- .../exoplayer2/text/ssa/SsaDecoder.java | 92 ++++++++++++------- library/core/src/test/assets/ssa/overlap | 16 ++++ .../exoplayer2/text/ssa/SsaDecoderTest.java | 40 +++++++- 3 files changed, 113 insertions(+), 35 deletions(-) create mode 100644 library/core/src/test/assets/ssa/overlap diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java index c3b0ab9f18..305bbe63d7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java @@ -226,44 +226,68 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { cue = new Cue(text); } - int startTimeIndex = insertToCueTimes(cueTimesUs, startTimeUs); + int startTimeIndex = 0; + boolean startTimeFound = false; + // Search the insertion index for startTimeUs in cueTimesUs + for (int i = cueTimesUs.size() - 1; i >= 0; i--) { + if (cueTimesUs.get(i) == startTimeUs) { + startTimeIndex = i; + startTimeFound = true; + break; + } - List startCueList = new ArrayList<>(); - if (startTimeIndex != 0) { - startCueList.addAll(cues.get(startTimeIndex - 1)); - } - cues.add(startTimeIndex, startCueList); - - if (endTimeUs != C.TIME_UNSET) { - int endTimeIndex = insertToCueTimes(cueTimesUs, endTimeUs); - List endList = new ArrayList<>(cues.get(endTimeIndex - 1)); - cues.add(endTimeIndex, endList); - - int i = startTimeIndex; - do { - cues.get(i).add(cue); - i++; - } while (i != endTimeIndex); - } - } - - /** - * Insert the given cue time into the given array keeping the array sorted. - * - * @param cueTimes The array with sorted cue times - * @param timeUs The cue time to be inserted - * @return The index where the cue time was inserted - */ - private static int insertToCueTimes(List cueTimes, long timeUs) { - for (int i = cueTimes.size() - 1; i >= 0; i--) { - if (cueTimes.get(i) <= timeUs) { - cueTimes.add(i + 1, timeUs); - return i + 1; + if (cueTimesUs.get(i) < startTimeUs) { + startTimeIndex = i + 1; + break; } } - cueTimes.add(0, timeUs); - return 0; + if (startTimeIndex == 0) { + // Handle first cue + cueTimesUs.add(startTimeIndex, startTimeUs); + cues.add(startTimeIndex, new ArrayList<>()); + } else { + if (!startTimeFound) { + // Add the startTimeUs only if it wasn't found in cueTimesUs + cueTimesUs.add(startTimeIndex, startTimeUs); + // Copy over cues from left + List startCueList = new ArrayList<>(cues.get(startTimeIndex - 1)); + cues.add(startTimeIndex, startCueList); + } + } + + int endTimeIndex = 0; + if (endTimeUs != C.TIME_UNSET) { + boolean endTimeFound = false; + + // Search the insertion index for endTimeUs in cueTimesUs + for (int i = cueTimesUs.size() - 1; i >= 0; i--) { + if (cueTimesUs.get(i) == endTimeUs) { + endTimeIndex = i; + endTimeFound = true; + break; + } + + if (cueTimesUs.get(i) < endTimeUs) { + endTimeIndex = i + 1; + break; + } + } + + if (!endTimeFound) { + // Add the endTimeUs only if it wasn't found in cueTimesUs + cueTimesUs.add(endTimeIndex, endTimeUs); + // Copy over cues from left + cues.add(endTimeIndex, new ArrayList<>(cues.get(endTimeIndex - 1))); + } + } + + // Iterate on cues from startTimeIndex until endTimeIndex, add the current cue + int i = startTimeIndex; + do { + cues.get(i).add(cue); + i++; + } while (i < endTimeIndex); } /** diff --git a/library/core/src/test/assets/ssa/overlap b/library/core/src/test/assets/ssa/overlap new file mode 100644 index 0000000000..c2a0c53f86 --- /dev/null +++ b/library/core/src/test/assets/ssa/overlap @@ -0,0 +1,16 @@ +[Script Info] +Title: SomeTitle + +[V4+ Styles] +Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding +Style: Default,Open Sans Semibold,36,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,2,0,0,28,1 + +[Events] +Format: Layer, Start, End, Style, Name, Text +Dialogue: 0,0:00:01.00,0:00:04.23,Default,Olly,Subtitle A +Dialogue: 0,0:00:02.00,0:00:05.23,Default,Olly,Subtitle B +Dialogue: 0,0:00:06.00,0:00:08.44,Default,Olly,Subtitle C +Dialogue: 0,0:00:08.44,0:00:09.44,Default,Olly,Subtitle D +Dialogue: 0,0:00:08.44,0:00:09.44,Default,Olly,Subtitle E +Dialogue: 0,0:00:10.72,0:00:15.65,Default,Olly,Subtitle F +Dialogue: 0,0:00:13.22,0:00:14.22,Default,Olly,Subtitle G diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java index 7095962801..3f07fcdd87 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java @@ -32,6 +32,7 @@ public final class SsaDecoderTest { private static final String EMPTY = "ssa/empty"; private static final String TYPICAL = "ssa/typical"; + private static final String OVERLAP = "ssa/overlap"; private static final String TYPICAL_HEADER_ONLY = "ssa/typical_header"; private static final String TYPICAL_DIALOGUE_ONLY = "ssa/typical_dialogue"; private static final String TYPICAL_FORMAT_ONLY = "ssa/typical_format"; @@ -60,6 +61,37 @@ public final class SsaDecoderTest { assertTypicalCue3(subtitle, 4); } + @Test + public void testDecodeOverlap() throws IOException { + SsaDecoder decoder = new SsaDecoder(); + byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), OVERLAP); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + + assertThat(subtitle.getEventTime(0)).isEqualTo(1000000); + assertThat(subtitle.getEventTime(1)).isEqualTo(2000000); + assertThat(subtitle.getEventTime(2)).isEqualTo(4230000); + assertThat(subtitle.getEventTime(3)).isEqualTo(5230000); + assertThat(subtitle.getEventTime(4)).isEqualTo(6000000); + assertThat(subtitle.getEventTime(5)).isEqualTo(8440000); + assertThat(subtitle.getEventTime(6)).isEqualTo(9440000); + assertThat(subtitle.getEventTime(7)).isEqualTo(10720000); + assertThat(subtitle.getEventTime(8)).isEqualTo(13220000); + assertThat(subtitle.getEventTime(9)).isEqualTo(14220000); + assertThat(subtitle.getEventTime(10)).isEqualTo(15650000); + + assertThat(subtitle.getCues(1000010).size()).isEqualTo(1); + assertThat(subtitle.getCues(2000010).size()).isEqualTo(2); + assertThat(subtitle.getCues(4230010).size()).isEqualTo(1); + assertThat(subtitle.getCues(5230010).size()).isEqualTo(0); + assertThat(subtitle.getCues(6000010).size()).isEqualTo(1); + assertThat(subtitle.getCues(8440010).size()).isEqualTo(2); + assertThat(subtitle.getCues(9440010).size()).isEqualTo(0); + assertThat(subtitle.getCues(10720010).size()).isEqualTo(1); + assertThat(subtitle.getCues(13220010).size()).isEqualTo(2); + assertThat(subtitle.getCues(14220010).size()).isEqualTo(1); + assertThat(subtitle.getCues(15650010).size()).isEqualTo(0); + } + @Test public void testDecodeTypicalWithInitializationData() throws IOException { byte[] headerBytes = @@ -107,10 +139,16 @@ public final class SsaDecoderTest { assertThat(subtitle.getEventTime(1)).isEqualTo(2340000); assertThat(subtitle.getCues(subtitle.getEventTime(1)).get(0).text.toString()) + .isEqualTo("This is the first subtitle."); + assertThat(subtitle.getCues(subtitle.getEventTime(1)).get(1).text.toString()) .isEqualTo("This is the second subtitle \nwith a newline \nand another."); assertThat(subtitle.getEventTime(2)).isEqualTo(4560000); - assertThat(subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString()) + assertThat(subtitle.getCues(subtitle.getEventTime(1)).get(0).text.toString()) + .isEqualTo("This is the first subtitle."); + assertThat(subtitle.getCues(subtitle.getEventTime(1)).get(1).text.toString()) + .isEqualTo("This is the second subtitle \nwith a newline \nand another."); + assertThat(subtitle.getCues(subtitle.getEventTime(2)).get(2).text.toString()) .isEqualTo("This is the third subtitle, with a comma."); }