Simplify Subrip support

This commit is contained in:
Oliver Woodman 2015-06-10 17:48:01 +01:00
parent 71252784e9
commit fbbf3f27fd
4 changed files with 60 additions and 149 deletions

View File

@ -25,24 +25,10 @@ import com.google.android.exoplayer.text.Cue;
public final long startTime; public final long startTime;
public final long endTime; public final long endTime;
public SubripCue(CharSequence text) { public SubripCue(long startTime, long endTime, CharSequence text) {
this(Cue.UNSET_VALUE, Cue.UNSET_VALUE, Cue.UNSET_VALUE,text); super(text);
}
public SubripCue(long startTime, long endTime, int position, CharSequence text) {
super(text, Cue.UNSET_VALUE, position, null, Cue.UNSET_VALUE);
this.startTime = startTime; this.startTime = startTime;
this.endTime = endTime; 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);
}
} }

View File

@ -15,14 +15,15 @@
*/ */
package com.google.android.exoplayer.text.subrip; package com.google.android.exoplayer.text.subrip;
import android.text.Html;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.ParserException; 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.text.SubtitleParser;
import com.google.android.exoplayer.util.MimeTypes; 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.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -32,93 +33,66 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** /**
* A simple SRT parser. * A simple SubRip parser.
* <p/> * <p/>
* * @see <a href="https://en.wikipedia.org/wiki/SubRip">Wikipedia on SubRip</a>
* @see <a href="https://en.wikipedia.org/wiki/SubRip">Wikipedia on SRT</a>
*/ */
public final class SubripParser implements SubtitleParser { public final class SubripParser implements SubtitleParser {
private static final String TAG = "SubRipParser"; private static final Pattern SUBRIP_TIMING_LINE = Pattern.compile("(.*)\\s+-->\\s+(.*)");
private static final Pattern SUBRIP_TIMESTAMP =
private static final String SUBRIP_POSITION_STRING = "^(\\d)$"; Pattern.compile("(?:(\\d+):)?(\\d+):(\\d+),(\\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 final StringBuilder textBuilder; private final StringBuilder textBuilder;
private final boolean strictParsing;
public SubripParser() { public SubripParser() {
this(true);
}
public SubripParser(boolean strictParsing) {
this.strictParsing = strictParsing;
textBuilder = new StringBuilder(); textBuilder = new StringBuilder();
} }
@Override @Override
public SubripSubtitle parse(InputStream inputStream, String inputEncoding, long startTimeUs) public SubripSubtitle parse(InputStream inputStream, String inputEncoding, long startTimeUs)
throws IOException { throws IOException {
ArrayList<SubripCue> subtitles = new ArrayList<>(); ArrayList<SubripCue> cues = new ArrayList<>();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, C.UTF8_NAME));
String currentLine;
// file should not be empty while ((currentLine = reader.readLine()) != null) {
if (inputStream.available() == 0) { // Parse the numeric counter as a sanity check.
throw new ParserException("File is empty?"); try {
} Integer.parseInt(currentLine);
} catch (NumberFormatException e) {
BufferedReader subripData = new BufferedReader(new InputStreamReader(inputStream, C.UTF8_NAME)); throw new ParserException("Expected numeric counter: " + currentLine);
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());
} }
line = subripData.readLine(); // Read and parse the timing line.
long cueStartTimeUs;
// parse cue time long cueEndTimeUs;
matcher = SUBRIP_CUE_IDENTIFIER.matcher(line); currentLine = reader.readLine();
if (!matcher.find()) { Matcher matcher = SUBRIP_TIMING_LINE.matcher(currentLine);
throw new ParserException("Expected cue start time: " + line); if (matcher.find()) {
cueStartTimeUs = parseTimestampUs(matcher.group(1)) + startTimeUs;
cueEndTimeUs = parseTimestampUs(matcher.group(2)) + startTimeUs;
} else { } else {
startTime = parseTimestampUs(matcher.group(1)) + startTimeUs; throw new ParserException("Expected timing line: " + currentLine);
endTime = parseTimestampUs(matcher.group(2)) + startTimeUs;
} }
// parse text // Read and parse the text.
textBuilder.setLength(0); textBuilder.setLength(0);
while (((line = subripData.readLine()) != null) && (!line.isEmpty())) { while (!TextUtils.isEmpty(currentLine = reader.readLine())) {
if (textBuilder.length() > 0) { if (textBuilder.length() > 0) {
textBuilder.append("<br>"); textBuilder.append("<br>");
} }
textBuilder.append(line.trim()); textBuilder.append(currentLine.trim());
} }
text = Html.fromHtml(textBuilder.toString());
SubripCue cue = new SubripCue(startTime, endTime, position, text); Spanned text = Html.fromHtml(textBuilder.toString());
subtitles.add(cue); SubripCue cue = new SubripCue(cueStartTimeUs, cueEndTimeUs, text);
cues.add(cue);
} }
subripData.close(); reader.close();
inputStream.close(); inputStream.close();
SubripSubtitle subtitle = new SubripSubtitle(subtitles, startTimeUs); SubripSubtitle subtitle = new SubripSubtitle(cues, startTimeUs);
return subtitle; return subtitle;
} }
@ -127,23 +101,16 @@ public final class SubripParser implements SubtitleParser {
return MimeTypes.APPLICATION_SUBRIP.equals(mimeType); 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 { 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"); throw new NumberFormatException("has invalid format");
} }
long timestampMs = Long.parseLong(matcher.group(1)) * 60 * 60 * 1000;
String[] parts = s.split(",", 2); timestampMs += Long.parseLong(matcher.group(2)) * 60 * 1000;
long value = 0; timestampMs += Long.parseLong(matcher.group(3)) * 1000;
for (String group : parts[0].split(":")) { timestampMs += Long.parseLong(matcher.group(4));
value = value * 60 + Long.parseLong(group); return timestampMs * 1000;
}
return (value * 1000 + Long.parseLong(parts[1])) * 1000;
} }
} }

