mirror of
https://github.com/androidx/media.git
synced 2025-05-16 03:59:54 +08:00
Simplify Subrip support
This commit is contained in:
parent
71252784e9
commit
fbbf3f27fd
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user