From 579167743bd7677506c8fe7c318fc443f2fb6eb8 Mon Sep 17 00:00:00 2001 From: Arnold Szabo Date: Sun, 27 Oct 2019 18:54:34 +0200 Subject: [PATCH 1/9] Parse and apply position attribute in SSA subtitles --- .../exoplayer2/text/ssa/SsaDecoder.java | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) 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 8da37e7f8f..34b3f67d36 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 @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.text.ssa; import android.text.TextUtils; +import android.util.Pair; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; @@ -40,6 +41,9 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { private static final Pattern SSA_TIMECODE_PATTERN = Pattern.compile( "(?:(\\d+):)?(\\d+):(\\d+)(?::|\\.)(\\d+)"); + private static final Pattern SSA_POSITION_PATTERN = Pattern.compile( + "\\\\pos\\((\\d+(\\.\\d+)?),\\s*(\\d+(\\.\\d+)?)"); + private static final String FORMAT_LINE_PREFIX = "Format: "; private static final String DIALOGUE_LINE_PREFIX = "Dialogue: "; @@ -50,6 +54,9 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { private int formatEndIndex; private int formatTextIndex; + private int playResX; + private int playResY; + public SsaDecoder() { this(/* initializationData= */ null); } @@ -98,6 +105,12 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { private void parseHeader(ParsableByteArray data) { String currentLine; while ((currentLine = data.readLine()) != null) { + if (currentLine.startsWith("PlayResX:")) { + playResX = Integer.valueOf(currentLine.substring(9).trim()); + } + if (currentLine.startsWith("PlayResY:")) { + playResY = Integer.valueOf(currentLine.substring(9).trim()); + } // TODO: Parse useful data from the header. if (currentLine.startsWith("[Events]")) { // We've reached the event body. @@ -196,11 +209,30 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { } } + // Parse \pos{x,y} attribute + Pair position = parsePosition(lineValues[formatTextIndex]); + String text = lineValues[formatTextIndex] .replaceAll("\\{.*?\\}", "") .replaceAll("\\\\N", "\n") .replaceAll("\\\\n", "\n"); - cues.add(new Cue(text)); + + Cue cue; + if (position != null && playResX != 0 && playResY != 0) { + cue = new Cue( + text, + /* textAlignment */ null, + 1 - position.second / playResY, + Cue.LINE_TYPE_FRACTION, + Cue.ANCHOR_TYPE_START, + position.first / playResX, + Cue.ANCHOR_TYPE_MIDDLE, + Cue.DIMEN_UNSET); + } else { + cue = new Cue(text); + } + + cues.add(cue); cueTimesUs.add(startTimeUs); if (endTimeUs != C.TIME_UNSET) { cues.add(Cue.EMPTY); @@ -226,4 +258,21 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { return timestampUs; } + /** + * Parses an SSA position attribute. + * + * @param line The string to parse. + * @return The parsed position in a pair (x,y). + */ + @Nullable + public static Pair parsePosition(String line) { + Matcher matcher = SSA_POSITION_PATTERN.matcher(line); + if (!matcher.find()) { + return null; + } + float x = Float.parseFloat(matcher.group(1)); + float y = Float.parseFloat(matcher.group(3)); + return new Pair<>(x, y); + } + } From 0391e73a0bb0aa9f65cdf6606fc4c925185682c4 Mon Sep 17 00:00:00 2001 From: Arnold Szabo Date: Wed, 30 Oct 2019 00:43:15 +0200 Subject: [PATCH 2/9] Adding support for overlapping subtitles --- .../exoplayer2/text/ssa/SsaDecoder.java | 73 ++++++++++++------- .../exoplayer2/text/ssa/SsaSubtitle.java | 19 +++-- 2 files changed, 56 insertions(+), 36 deletions(-) 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 34b3f67d36..e950998c0d 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 @@ -24,7 +24,6 @@ import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; -import com.google.android.exoplayer2.util.LongArray; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; @@ -82,19 +81,15 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { @Override protected Subtitle decode(byte[] bytes, int length, boolean reset) { - ArrayList cues = new ArrayList<>(); - LongArray cueTimesUs = new LongArray(); + ArrayList> cues = new ArrayList<>(); + List cueTimesUs = new ArrayList<>(); ParsableByteArray data = new ParsableByteArray(bytes, length); if (!haveInitializationData) { parseHeader(data); } parseEventBody(data, cues, cueTimesUs); - - Cue[] cuesArray = new Cue[cues.size()]; - cues.toArray(cuesArray); - long[] cueTimesUsArray = cueTimesUs.toArray(); - return new SsaSubtitle(cuesArray, cueTimesUsArray); + return new SsaSubtitle(cues, cueTimesUs); } /** @@ -126,7 +121,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { * @param cues A list to which parsed cues will be added. * @param cueTimesUs An array to which parsed cue timestamps will be added. */ - private void parseEventBody(ParsableByteArray data, List cues, LongArray cueTimesUs) { + private void parseEventBody(ParsableByteArray data, List> cues, List cueTimesUs) { String currentLine; while ((currentLine = data.readLine()) != null) { if (!haveInitializationData && currentLine.startsWith(FORMAT_LINE_PREFIX)) { @@ -180,7 +175,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { * @param cues A list to which parsed cues will be added. * @param cueTimesUs An array to which parsed cue timestamps will be added. */ - private void parseDialogueLine(String dialogueLine, List cues, LongArray cueTimesUs) { + private void parseDialogueLine(String dialogueLine, List> cues, List cueTimesUs) { if (formatKeyCount == 0) { Log.w(TAG, "Skipping dialogue line before complete format: " + dialogueLine); return; @@ -222,7 +217,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { cue = new Cue( text, /* textAlignment */ null, - 1 - position.second / playResY, + position.second / playResY, Cue.LINE_TYPE_FRACTION, Cue.ANCHOR_TYPE_START, position.first / playResX, @@ -232,12 +227,44 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { cue = new Cue(text); } - cues.add(cue); - cueTimesUs.add(startTimeUs); - if (endTimeUs != C.TIME_UNSET) { - cues.add(Cue.EMPTY); - cueTimesUs.add(endTimeUs); + int startTimeIndex = insertToCueTimes(cueTimesUs, startTimeUs); + + 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; + } + } + + cueTimes.add(0, timeUs); + return 0; } /** @@ -246,7 +273,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { * @param timeString The string to parse. * @return The parsed timestamp in microseconds. */ - public static long parseTimecodeUs(String timeString) { + private static long parseTimecodeUs(String timeString) { Matcher matcher = SSA_TIMECODE_PATTERN.matcher(timeString); if (!matcher.matches()) { return C.TIME_UNSET; @@ -258,21 +285,15 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { return timestampUs; } - /** - * Parses an SSA position attribute. - * - * @param line The string to parse. - * @return The parsed position in a pair (x,y). - */ @Nullable - public static Pair parsePosition(String line) { + public static Pair parsePosition(String line){ Matcher matcher = SSA_POSITION_PATTERN.matcher(line); - if (!matcher.find()) { + if(!matcher.find()){ return null; } float x = Float.parseFloat(matcher.group(1)); float y = Float.parseFloat(matcher.group(3)); - return new Pair<>(x, y); + return new Pair<>(x,y); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java index 9a3756194f..9200dbd226 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java @@ -28,14 +28,14 @@ import java.util.List; */ /* package */ final class SsaSubtitle implements Subtitle { - private final Cue[] cues; - private final long[] cueTimesUs; + private final List> cues; + private final List cueTimesUs; /** * @param cues The cues in the subtitle. * @param cueTimesUs The cue times, in microseconds. */ - public SsaSubtitle(Cue[] cues, long[] cueTimesUs) { + public SsaSubtitle(List> cues, List cueTimesUs) { this.cues = cues; this.cueTimesUs = cueTimesUs; } @@ -43,30 +43,29 @@ import java.util.List; @Override public int getNextEventTimeIndex(long timeUs) { int index = Util.binarySearchCeil(cueTimesUs, timeUs, false, false); - return index < cueTimesUs.length ? index : C.INDEX_UNSET; + return index < cueTimesUs.size() ? index : C.INDEX_UNSET; } @Override public int getEventTimeCount() { - return cueTimesUs.length; + return cueTimesUs.size(); } @Override public long getEventTime(int index) { Assertions.checkArgument(index >= 0); - Assertions.checkArgument(index < cueTimesUs.length); - return cueTimesUs[index]; + Assertions.checkArgument(index < cueTimesUs.size()); + return cueTimesUs.get(index); } @Override public List getCues(long timeUs) { int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false); - if (index == -1 || cues[index] == Cue.EMPTY) { + if (index == -1 || cues.get(index).isEmpty()) { // timeUs is earlier than the start of the first cue, or we have an empty cue. return Collections.emptyList(); } else { - return Collections.singletonList(cues[index]); + return cues.get(index); } } - } From 4d6d8060b4225f78084268dc43f5739fc3993f05 Mon Sep 17 00:00:00 2001 From: Arnold Szabo Date: Wed, 30 Oct 2019 22:45:31 +0200 Subject: [PATCH 3/9] Use PointF instead of Pair when parsing the position --- Project Default.xml | 5 +++++ .../android/exoplayer2/text/ssa/SsaDecoder.java | 12 ++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 Project Default.xml diff --git a/Project Default.xml b/Project Default.xml new file mode 100644 index 0000000000..8d66637cb9 --- /dev/null +++ b/Project Default.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file 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 e950998c0d..827a735e9e 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 @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.text.ssa; +import android.graphics.PointF; import android.text.TextUtils; -import android.util.Pair; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; @@ -205,7 +205,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { } // Parse \pos{x,y} attribute - Pair position = parsePosition(lineValues[formatTextIndex]); + PointF position = parsePosition(lineValues[formatTextIndex]); String text = lineValues[formatTextIndex] .replaceAll("\\{.*?\\}", "") @@ -217,10 +217,10 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { cue = new Cue( text, /* textAlignment */ null, - position.second / playResY, + position.y / playResY, Cue.LINE_TYPE_FRACTION, Cue.ANCHOR_TYPE_START, - position.first / playResX, + position.x / playResX, Cue.ANCHOR_TYPE_MIDDLE, Cue.DIMEN_UNSET); } else { @@ -286,14 +286,14 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { } @Nullable - public static Pair parsePosition(String line){ + public static PointF parsePosition(String line){ Matcher matcher = SSA_POSITION_PATTERN.matcher(line); if(!matcher.find()){ return null; } float x = Float.parseFloat(matcher.group(1)); float y = Float.parseFloat(matcher.group(3)); - return new Pair<>(x,y); + return new PointF(x, y); } } From 3b741e591fbff2ed69e9b7efdc3b084e64ba21ca Mon Sep 17 00:00:00 2001 From: Arnold Szabo Date: Wed, 30 Oct 2019 22:51:13 +0200 Subject: [PATCH 4/9] Remove hardcoded index when parsing PlayResX and PlayResY --- .../com/google/android/exoplayer2/text/ssa/SsaDecoder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 827a735e9e..181ce47abc 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 @@ -101,10 +101,10 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { String currentLine; while ((currentLine = data.readLine()) != null) { if (currentLine.startsWith("PlayResX:")) { - playResX = Integer.valueOf(currentLine.substring(9).trim()); + playResX = Integer.valueOf(currentLine.substring("PlayResX:".length()).trim()); } if (currentLine.startsWith("PlayResY:")) { - playResY = Integer.valueOf(currentLine.substring(9).trim()); + playResY = Integer.valueOf(currentLine.substring("PlayResY:".length()).trim()); } // TODO: Parse useful data from the header. if (currentLine.startsWith("[Events]")) { From 86efd1944ec6b1506de85ec029f9328378b53260 Mon Sep 17 00:00:00 2001 From: Arnold Szabo Date: Wed, 30 Oct 2019 22:58:32 +0200 Subject: [PATCH 5/9] Add jdoc to SSA parsePosition(..) method --- .../android/exoplayer2/text/ssa/SsaDecoder.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) 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 181ce47abc..4079a6a433 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 @@ -204,7 +204,6 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { } } - // Parse \pos{x,y} attribute PointF position = parsePosition(lineValues[formatTextIndex]); String text = lineValues[formatTextIndex] @@ -285,10 +284,17 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { return timestampUs; } + /** + * Parses the position of an SSA dialogue line. + * The attribute is expected to be in this form: "\pos{x,y}". + * + * @param line The string to parse. + * @return The parsed position. + */ @Nullable - public static PointF parsePosition(String line){ + private static PointF parsePosition(String line) { Matcher matcher = SSA_POSITION_PATTERN.matcher(line); - if(!matcher.find()){ + if (!matcher.find()) { return null; } float x = Float.parseFloat(matcher.group(1)); From 7a6de79f91309fbba5b3cc1d335d5301e6022d5f Mon Sep 17 00:00:00 2001 From: Arnold Szabo Date: Wed, 30 Oct 2019 23:00:45 +0200 Subject: [PATCH 6/9] Add initial values to playResX and playResY --- .../com/google/android/exoplayer2/text/ssa/SsaDecoder.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 4079a6a433..c3b0ab9f18 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 @@ -53,8 +53,8 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { private int formatEndIndex; private int formatTextIndex; - private int playResX; - private int playResY; + private int playResX = C.LENGTH_UNSET; + private int playResY = C.LENGTH_UNSET; public SsaDecoder() { this(/* initializationData= */ null); @@ -212,7 +212,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { .replaceAll("\\\\n", "\n"); Cue cue; - if (position != null && playResX != 0 && playResY != 0) { + if (position != null && playResX != C.LENGTH_UNSET && playResY != C.LENGTH_UNSET) { cue = new Cue( text, /* textAlignment */ null, From 925a7fd045462126be5a959ce483448ff613f653 Mon Sep 17 00:00:00 2001 From: Arnold Szabo Date: Wed, 30 Oct 2019 23:02:37 +0200 Subject: [PATCH 7/9] Remove unnecessary empty check in getCues(..) --- .../com/google/android/exoplayer2/text/ssa/SsaSubtitle.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java index 9200dbd226..4093f7974d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java @@ -61,8 +61,8 @@ import java.util.List; @Override public List getCues(long timeUs) { int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false); - if (index == -1 || cues.get(index).isEmpty()) { - // timeUs is earlier than the start of the first cue, or we have an empty cue. + if (index == -1) { + // timeUs is earlier than the start of the first cue. return Collections.emptyList(); } else { return cues.get(index); From fb2a70291847115cee2554c45cbc553aec9d78d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arnold=20Szab=C3=B3?= Date: Wed, 30 Oct 2019 23:04:44 +0200 Subject: [PATCH 8/9] Delete accidentally pushed Project Default.xml --- Project Default.xml | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 Project Default.xml diff --git a/Project Default.xml b/Project Default.xml deleted file mode 100644 index 8d66637cb9..0000000000 --- a/Project Default.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file From 0c5d47028342aa7bd88a3d171bc28e1bc5970f0f Mon Sep 17 00:00:00 2001 From: Arnold Szabo Date: Sun, 3 Nov 2019 13:59:28 +0200 Subject: [PATCH 9/9] 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."); }