Subrip (SRT) support.
This commit is contained in:
parent
ac54b4f696
commit
2fb2e5a509
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer.text.subrip;
|
||||
|
||||
import com.google.android.exoplayer.text.Cue;
|
||||
|
||||
/**
|
||||
* A representation of a SubRip cue.
|
||||
*/
|
||||
/* package */ final class SubripCue extends 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);
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
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 java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* A simple SRT parser.
|
||||
* <p/>
|
||||
*
|
||||
* @see <a href="https://en.wikipedia.org/wiki/SubRip">Wikipedia on SRT</a>
|
||||
*/
|
||||
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 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<SubripCue> subtitles = new ArrayList<>();
|
||||
|
||||
// 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());
|
||||
}
|
||||
|
||||
line = subripData.readLine();
|
||||
|
||||
// parse cue time
|
||||
matcher = SUBRIP_CUE_IDENTIFIER.matcher(line);
|
||||
if (!matcher.find()) {
|
||||
throw new ParserException("Expected cue start time: " + line);
|
||||
} else {
|
||||
startTime = parseTimestampUs(matcher.group(1)) + startTimeUs;
|
||||
endTime = parseTimestampUs(matcher.group(2)) + startTimeUs;
|
||||
}
|
||||
|
||||
// parse text
|
||||
textBuilder.setLength(0);
|
||||
while (((line = subripData.readLine()) != null) && (!line.isEmpty())) {
|
||||
if (textBuilder.length() > 0) {
|
||||
textBuilder.append("<br>");
|
||||
}
|
||||
textBuilder.append(line.trim());
|
||||
}
|
||||
text = Html.fromHtml(textBuilder.toString());
|
||||
|
||||
SubripCue cue = new SubripCue(startTime, endTime, position, text);
|
||||
subtitles.add(cue);
|
||||
}
|
||||
|
||||
subripData.close();
|
||||
inputStream.close();
|
||||
SubripSubtitle subtitle = new SubripSubtitle(subtitles, startTimeUs);
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canParse(String 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 {
|
||||
if (!s.matches(SUBRIP_TIMESTAMP_STRING)) {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* A representation of a SubRip subtitle.
|
||||
*/
|
||||
/* package */ final class SubripSubtitle implements Subtitle {
|
||||
|
||||
private final List<SubripCue> cues;
|
||||
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.
|
||||
* @param startTimeUs The start time of the subtitle.
|
||||
*/
|
||||
public SubripSubtitle(List<SubripCue> cues, long startTimeUs) {
|
||||
this.cues = cues;
|
||||
numCues = cues.size();
|
||||
this.startTimeUs = startTimeUs;
|
||||
|
||||
this.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
|
||||
public long getStartTime() {
|
||||
return startTimeUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNextEventTimeIndex(long timeUs) {
|
||||
Assertions.checkArgument(timeUs >= 0);
|
||||
int index = Util.binarySearchCeil(sortedCueTimesUs, timeUs, false, false);
|
||||
return index < sortedCueTimesUs.length ? index : -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEventTimeCount() {
|
||||
return sortedCueTimesUs.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getEventTime(int index) {
|
||||
Assertions.checkArgument(index >= 0);
|
||||
Assertions.checkArgument(index < sortedCueTimesUs.length);
|
||||
return sortedCueTimesUs[index];
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLastEventTime() {
|
||||
if (getEventTimeCount() == 0) {
|
||||
return -1;
|
||||
}
|
||||
return sortedCueTimesUs[sortedCueTimesUs.length - 1];
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Cue> getCues(long timeUs) {
|
||||
ArrayList<Cue> 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 {
|
||||
return Collections.<Cue>emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -55,6 +55,7 @@ public class MimeTypes {
|
||||
|
||||
public static final String APPLICATION_ID3 = BASE_TYPE_APPLICATION + "/id3";
|
||||
public static final String APPLICATION_EIA608 = BASE_TYPE_APPLICATION + "/eia-608";
|
||||
public static final String APPLICATION_SUBRIP = BASE_TYPE_APPLICATION + "/x-subrip";
|
||||
public static final String APPLICATION_TTML = BASE_TYPE_APPLICATION + "/ttml+xml";
|
||||
public static final String APPLICATION_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL";
|
||||
|
||||
|
0
library/src/test/assets/subrip/empty
Normal file
0
library/src/test/assets/subrip/empty
Normal file
8
library/src/test/assets/subrip/typical
Normal file
8
library/src/test/assets/subrip/typical
Normal file
@ -0,0 +1,8 @@
|
||||
1
|
||||
00:00:00,000 --> 00:00:01,234
|
||||
This is the first subtitle.
|
||||
|
||||
2
|
||||
00:00:02,345 --> 00:00:03,456
|
||||
This is the second subtitle.
|
||||
Second subtitle with second line.
|
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer.text.subrip;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
|
||||
import com.google.android.exoplayer.C;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Unit test for {@link SubripParser}.
|
||||
*/
|
||||
public class SubripParserTest extends InstrumentationTestCase {
|
||||
|
||||
private static final String TYPICAL_SUBRIP_FILE = "subrip/typical";
|
||||
private static final String EMPTY_SUBRIP_FILE = "subrip/empty";
|
||||
|
||||
public void testParseNullSubripFile() throws IOException {
|
||||
SubripParser parser = new SubripParser();
|
||||
InputStream inputStream =
|
||||
getInstrumentation().getContext().getResources().getAssets().open(EMPTY_SUBRIP_FILE);
|
||||
|
||||
try {
|
||||
parser.parse(inputStream, C.UTF8_NAME, 0);
|
||||
fail("Expected IOException");
|
||||
} catch (IOException expected) {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
public void testParseTypicalSubripFile() throws IOException {
|
||||
SubripParser parser = new SubripParser();
|
||||
InputStream inputStream =
|
||||
getInstrumentation().getContext().getResources().getAssets().open(TYPICAL_SUBRIP_FILE);
|
||||
SubripSubtitle subtitle = parser.parse(inputStream, C.UTF8_NAME, 0);
|
||||
|
||||
// test start time and event count
|
||||
long startTimeUs = 0;
|
||||
assertEquals(startTimeUs, subtitle.getStartTime());
|
||||
assertEquals(4, subtitle.getEventTimeCount());
|
||||
|
||||
// test first cue
|
||||
assertEquals(startTimeUs, subtitle.getEventTime(0));
|
||||
assertEquals("This is the first subtitle.",
|
||||
subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString());
|
||||
assertEquals(startTimeUs + 1234000, subtitle.getEventTime(1));
|
||||
|
||||
// test second cue
|
||||
assertEquals(startTimeUs + 2345000, subtitle.getEventTime(2));
|
||||
assertEquals("This is the second subtitle.\nSecond subtitle with second line.",
|
||||
subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString());
|
||||
assertEquals(startTimeUs + 3456000, subtitle.getEventTime(3));
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user