View File

@ -15,15 +15,11 @@
*/ */
package com.google.android.exoplayer.text.subrip; package com.google.android.exoplayer.text.subrip;
import android.text.SpannableStringBuilder;
import com.google.android.exoplayer.text.Cue; import com.google.android.exoplayer.text.Cue;
import com.google.android.exoplayer.text.Subtitle; import com.google.android.exoplayer.text.Subtitle;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -36,7 +32,6 @@ import java.util.List;
private final int numCues; private final int numCues;
private final long startTimeUs; private final long startTimeUs;
private final long[] cueTimesUs; private final long[] cueTimesUs;
private final long[] sortedCueTimesUs;
/** /**
* @param cues A list of the cues in this subtitle. * @param cues A list of the cues in this subtitle.
@ -44,19 +39,16 @@ import java.util.List;
*/ */
public SubripSubtitle(List<SubripCue> cues, long startTimeUs) { public SubripSubtitle(List<SubripCue> cues, long startTimeUs) {
this.cues = cues; this.cues = cues;
numCues = cues.size();
this.startTimeUs = startTimeUs; this.startTimeUs = startTimeUs;
this.cueTimesUs = new long[2 * numCues]; numCues = cues.size();
cueTimesUs = new long[2 * numCues];
for (int cueIndex = 0; cueIndex < numCues; cueIndex++) { for (int cueIndex = 0; cueIndex < numCues; cueIndex++) {
SubripCue cue = cues.get(cueIndex); SubripCue cue = cues.get(cueIndex);
int arrayIndex = cueIndex * 2; int arrayIndex = cueIndex * 2;
cueTimesUs[arrayIndex] = cue.startTime; cueTimesUs[arrayIndex] = cue.startTime;
cueTimesUs[arrayIndex + 1] = cue.endTime; cueTimesUs[arrayIndex + 1] = cue.endTime;
} }
this.sortedCueTimesUs = Arrays.copyOf(cueTimesUs, cueTimesUs.length);
Arrays.sort(sortedCueTimesUs);
} }
@Override @Override
@ -67,20 +59,20 @@ import java.util.List;
@Override @Override
public int getNextEventTimeIndex(long timeUs) { public int getNextEventTimeIndex(long timeUs) {
Assertions.checkArgument(timeUs >= 0); Assertions.checkArgument(timeUs >= 0);
int index = Util.binarySearchCeil(sortedCueTimesUs, timeUs, false, false); int index = Util.binarySearchCeil(cueTimesUs, timeUs, false, false);
return index < sortedCueTimesUs.length ? index : -1; return index < cueTimesUs.length ? index : -1;
} }
@Override @Override
public int getEventTimeCount() { public int getEventTimeCount() {
return sortedCueTimesUs.length; return cueTimesUs.length;
} }
@Override @Override
public long getEventTime(int index) { public long getEventTime(int index) {
Assertions.checkArgument(index >= 0); Assertions.checkArgument(index >= 0);
Assertions.checkArgument(index < sortedCueTimesUs.length); Assertions.checkArgument(index < cueTimesUs.length);
return sortedCueTimesUs[index]; return cueTimesUs[index];
} }
@Override @Override
@ -88,50 +80,17 @@ import java.util.List;
if (getEventTimeCount() == 0) { if (getEventTimeCount() == 0) {
return -1; return -1;
} }
return sortedCueTimesUs[sortedCueTimesUs.length - 1]; return cueTimesUs[cueTimesUs.length - 1];
} }
@Override @Override
public List<Cue> getCues(long timeUs) { public List<Cue> getCues(long timeUs) {
ArrayList<Cue> list = null; int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false);
SubripCue firstNormalCue = null; if (index == -1 || index % 2 == 1) {
SpannableStringBuilder normalCueTextBuilder = null; // timeUs is earlier than the start of the first cue, or corresponds to a gap between cues.
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 {
return Collections.<Cue>emptyList(); return Collections.<Cue>emptyList();
} else {
return Collections.singletonList((Cue) cues.get(index / 2));
} }
} }

View File

@ -44,18 +44,17 @@ public class WebvttSubtitle implements Subtitle {
*/ */
public WebvttSubtitle(List<WebvttCue> cues, long startTimeUs) { public WebvttSubtitle(List<WebvttCue> cues, long startTimeUs) {
this.cues = cues; this.cues = cues;
numCues = cues.size();
this.startTimeUs = startTimeUs; this.startTimeUs = startTimeUs;
this.cueTimesUs = new long[2 * numCues]; numCues = cues.size();
cueTimesUs = new long[2 * numCues];
for (int cueIndex = 0; cueIndex < numCues; cueIndex++) { for (int cueIndex = 0; cueIndex < numCues; cueIndex++) {
WebvttCue cue = cues.get(cueIndex); WebvttCue cue = cues.get(cueIndex);
int arrayIndex = cueIndex * 2; int arrayIndex = cueIndex * 2;
cueTimesUs[arrayIndex] = cue.startTime; cueTimesUs[arrayIndex] = cue.startTime;
cueTimesUs[arrayIndex + 1] = cue.endTime; cueTimesUs[arrayIndex + 1] = cue.endTime;
} }
sortedCueTimesUs = Arrays.copyOf(cueTimesUs, cueTimesUs.length);
this.sortedCueTimesUs = Arrays.copyOf(cueTimesUs, cueTimesUs.length);
Arrays.sort(sortedCueTimesUs); Arrays.sort(sortedCueTimesUs);
} }