diff --git a/library/src/main/java/com/google/android/exoplayer/text/subrip/SubripCue.java b/library/src/main/java/com/google/android/exoplayer/text/subrip/SubripCue.java index c60aeeb679..a464316b00 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/subrip/SubripCue.java +++ b/library/src/main/java/com/google/android/exoplayer/text/subrip/SubripCue.java @@ -25,24 +25,10 @@ import com.google.android.exoplayer.text.Cue; public final long startTime; public final long endTime; - public SubripCue(CharSequence text) { - this(Cue.UNSET_VALUE, Cue.UNSET_VALUE, Cue.UNSET_VALUE,text); - } - - public SubripCue(long startTime, long endTime, int position, CharSequence text) { - super(text, Cue.UNSET_VALUE, position, null, Cue.UNSET_VALUE); + public SubripCue(long startTime, long endTime, CharSequence text) { + super(text); this.startTime = startTime; this.endTime = endTime; } - /** - * Returns whether or not this cue should be placed in the default position and rolled-up with - * the other "normal" cues. - * - * @return True if this cue should be placed in the default position; false otherwise. - */ - public boolean isNormalCue() { - return (line == UNSET_VALUE && position == UNSET_VALUE); - } - } diff --git a/library/src/main/java/com/google/android/exoplayer/text/subrip/SubripParser.java b/library/src/main/java/com/google/android/exoplayer/text/subrip/SubripParser.java index 66a4dbff86..e633cdc9b3 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/subrip/SubripParser.java +++ b/library/src/main/java/com/google/android/exoplayer/text/subrip/SubripParser.java @@ -15,14 +15,15 @@ */ package com.google.android.exoplayer.text.subrip; -import android.text.Html; - import com.google.android.exoplayer.C; import com.google.android.exoplayer.ParserException; -import com.google.android.exoplayer.text.Cue; import com.google.android.exoplayer.text.SubtitleParser; import com.google.android.exoplayer.util.MimeTypes; +import android.text.Html; +import android.text.Spanned; +import android.text.TextUtils; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -32,93 +33,66 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; /** - * A simple SRT parser. + * A simple SubRip parser. *

- * - * @see Wikipedia on SRT + * @see Wikipedia on SubRip */ public final class SubripParser implements SubtitleParser { - private static final String TAG = "SubRipParser"; - - private static final String SUBRIP_POSITION_STRING = "^(\\d)$"; - private static final Pattern SUBRIP_POSITION = Pattern.compile(SUBRIP_POSITION_STRING); - - private static final String SUBRIP_CUE_IDENTIFIER_STRING = "^(.*)\\s-->\\s(.*)$"; - private static final Pattern SUBRIP_CUE_IDENTIFIER = - Pattern.compile(SUBRIP_CUE_IDENTIFIER_STRING); - - private static final String SUBRIP_TIMESTAMP_STRING = "(\\d+:)?[0-5]\\d:[0-5]\\d:[0-5]\\d,\\d{3}"; - // private static final Pattern SUBRIP_TIMESTAMP = Pattern.compile(SUBRIP_TIMESTAMP_STRING); + private static final Pattern SUBRIP_TIMING_LINE = Pattern.compile("(.*)\\s+-->\\s+(.*)"); + private static final Pattern SUBRIP_TIMESTAMP = + Pattern.compile("(?:(\\d+):)?(\\d+):(\\d+),(\\d+)"); private final StringBuilder textBuilder; - private final boolean strictParsing; - public SubripParser() { - this(true); - } - - public SubripParser(boolean strictParsing) { - this.strictParsing = strictParsing; - textBuilder = new StringBuilder(); } @Override public SubripSubtitle parse(InputStream inputStream, String inputEncoding, long startTimeUs) throws IOException { - ArrayList subtitles = new ArrayList<>(); + ArrayList cues = new ArrayList<>(); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, C.UTF8_NAME)); + String currentLine; - // file should not be empty - if (inputStream.available() == 0) { - throw new ParserException("File is empty?"); - } - - BufferedReader subripData = new BufferedReader(new InputStreamReader(inputStream, C.UTF8_NAME)); - String line; - - - // process the cues and text - while ((line = subripData.readLine()) != null) { - long startTime = Cue.UNSET_VALUE; - long endTime = Cue.UNSET_VALUE; - CharSequence text = null; - int position = Cue.UNSET_VALUE; - - Matcher matcher = SUBRIP_POSITION.matcher(line); - if (matcher.matches()) { - position = Integer.parseInt(matcher.group()); + while ((currentLine = reader.readLine()) != null) { + // Parse the numeric counter as a sanity check. + try { + Integer.parseInt(currentLine); + } catch (NumberFormatException e) { + throw new ParserException("Expected numeric counter: " + currentLine); } - line = subripData.readLine(); - - // parse cue time - matcher = SUBRIP_CUE_IDENTIFIER.matcher(line); - if (!matcher.find()) { - throw new ParserException("Expected cue start time: " + line); + // Read and parse the timing line. + long cueStartTimeUs; + long cueEndTimeUs; + currentLine = reader.readLine(); + Matcher matcher = SUBRIP_TIMING_LINE.matcher(currentLine); + if (matcher.find()) { + cueStartTimeUs = parseTimestampUs(matcher.group(1)) + startTimeUs; + cueEndTimeUs = parseTimestampUs(matcher.group(2)) + startTimeUs; } else { - startTime = parseTimestampUs(matcher.group(1)) + startTimeUs; - endTime = parseTimestampUs(matcher.group(2)) + startTimeUs; + throw new ParserException("Expected timing line: " + currentLine); } - // parse text + // Read and parse the text. textBuilder.setLength(0); - while (((line = subripData.readLine()) != null) && (!line.isEmpty())) { + while (!TextUtils.isEmpty(currentLine = reader.readLine())) { if (textBuilder.length() > 0) { textBuilder.append("
"); } - textBuilder.append(line.trim()); + textBuilder.append(currentLine.trim()); } - text = Html.fromHtml(textBuilder.toString()); - SubripCue cue = new SubripCue(startTime, endTime, position, text); - subtitles.add(cue); + Spanned text = Html.fromHtml(textBuilder.toString()); + SubripCue cue = new SubripCue(cueStartTimeUs, cueEndTimeUs, text); + cues.add(cue); } - subripData.close(); + reader.close(); inputStream.close(); - SubripSubtitle subtitle = new SubripSubtitle(subtitles, startTimeUs); + SubripSubtitle subtitle = new SubripSubtitle(cues, startTimeUs); return subtitle; } @@ -127,23 +101,16 @@ public final class SubripParser implements SubtitleParser { return MimeTypes.APPLICATION_SUBRIP.equals(mimeType); } - private void handleNoncompliantLine(String line) throws ParserException { - if (strictParsing) { - throw new ParserException("Unexpected line: " + line); - } - } - private static long parseTimestampUs(String s) throws NumberFormatException { - if (!s.matches(SUBRIP_TIMESTAMP_STRING)) { + Matcher matcher = SUBRIP_TIMESTAMP.matcher(s); + if (!matcher.matches()) { throw new NumberFormatException("has invalid format"); } - - String[] parts = s.split(",", 2); - long value = 0; - for (String group : parts[0].split(":")) { - value = value * 60 + Long.parseLong(group); - } - return (value * 1000 + Long.parseLong(parts[1])) * 1000; + long timestampMs = Long.parseLong(matcher.group(1)) * 60 * 60 * 1000; + timestampMs += Long.parseLong(matcher.group(2)) * 60 * 1000; + timestampMs += Long.parseLong(matcher.group(3)) * 1000; + timestampMs += Long.parseLong(matcher.group(4)); + return timestampMs * 1000; } } diff --git a/library/src/main/java/com/google/android/exoplayer/text/subrip/SubripSubtitle.java b/library/src/main/java/com/google/android/exoplayer/text/subrip/SubripSubtitle.java index 27cf7988ea..65282388be 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/subrip/SubripSubtitle.java +++ b/library/src/main/java/com/google/android/exoplayer/text/subrip/SubripSubtitle.java @@ -15,15 +15,11 @@ */ package com.google.android.exoplayer.text.subrip; -import android.text.SpannableStringBuilder; - import com.google.android.exoplayer.text.Cue; import com.google.android.exoplayer.text.Subtitle; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Util; -import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -36,7 +32,6 @@ import java.util.List; private final int numCues; private final long startTimeUs; private final long[] cueTimesUs; - private final long[] sortedCueTimesUs; /** * @param cues A list of the cues in this subtitle. @@ -44,19 +39,16 @@ import java.util.List; */ public SubripSubtitle(List cues, long startTimeUs) { this.cues = cues; - numCues = cues.size(); this.startTimeUs = startTimeUs; - this.cueTimesUs = new long[2 * numCues]; + numCues = cues.size(); + cueTimesUs = new long[2 * numCues]; for (int cueIndex = 0; cueIndex < numCues; cueIndex++) { SubripCue cue = cues.get(cueIndex); int arrayIndex = cueIndex * 2; cueTimesUs[arrayIndex] = cue.startTime; cueTimesUs[arrayIndex + 1] = cue.endTime; } - - this.sortedCueTimesUs = Arrays.copyOf(cueTimesUs, cueTimesUs.length); - Arrays.sort(sortedCueTimesUs); } @Override @@ -67,20 +59,20 @@ import java.util.List; @Override public int getNextEventTimeIndex(long timeUs) { Assertions.checkArgument(timeUs >= 0); - int index = Util.binarySearchCeil(sortedCueTimesUs, timeUs, false, false); - return index < sortedCueTimesUs.length ? index : -1; + int index = Util.binarySearchCeil(cueTimesUs, timeUs, false, false); + return index < cueTimesUs.length ? index : -1; } @Override public int getEventTimeCount() { - return sortedCueTimesUs.length; + return cueTimesUs.length; } @Override public long getEventTime(int index) { Assertions.checkArgument(index >= 0); - Assertions.checkArgument(index < sortedCueTimesUs.length); - return sortedCueTimesUs[index]; + Assertions.checkArgument(index < cueTimesUs.length); + return cueTimesUs[index]; } @Override @@ -88,50 +80,17 @@ import java.util.List; if (getEventTimeCount() == 0) { return -1; } - return sortedCueTimesUs[sortedCueTimesUs.length - 1]; + return cueTimesUs[cueTimesUs.length - 1]; } @Override public List getCues(long timeUs) { - ArrayList list = null; - SubripCue firstNormalCue = null; - SpannableStringBuilder normalCueTextBuilder = null; - - for (int i = 0; i < numCues; i++) { - if ((cueTimesUs[i * 2] <= timeUs) && (timeUs < cueTimesUs[i * 2 + 1])) { - if (list == null) { - list = new ArrayList<>(); - } - SubripCue cue = cues.get(i); - if (cue.isNormalCue()) { - // we want to merge all of the normal cues into a single cue to ensure they are drawn - // correctly (i.e. don't overlap) and to emulate roll-up, but only if there are multiple - // normal cues, otherwise we can just append the single normal cue - if (firstNormalCue == null) { - firstNormalCue = cue; - } else if (normalCueTextBuilder == null) { - normalCueTextBuilder = new SpannableStringBuilder(); - normalCueTextBuilder.append(firstNormalCue.text).append("\n").append(cue.text); - } else { - normalCueTextBuilder.append("\n").append(cue.text); - } - } else { - list.add(cue); - } - } - } - if (normalCueTextBuilder != null) { - // there were multiple normal cues, so create a new cue with all of the text - list.add(new SubripCue(normalCueTextBuilder)); - } else if (firstNormalCue != null) { - // there was only a single normal cue, so just add it to the list - list.add(firstNormalCue); - } - - if (list != null) { - return list; - } else { + int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false); + if (index == -1 || index % 2 == 1) { + // timeUs is earlier than the start of the first cue, or corresponds to a gap between cues. return Collections.emptyList(); + } else { + return Collections.singletonList((Cue) cues.get(index / 2)); } } diff --git a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitle.java b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitle.java index 0ef8577a44..3ca64cdc21 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitle.java +++ b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitle.java @@ -44,18 +44,17 @@ public class WebvttSubtitle implements Subtitle { */ public WebvttSubtitle(List cues, long startTimeUs) { this.cues = cues; - numCues = cues.size(); this.startTimeUs = startTimeUs; - this.cueTimesUs = new long[2 * numCues]; + numCues = cues.size(); + cueTimesUs = new long[2 * numCues]; for (int cueIndex = 0; cueIndex < numCues; cueIndex++) { WebvttCue cue = cues.get(cueIndex); int arrayIndex = cueIndex * 2; cueTimesUs[arrayIndex] = cue.startTime; cueTimesUs[arrayIndex + 1] = cue.endTime; } - - this.sortedCueTimesUs = Arrays.copyOf(cueTimesUs, cueTimesUs.length); + sortedCueTimesUs = Arrays.copyOf(cueTimesUs, cueTimesUs.length); Arrays.sort(sortedCueTimesUs); }