Simplify subtitle parsing.

- Currently all subtitles we parse contain timestamps relative to the sample
  timestamp, however we add the sample timestamp in inconsistent ways (sometimes
  in the Subtitle, sometimes in the SubtitleParser). This change converges on
  a single approach. It also paves the way for passing absolute offsets to use
  instead, and being able to apply them in a consistent way in a single place
  (PlayableSubtitle). This functionality will be required for ISO 14496-30 TTML
  embedded subtitles.

Issue: #689
This commit is contained in:
Oliver Woodman 2015-08-13 20:36:50 +01:00
parent fbf590fdf6
commit bf77f3b289
16 changed files with 127 additions and 115 deletions

View file

@ -34,7 +34,7 @@ public final class SubripParserTest extends InstrumentationTestCase {
SubripParser parser = new SubripParser();
InputStream inputStream =
getInstrumentation().getContext().getResources().getAssets().open(EMPTY_SUBRIP_FILE);
SubripSubtitle subtitle = parser.parse(inputStream, C.UTF8_NAME, 0);
SubripSubtitle subtitle = parser.parse(inputStream, C.UTF8_NAME);
// Assert that the subtitle is empty.
assertEquals(0, subtitle.getEventTimeCount());
assertTrue(subtitle.getCues(0).isEmpty());
@ -44,10 +44,9 @@ public final class SubripParserTest extends InstrumentationTestCase {
SubripParser parser = new SubripParser();
InputStream inputStream =
getInstrumentation().getContext().getResources().getAssets().open(TYPICAL_SUBRIP_FILE);
SubripSubtitle subtitle = parser.parse(inputStream, C.UTF8_NAME, 0);
SubripSubtitle subtitle = parser.parse(inputStream, C.UTF8_NAME);
// Test start time and event count.
assertEquals(0, subtitle.getStartTime());
// Test event count.
assertEquals(4, subtitle.getEventTimeCount());
// Test first cue.

View file

@ -39,7 +39,7 @@ public class WebvttParserTest extends InstrumentationTestCase {
getInstrumentation().getContext().getResources().getAssets().open(EMPTY_WEBVTT_FILE);
try {
parser.parse(inputStream, C.UTF8_NAME, 0);
parser.parse(inputStream, C.UTF8_NAME);
fail("Expected IOException");
} catch (IOException expected) {
// Do nothing.
@ -50,10 +50,9 @@ public class WebvttParserTest extends InstrumentationTestCase {
WebvttParser parser = new WebvttParser();
InputStream inputStream =
getInstrumentation().getContext().getResources().getAssets().open(TYPICAL_WEBVTT_FILE);
WebvttSubtitle subtitle = parser.parse(inputStream, C.UTF8_NAME, 0);
WebvttSubtitle subtitle = parser.parse(inputStream, C.UTF8_NAME);
// test start time and event count
assertEquals(0, subtitle.getStartTime());
// test event count
assertEquals(4, subtitle.getEventTimeCount());
// test first cue
@ -74,10 +73,9 @@ public class WebvttParserTest extends InstrumentationTestCase {
InputStream inputStream =
getInstrumentation().getContext().getResources().getAssets()
.open(TYPICAL_WITH_IDS_WEBVTT_FILE);
WebvttSubtitle subtitle = parser.parse(inputStream, C.UTF8_NAME, 0);
WebvttSubtitle subtitle = parser.parse(inputStream, C.UTF8_NAME);
// test start time and event count
assertEquals(0, subtitle.getStartTime());
// test event count
assertEquals(4, subtitle.getEventTimeCount());
// test first cue
@ -98,10 +96,9 @@ public class WebvttParserTest extends InstrumentationTestCase {
InputStream inputStream =
getInstrumentation().getContext().getResources().getAssets()
.open(TYPICAL_WITH_TAGS_WEBVTT_FILE);
WebvttSubtitle subtitle = parser.parse(inputStream, C.UTF8_NAME, 0);
WebvttSubtitle subtitle = parser.parse(inputStream, C.UTF8_NAME);
// test start time and event count
assertEquals(0, subtitle.getStartTime());
// test event count
assertEquals(8, subtitle.getEventTimeCount());
// test first cue
@ -133,11 +130,10 @@ public class WebvttParserTest extends InstrumentationTestCase {
WebvttParser parser = new WebvttParser();
InputStream inputStream =
getInstrumentation().getContext().getResources().getAssets().open(LIVE_TYPICAL_WEBVTT_FILE);
WebvttSubtitle subtitle = parser.parse(inputStream, C.UTF8_NAME, 0);
WebvttSubtitle subtitle = parser.parse(inputStream, C.UTF8_NAME);
// test start time and event count
// test event count
long startTimeUs = 0;
assertEquals(startTimeUs, subtitle.getStartTime());
assertEquals(4, subtitle.getEventTimeCount());
// test first cue

View file

@ -32,7 +32,7 @@ public class WebvttSubtitleTest extends TestCase {
private static final String FIRST_AND_SECOND_SUBTITLE_STRING =
FIRST_SUBTITLE_STRING + "\n" + SECOND_SUBTITLE_STRING;
private WebvttSubtitle emptySubtitle = new WebvttSubtitle(new ArrayList<WebvttCue>(), 0);
private WebvttSubtitle emptySubtitle = new WebvttSubtitle(new ArrayList<WebvttCue>());
private ArrayList<WebvttCue> simpleSubtitleCues = new ArrayList<>();
{
@ -42,7 +42,7 @@ public class WebvttSubtitleTest extends TestCase {
WebvttCue secondCue = new WebvttCue(3000000, 4000000, SECOND_SUBTITLE_STRING);
simpleSubtitleCues.add(secondCue);
}
private WebvttSubtitle simpleSubtitle = new WebvttSubtitle(simpleSubtitleCues, 0);
private WebvttSubtitle simpleSubtitle = new WebvttSubtitle(simpleSubtitleCues);
private ArrayList<WebvttCue> overlappingSubtitleCues = new ArrayList<>();
{
@ -52,7 +52,7 @@ public class WebvttSubtitleTest extends TestCase {
WebvttCue secondCue = new WebvttCue(2000000, 4000000, SECOND_SUBTITLE_STRING);
overlappingSubtitleCues.add(secondCue);
}
private WebvttSubtitle overlappingSubtitle = new WebvttSubtitle(overlappingSubtitleCues, 0);
private WebvttSubtitle overlappingSubtitle = new WebvttSubtitle(overlappingSubtitleCues);
private ArrayList<WebvttCue> nestedSubtitleCues = new ArrayList<>();
{
@ -62,7 +62,7 @@ public class WebvttSubtitleTest extends TestCase {
WebvttCue secondCue = new WebvttCue(2000000, 3000000, SECOND_SUBTITLE_STRING);
nestedSubtitleCues.add(secondCue);
}
private WebvttSubtitle nestedSubtitle = new WebvttSubtitle(nestedSubtitleCues, 0);
private WebvttSubtitle nestedSubtitle = new WebvttSubtitle(nestedSubtitleCues);
public void testEventCount() {
assertEquals(0, emptySubtitle.getEventTimeCount());
@ -71,13 +71,6 @@ public class WebvttSubtitleTest extends TestCase {
assertEquals(4, nestedSubtitle.getEventTimeCount());
}
public void testStartTime() {
assertEquals(0, emptySubtitle.getStartTime());
assertEquals(0, simpleSubtitle.getStartTime());
assertEquals(0, overlappingSubtitle.getStartTime());
assertEquals(0, nestedSubtitle.getStartTime());
}
public void testLastEventTime() {
assertEquals(-1, emptySubtitle.getLastEventTime());
assertEquals(4000000, simpleSubtitle.getLastEventTime());

View file

@ -0,0 +1,70 @@
package com.google.android.exoplayer.text;
/*
* 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.
*/
import java.util.List;
/**
* A subtitle that wraps another subtitle, making it playable by adjusting it to be correctly
* aligned with the playback timebase.
*/
/* package */ final class PlayableSubtitle implements Subtitle {
/**
* The start time of the subtitle.
* <p>
* May be less than {@code getEventTime(0)}, since a subtitle may begin prior to the time of the
* first event.
*/
public final long startTimeUs;
private final Subtitle subtitle;
/**
* @param startTimeUs The start time of the subtitle.
* @param subtitle The subtitle to wrap.
*/
public PlayableSubtitle(long startTimeUs, Subtitle subtitle) {
this.startTimeUs = startTimeUs;
this.subtitle = subtitle;
}
@Override
public int getNextEventTimeIndex(long timeUs) {
return subtitle.getNextEventTimeIndex(timeUs - startTimeUs);
}
@Override
public int getEventTimeCount() {
return subtitle.getEventTimeCount();
}
@Override
public long getEventTime(int index) {
return subtitle.getEventTime(index) + startTimeUs;
}
@Override
public long getLastEventTime() {
return subtitle.getLastEventTime() + startTimeUs;
}
@Override
public List<Cue> getCues(long timeUs) {
return subtitle.getCues(timeUs - startTimeUs);
}
}

View file

@ -22,16 +22,6 @@ import java.util.List;
*/
public interface Subtitle {
/**
* Gets the start time of the subtitle.
* <p>
* Note that the value returned may be less than {@code getEventTime(0)}, since a subtitle may
* begin prior to the time of the first event.
*
* @return The start time of the subtitle in microseconds.
*/
public long getStartTime();
/**
* Gets the index of the first event that occurs after a given time (exclusive).
*

View file

@ -36,11 +36,9 @@ public interface SubtitleParser {
*
* @param inputStream The stream from which to parse the subtitle.
* @param inputEncoding The encoding of the input stream.
* @param startTimeUs The start time of the subtitle.
* @return A parsed representation of the subtitle.
* @throws IOException If a problem occurred reading from the stream.
*/
public Subtitle parse(InputStream inputStream, String inputEncoding, long startTimeUs)
throws IOException;
public Subtitle parse(InputStream inputStream, String inputEncoding) throws IOException;
}

View file

@ -17,6 +17,7 @@ package com.google.android.exoplayer.text;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Util;
import android.media.MediaCodec;
import android.os.Handler;
@ -31,14 +32,14 @@ import java.io.InputStream;
* Wraps a {@link SubtitleParser}, exposing an interface similar to {@link MediaCodec} for
* asynchronous parsing of subtitles.
*/
public final class SubtitleParserHelper implements Handler.Callback {
/* package */ final class SubtitleParserHelper implements Handler.Callback {
private final SubtitleParser parser;
private final Handler handler;
private SampleHolder sampleHolder;
private boolean parsing;
private Subtitle result;
private PlayableSubtitle result;
private IOException error;
/**
@ -94,7 +95,8 @@ public final class SubtitleParserHelper implements Handler.Callback {
parsing = true;
result = null;
error = null;
handler.obtainMessage(0, sampleHolder).sendToTarget();
handler.obtainMessage(0, Util.getTopInt(sampleHolder.timeUs),
Util.getBottomInt(sampleHolder.timeUs), sampleHolder).sendToTarget();
}
/**
@ -106,7 +108,7 @@ public final class SubtitleParserHelper implements Handler.Callback {
* @return The result of the parsing operation, or null.
* @throws IOException If the parsing operation failed.
*/
public synchronized Subtitle getAndClearResult() throws IOException {
public synchronized PlayableSubtitle getAndClearResult() throws IOException {
try {
if (error != null) {
throw error;
@ -120,22 +122,21 @@ public final class SubtitleParserHelper implements Handler.Callback {
@Override
public boolean handleMessage(Message msg) {
Subtitle result;
IOException error;
long sampleTimeUs = Util.getLong(msg.arg1, msg.arg2);
SampleHolder holder = (SampleHolder) msg.obj;
Subtitle parsedSubtitle = null;
IOException error = null;
try {
InputStream inputStream = new ByteArrayInputStream(holder.data.array(), 0, holder.size);
result = parser.parse(inputStream, null, sampleHolder.timeUs);
error = null;
parsedSubtitle = parser.parse(inputStream, null);
} catch (IOException e) {
result = null;
error = e;
}
synchronized (this) {
if (sampleHolder != holder) {
// A flush has occurred since this holder was posted. Do nothing.
} else {
this.result = result;
this.result = new PlayableSubtitle(sampleTimeUs, parsedSubtitle);
this.error = error;
this.parsing = false;
}

View file

@ -110,8 +110,8 @@ public final class TextTrackRenderer extends SampleSourceTrackRenderer implement
private int parserIndex;
private boolean inputStreamEnded;
private Subtitle subtitle;
private Subtitle nextSubtitle;
private PlayableSubtitle subtitle;
private PlayableSubtitle nextSubtitle;
private SubtitleParserHelper parserHelper;
private HandlerThread parserThread;
private int nextSubtitleEventIndex;
@ -208,7 +208,7 @@ public final class TextTrackRenderer extends SampleSourceTrackRenderer implement
}
if (subtitleNextEventTimeUs == Long.MAX_VALUE && nextSubtitle != null
&& nextSubtitle.getStartTime() <= positionUs) {
&& nextSubtitle.startTimeUs <= positionUs) {
// Advance to the next subtitle. Sync the next event index and trigger an update.
subtitle = nextSubtitle;
nextSubtitle = null;

View file

@ -50,8 +50,7 @@ public final class SubripParser implements SubtitleParser {
}
@Override
public SubripSubtitle parse(InputStream inputStream, String inputEncoding, long startTimeUs)
throws IOException {
public SubripSubtitle parse(InputStream inputStream, String inputEncoding) throws IOException {
ArrayList<Cue> cues = new ArrayList<>();
LongArray cueTimesUs = new LongArray();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, C.UTF8_NAME));
@ -69,8 +68,8 @@ public final class SubripParser implements SubtitleParser {
currentLine = reader.readLine();
Matcher matcher = SUBRIP_TIMING_LINE.matcher(currentLine);
if (matcher.find()) {
cueTimesUs.add(startTimeUs + parseTimestampUs(matcher.group(1)));
cueTimesUs.add(startTimeUs + parseTimestampUs(matcher.group(2)));
cueTimesUs.add(parseTimestampUs(matcher.group(1)));
cueTimesUs.add(parseTimestampUs(matcher.group(2)));
} else {
throw new ParserException("Expected timing line: " + currentLine);
}
@ -91,7 +90,7 @@ public final class SubripParser implements SubtitleParser {
Cue[] cuesArray = new Cue[cues.size()];
cues.toArray(cuesArray);
long[] cueTimesUsArray = cueTimesUs.toArray();
return new SubripSubtitle(startTimeUs, cuesArray, cueTimesUsArray);
return new SubripSubtitle(cuesArray, cueTimesUsArray);
}
@Override

View file

@ -28,27 +28,18 @@ import java.util.List;
*/
/* package */ final class SubripSubtitle implements Subtitle {
private final long startTimeUs;
private final Cue[] cues;
private final long[] cueTimesUs;
/**
* @param startTimeUs The start time of the subtitle, in microseconds.
* @param cues The cues in the subtitle.
* @param cueTimesUs Interleaved cue start and end times, in microseconds.
*/
public SubripSubtitle(long startTimeUs, Cue[] cues, long[] cueTimesUs) {
this.startTimeUs = startTimeUs;
public SubripSubtitle(Cue[] cues, long[] cueTimesUs) {
this.cues = cues;
this.cueTimesUs = cueTimesUs;
}
@Override
public long getStartTime() {
return startTimeUs;
}
@Override
public int getNextEventTimeIndex(long timeUs) {
int index = Util.binarySearchCeil(cueTimesUs, timeUs, false, false);

View file

@ -84,7 +84,7 @@ public final class TtmlParser implements SubtitleParser {
}
/**
* @param strictParsing If true, {@link #parse(InputStream, String, long)} will throw a
* @param strictParsing If true, {@link #parse(InputStream, String)} will throw a
* {@link ParserException} if the stream contains invalid data. If false, the parser will
* make a best effort to ignore minor errors in the stream. Note however that a
* {@link ParserException} will still be thrown when this is not possible.
@ -99,8 +99,7 @@ public final class TtmlParser implements SubtitleParser {
}
@Override
public Subtitle parse(InputStream inputStream, String inputEncoding, long startTimeUs)
throws IOException {
public Subtitle parse(InputStream inputStream, String inputEncoding) throws IOException {
try {
XmlPullParser xmlParser = xmlParserFactory.newPullParser();
xmlParser.setInput(inputStream, inputEncoding);
@ -137,7 +136,7 @@ public final class TtmlParser implements SubtitleParser {
parent.addChild(TtmlNode.buildTextNode(xmlParser.getText()));
} else if (eventType == XmlPullParser.END_TAG) {
if (xmlParser.getName().equals(TtmlNode.TAG_TT)) {
ttmlSubtitle = new TtmlSubtitle(nodeStack.getLast(), startTimeUs);
ttmlSubtitle = new TtmlSubtitle(nodeStack.getLast());
}
nodeStack.removeLast();
}

View file

@ -28,23 +28,16 @@ import java.util.List;
public final class TtmlSubtitle implements Subtitle {
private final TtmlNode root;
private final long startTimeUs;
private final long[] eventTimesUs;
public TtmlSubtitle(TtmlNode root, long startTimeUs) {
public TtmlSubtitle(TtmlNode root) {
this.root = root;
this.startTimeUs = startTimeUs;
this.eventTimesUs = root.getEventTimesUs();
}
@Override
public long getStartTime() {
return startTimeUs;
}
@Override
public int getNextEventTimeIndex(long timeUs) {
int index = Util.binarySearchCeil(eventTimesUs, timeUs - startTimeUs, false, false);
int index = Util.binarySearchCeil(eventTimesUs, timeUs, false, false);
return index < eventTimesUs.length ? index : -1;
}
@ -55,17 +48,17 @@ public final class TtmlSubtitle implements Subtitle {
@Override
public long getEventTime(int index) {
return eventTimesUs[index] + startTimeUs;
return eventTimesUs[index];
}
@Override
public long getLastEventTime() {
return (eventTimesUs.length == 0 ? -1 : eventTimesUs[eventTimesUs.length - 1]) + startTimeUs;
return (eventTimesUs.length == 0 ? -1 : eventTimesUs[eventTimesUs.length - 1]);
}
@Override
public List<Cue> getCues(long timeUs) {
CharSequence cueText = root.getText(timeUs - startTimeUs);
CharSequence cueText = root.getText(timeUs);
if (cueText == null) {
return Collections.<Cue>emptyList();
} else {

View file

@ -32,11 +32,10 @@ import java.io.InputStream;
public final class Tx3gParser implements SubtitleParser {
@Override
public Subtitle parse(InputStream inputStream, String inputEncoding, long startTimeUs)
throws IOException {
public Subtitle parse(InputStream inputStream, String inputEncoding) throws IOException {
DataInputStream dataInputStream = new DataInputStream(inputStream);
String cueText = dataInputStream.readUTF();
return new Tx3gSubtitle(startTimeUs, new Cue(cueText));
return new Tx3gSubtitle(new Cue(cueText));
}
@Override

View file

@ -27,22 +27,15 @@ import java.util.List;
*/
/* package */ final class Tx3gSubtitle implements Subtitle {
private final long startTimeUs;
private final List<Cue> cues;
public Tx3gSubtitle(long startTimeUs, Cue cue) {
this.startTimeUs = startTimeUs;
public Tx3gSubtitle(Cue cue) {
this.cues = Collections.singletonList(cue);
}
@Override
public long getStartTime() {
return startTimeUs;
}
@Override
public int getNextEventTimeIndex(long timeUs) {
return timeUs < startTimeUs ? 0 : -1;
return timeUs < 0 ? 0 : -1;
}
@Override
@ -53,17 +46,17 @@ import java.util.List;
@Override
public long getEventTime(int index) {
Assertions.checkArgument(index == 0);
return startTimeUs;
return 0;
}
@Override
public long getLastEventTime() {
return startTimeUs;
return 0;
}
@Override
public List<Cue> getCues(long timeUs) {
return timeUs >= startTimeUs ? cues : Collections.<Cue>emptyList();
return timeUs >= 0 ? cues : Collections.<Cue>emptyList();
}
}

View file

@ -74,7 +74,7 @@ public final class WebvttParser implements SubtitleParser {
}
/**
* @param strictParsing If true, {@link #parse(InputStream, String, long)} will throw a
* @param strictParsing If true, {@link #parse(InputStream, String)} will throw a
* {@link ParserException} if the stream contains invalid data. If false, the parser will
* make a best effort to ignore minor errors in the stream. Note however that a
* {@link ParserException} will still be thrown when this is not possible.
@ -85,7 +85,7 @@ public final class WebvttParser implements SubtitleParser {
}
@Override
public final WebvttSubtitle parse(InputStream inputStream, String inputEncoding, long startTimeUs)
public final WebvttSubtitle parse(InputStream inputStream, String inputEncoding)
throws IOException {
ArrayList<WebvttCue> subtitles = new ArrayList<>();
@ -142,7 +142,7 @@ public final class WebvttParser implements SubtitleParser {
if (!matcher.find()) {
throw new ParserException("Expected cue start time: " + line);
} else {
startTime = parseTimestampUs(matcher.group()) + startTimeUs;
startTime = parseTimestampUs(matcher.group());
}
// parse end timestamp
@ -151,7 +151,7 @@ public final class WebvttParser implements SubtitleParser {
throw new ParserException("Expected cue end time: " + line);
} else {
endTimeString = matcher.group();
endTime = parseTimestampUs(endTimeString) + startTimeUs;
endTime = parseTimestampUs(endTimeString);
}
// parse the (optional) cue setting list
@ -213,7 +213,7 @@ public final class WebvttParser implements SubtitleParser {
subtitles.add(cue);
}
return new WebvttSubtitle(subtitles, startTimeUs);
return new WebvttSubtitle(subtitles);
}
@Override

View file

@ -34,18 +34,14 @@ public final class WebvttSubtitle implements Subtitle {
private final List<WebvttCue> 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 WebvttSubtitle(List<WebvttCue> cues, long startTimeUs) {
public WebvttSubtitle(List<WebvttCue> cues) {
this.cues = cues;
this.startTimeUs = startTimeUs;
numCues = cues.size();
cueTimesUs = new long[2 * numCues];
for (int cueIndex = 0; cueIndex < numCues; cueIndex++) {
@ -58,11 +54,6 @@ public final class WebvttSubtitle implements Subtitle {
Arrays.sort(sortedCueTimesUs);
}
@Override
public long getStartTime() {
return startTimeUs;
}
@Override
public int getNextEventTimeIndex(long timeUs) {
Assertions.checkArgument(timeUs >= 0);