提交 0dc0d703 编写于 作者: O olly 提交者: Oliver Woodman

Use SimpleDecoder for subtitles.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=117543706
上级 ed4f8397
......@@ -37,7 +37,7 @@ public final class SubripParserTest extends InstrumentationTestCase {
public void testParseEmpty() throws IOException {
SubripParser parser = new SubripParser();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), EMPTY_FILE);
SubripSubtitle subtitle = parser.parse(bytes, 0, bytes.length);
SubripSubtitle subtitle = parser.decode(bytes, bytes.length);
// Assert that the subtitle is empty.
assertEquals(0, subtitle.getEventTimeCount());
assertTrue(subtitle.getCues(0).isEmpty());
......@@ -46,7 +46,7 @@ public final class SubripParserTest extends InstrumentationTestCase {
public void testParseTypical() throws IOException {
SubripParser parser = new SubripParser();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_FILE);
SubripSubtitle subtitle = parser.parse(bytes, 0, bytes.length);
SubripSubtitle subtitle = parser.decode(bytes, bytes.length);
assertEquals(6, subtitle.getEventTimeCount());
assertTypicalCue1(subtitle, 0);
assertTypicalCue2(subtitle, 2);
......@@ -56,7 +56,7 @@ public final class SubripParserTest extends InstrumentationTestCase {
public void testParseTypicalWithByteOrderMark() throws IOException {
SubripParser parser = new SubripParser();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_WITH_BYTE_ORDER_MARK);
SubripSubtitle subtitle = parser.parse(bytes, 0, bytes.length);
SubripSubtitle subtitle = parser.decode(bytes, bytes.length);
assertEquals(6, subtitle.getEventTimeCount());
assertTypicalCue1(subtitle, 0);
assertTypicalCue2(subtitle, 2);
......@@ -66,7 +66,7 @@ public final class SubripParserTest extends InstrumentationTestCase {
public void testParseTypicalExtraBlankLine() throws IOException {
SubripParser parser = new SubripParser();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_EXTRA_BLANK_LINE);
SubripSubtitle subtitle = parser.parse(bytes, 0, bytes.length);
SubripSubtitle subtitle = parser.decode(bytes, bytes.length);
assertEquals(6, subtitle.getEventTimeCount());
assertTypicalCue1(subtitle, 0);
assertTypicalCue2(subtitle, 2);
......@@ -77,7 +77,7 @@ public final class SubripParserTest extends InstrumentationTestCase {
// Parsing should succeed, parsing the first and third cues only.
SubripParser parser = new SubripParser();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_MISSING_TIMECODE);
SubripSubtitle subtitle = parser.parse(bytes, 0, bytes.length);
SubripSubtitle subtitle = parser.decode(bytes, bytes.length);
assertEquals(4, subtitle.getEventTimeCount());
assertTypicalCue1(subtitle, 0);
assertTypicalCue3(subtitle, 2);
......@@ -87,7 +87,7 @@ public final class SubripParserTest extends InstrumentationTestCase {
// Parsing should succeed, parsing the first and third cues only.
SubripParser parser = new SubripParser();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_MISSING_SEQUENCE);
SubripSubtitle subtitle = parser.parse(bytes, 0, bytes.length);
SubripSubtitle subtitle = parser.decode(bytes, bytes.length);
assertEquals(4, subtitle.getEventTimeCount());
assertTypicalCue1(subtitle, 0);
assertTypicalCue3(subtitle, 2);
......@@ -96,7 +96,7 @@ public final class SubripParserTest extends InstrumentationTestCase {
public void testParseNoEndTimecodes() throws IOException {
SubripParser parser = new SubripParser();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), NO_END_TIMECODES_FILE);
SubripSubtitle subtitle = parser.parse(bytes, 0, bytes.length);
SubripSubtitle subtitle = parser.decode(bytes, bytes.length);
// Test event count.
assertEquals(3, subtitle.getEventTimeCount());
......
......@@ -472,6 +472,6 @@ public final class TtmlParserTest extends InstrumentationTestCase {
private TtmlSubtitle getSubtitle(String file) throws IOException {
TtmlParser ttmlParser = new TtmlParser();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), file);
return ttmlParser.parse(bytes, 0, bytes.length);
return ttmlParser.decode(bytes, bytes.length);
}
}
......@@ -18,16 +18,11 @@ package com.google.android.exoplayer.text.webvtt;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.text.Cue;
import com.google.android.exoplayer.text.Subtitle;
import com.google.android.exoplayer.util.Util;
import android.util.ArraySet;
import junit.framework.TestCase;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
/**
* Unit test for {@link Mp4WebvttParser}.
......@@ -96,20 +91,20 @@ public final class Mp4WebvttParserTest extends TestCase {
// Positive tests.
public void testSingleCueSample() throws ParserException {
Subtitle result = parser.parse(SINGLE_CUE_SAMPLE, 0, SINGLE_CUE_SAMPLE.length);
Subtitle result = parser.decode(SINGLE_CUE_SAMPLE, SINGLE_CUE_SAMPLE.length);
Cue expectedCue = new Cue("Hello World"); // Line feed must be trimmed by the parser
assertMp4WebvttSubtitleEquals(result, expectedCue);
}
public void testTwoCuesSample() throws ParserException {
Subtitle result = parser.parse(DOUBLE_CUE_SAMPLE, 0, DOUBLE_CUE_SAMPLE.length);
Subtitle result = parser.decode(DOUBLE_CUE_SAMPLE, DOUBLE_CUE_SAMPLE.length);
Cue firstExpectedCue = new Cue("Hello World");
Cue secondExpectedCue = new Cue("Bye Bye");
assertMp4WebvttSubtitleEquals(result, firstExpectedCue, secondExpectedCue);
}
public void testNoCueSample() throws IOException {
Subtitle result = parser.parse(NO_CUE_SAMPLE, 0, NO_CUE_SAMPLE.length);
Subtitle result = parser.decode(NO_CUE_SAMPLE, NO_CUE_SAMPLE.length);
assertMp4WebvttSubtitleEquals(result, new Cue[] {});
}
......@@ -117,7 +112,7 @@ public final class Mp4WebvttParserTest extends TestCase {
public void testSampleWithIncompleteHeader() {
try {
parser.parse(INCOMPLETE_HEADER_SAMPLE, 0, INCOMPLETE_HEADER_SAMPLE.length);
parser.decode(INCOMPLETE_HEADER_SAMPLE, INCOMPLETE_HEADER_SAMPLE.length);
} catch (ParserException e) {
return;
}
......@@ -130,55 +125,31 @@ public final class Mp4WebvttParserTest extends TestCase {
* Asserts that the Subtitle's cues (which are all part of the event at t=0) are equal to the
* expected Cues.
*
* @param sub The parsed {@link Subtitle} to check.
* @param expectedCues Expected {@link Cue}s in order of appearance.
* @param subtitle The {@link Subtitle} to check.
* @param expectedCues The expected {@link Cue}s.
*/
private static void assertMp4WebvttSubtitleEquals(Subtitle sub, Cue... expectedCues) {
assertEquals(1, sub.getEventTimeCount());
assertEquals(0, sub.getEventTime(0));
List<Cue> subtitleCues = sub.getCues(0);
private static void assertMp4WebvttSubtitleEquals(Subtitle subtitle, Cue... expectedCues) {
assertEquals(1, subtitle.getEventTimeCount());
assertEquals(0, subtitle.getEventTime(0));
List<Cue> subtitleCues = subtitle.getCues(0);
assertEquals(expectedCues.length, subtitleCues.size());
for (int i = 0; i < subtitleCues.size(); i++) {
Set<String> differences = getCueDifferences(subtitleCues.get(i), expectedCues[i]);
assertTrue("Cues at position " + i + " are not equal. Different fields are "
+ Arrays.toString(differences.toArray()), differences.isEmpty());
assertCueEquals(expectedCues[i], subtitleCues.get(i));
}
}
/**
* Checks whether two non null cues are equal. Check fails if any of the Cues are null.
*
* @return a set that contains the names of the different fields.
* Asserts that two cues are equal.
*/
private static Set<String> getCueDifferences(Cue aCue, Cue anotherCue) {
assertNotNull(aCue);
assertNotNull(anotherCue);
Set<String> differences = new ArraySet<>();
if (aCue.line != anotherCue.line) {
differences.add("line: " + aCue.line + " | " + anotherCue.line);
}
if (aCue.lineAnchor != anotherCue.lineAnchor) {
differences.add("lineAnchor: " + aCue.lineAnchor + " | " + anotherCue.lineAnchor);
}
if (aCue.lineType != anotherCue.lineType) {
differences.add("lineType: " + aCue.lineType + " | " + anotherCue.lineType);
}
if (aCue.position != anotherCue.position) {
differences.add("position: " + aCue.position + " | " + anotherCue.position);
}
if (aCue.positionAnchor != anotherCue.positionAnchor) {
differences.add("positionAnchor: " + aCue.positionAnchor + " | " + anotherCue.positionAnchor);
}
if (aCue.size != anotherCue.size) {
differences.add("size: " + aCue.size + " | " + anotherCue.size);
}
if (!Util.areEqual(aCue.text.toString(), anotherCue.text.toString())) {
differences.add("text: '" + aCue.text + "' | '" + anotherCue.text + '\'');
}
if (!Util.areEqual(aCue.textAlignment, anotherCue.textAlignment)) {
differences.add("textAlignment: " + aCue.textAlignment + " | " + anotherCue.textAlignment);
}
return differences;
private static void assertCueEquals(Cue expected, Cue actual) {
assertEquals(expected.line, actual.line);
assertEquals(expected.lineAnchor, actual.lineAnchor);
assertEquals(expected.lineType, actual.lineType);
assertEquals(expected.position, actual.position);
assertEquals(expected.positionAnchor, actual.positionAnchor);
assertEquals(expected.size, actual.size);
assertEquals(expected.text.toString(), actual.text.toString());
assertEquals(expected.textAlignment, actual.textAlignment);
}
}
......@@ -42,7 +42,7 @@ public class WebvttParserTest extends InstrumentationTestCase {
WebvttParser parser = new WebvttParser();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), EMPTY_FILE);
try {
parser.parse(bytes, 0, bytes.length);
parser.decode(bytes, bytes.length);
fail("Expected ParserException");
} catch (ParserException expected) {
// Do nothing.
......@@ -52,7 +52,7 @@ public class WebvttParserTest extends InstrumentationTestCase {
public void testParseTypical() throws IOException {
WebvttParser parser = new WebvttParser();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_FILE);
WebvttSubtitle subtitle = parser.parse(bytes, 0, bytes.length);
WebvttSubtitle subtitle = parser.decode(bytes, bytes.length);
// test event count
assertEquals(4, subtitle.getEventTimeCount());
......@@ -65,7 +65,7 @@ public class WebvttParserTest extends InstrumentationTestCase {
public void testParseTypicalWithIds() throws IOException {
WebvttParser parser = new WebvttParser();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_WITH_IDS_FILE);
WebvttSubtitle subtitle = parser.parse(bytes, 0, bytes.length);
WebvttSubtitle subtitle = parser.decode(bytes, bytes.length);
// test event count
assertEquals(4, subtitle.getEventTimeCount());
......@@ -78,7 +78,7 @@ public class WebvttParserTest extends InstrumentationTestCase {
public void testParseTypicalWithComments() throws IOException {
WebvttParser parser = new WebvttParser();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_WITH_COMMENTS_FILE);
WebvttSubtitle subtitle = parser.parse(bytes, 0, bytes.length);
WebvttSubtitle subtitle = parser.decode(bytes, bytes.length);
// test event count
assertEquals(4, subtitle.getEventTimeCount());
......@@ -91,7 +91,7 @@ public class WebvttParserTest extends InstrumentationTestCase {
public void testParseWithTags() throws IOException {
WebvttParser parser = new WebvttParser();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), WITH_TAGS_FILE);
WebvttSubtitle subtitle = parser.parse(bytes, 0, bytes.length);
WebvttSubtitle subtitle = parser.decode(bytes, bytes.length);
// test event count
assertEquals(8, subtitle.getEventTimeCount());
......@@ -106,7 +106,7 @@ public class WebvttParserTest extends InstrumentationTestCase {
public void testParseWithPositioning() throws IOException {
WebvttParser parser = new WebvttParser();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), WITH_POSITIONING_FILE);
WebvttSubtitle subtitle = parser.parse(bytes, 0, bytes.length);
WebvttSubtitle subtitle = parser.decode(bytes, bytes.length);
// test event count
assertEquals(12, subtitle.getEventTimeCount());
......@@ -134,7 +134,7 @@ public class WebvttParserTest extends InstrumentationTestCase {
public void testParseWithBadCueHeader() throws IOException {
WebvttParser parser = new WebvttParser();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), WITH_BAD_CUE_HEADER_FILE);
WebvttSubtitle subtitle = parser.parse(bytes, 0, bytes.length);
WebvttSubtitle subtitle = parser.decode(bytes, bytes.length);
// test event count
assertEquals(4, subtitle.getEventTimeCount());
......
/*
* 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;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.util.extensions.InputBuffer;
/**
* An input buffer for {@link SubtitleParser}.
*/
/* package */ final class SubtitleInputBuffer extends InputBuffer {
public long subsampleOffsetUs;
public SubtitleInputBuffer() {
super(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
}
}
package com.google.android.exoplayer.text;
/*
* Copyright (C) 2014 The Android Open Source Project
*
......@@ -15,36 +13,32 @@ package com.google.android.exoplayer.text;
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.text;
import com.google.android.exoplayer.Format;
import com.google.android.exoplayer.util.extensions.OutputBuffer;
import java.util.List;
/**
* A subtitle that wraps another subtitle, making it playable by adjusting it to be correctly
* aligned with the playback timebase.
* A {@link Subtitle} output from a {@link SubtitleParser}.
*/
/* package */ final class PlayableSubtitle implements Subtitle {
/* package */ final class SubtitleOutputBuffer extends OutputBuffer 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 SubtitleParser owner;
private final Subtitle subtitle;
private final long offsetUs;
private Subtitle subtitle;
private long offsetUs;
/**
* @param subtitle The subtitle to wrap.
* @param isRelative True if the wrapped subtitle's timestamps are relative to the start time.
* False if they are absolute.
* @param startTimeUs The start time of the subtitle.
* @param offsetUs An offset to add to the subtitle timestamps.
*/
public PlayableSubtitle(Subtitle subtitle, boolean isRelative, long startTimeUs, long offsetUs) {
public SubtitleOutputBuffer(SubtitleParser owner) {
this.owner = owner;
}
public void setOutput(long timestampUs, Subtitle subtitle, long subsampleOffsetUs) {
this.timestampUs = timestampUs;
this.subtitle = subtitle;
this.startTimeUs = startTimeUs;
this.offsetUs = (isRelative ? startTimeUs : 0) + offsetUs;
this.offsetUs = subsampleOffsetUs == Format.OFFSET_SAMPLE_RELATIVE ? timestampUs
: subsampleOffsetUs;
}
@Override
......@@ -72,4 +66,9 @@ import java.util.List;
return subtitle.getCues(timeUs - offsetUs);
}
@Override
public void release() {
owner.releaseOutputBuffer(this);
}
}
......@@ -16,29 +16,48 @@
package com.google.android.exoplayer.text;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.util.extensions.SimpleDecoder;
/**
* Parses {@link Subtitle}s from a byte array.
* Parses {@link Subtitle}s from {@link SubtitleInputBuffer}s.
*/
public interface SubtitleParser {
/**
* Checks whether the parser supports a given subtitle mime type.
*
* @param mimeType A subtitle mime type.
* @return Whether the mime type is supported.
*/
public boolean canParse(String mimeType);
/**
* Parses a {@link Subtitle} from the provided {@code byte[]}.
*
* @param bytes The array holding the subtitle data.
* @param offset The offset of the subtitle data in bytes.
* @param length The length of the subtitle data in bytes.
* @return A parsed representation of the subtitle.
* @throws ParserException If a problem occurred parsing the subtitle data.
*/
public Subtitle parse(byte[] bytes, int offset, int length) throws ParserException;
public abstract class SubtitleParser extends
SimpleDecoder<SubtitleInputBuffer, SubtitleOutputBuffer, ParserException> {
protected SubtitleParser() {
super(new SubtitleInputBuffer[2], new SubtitleOutputBuffer[2]);
setInitialInputBufferSize(1024);
}
@Override
protected final SubtitleInputBuffer createInputBuffer() {
return new SubtitleInputBuffer();
}
@Override
protected final SubtitleOutputBuffer createOutputBuffer() {
return new SubtitleOutputBuffer(this);
}
@Override
protected final void releaseOutputBuffer(SubtitleOutputBuffer buffer) {
super.releaseOutputBuffer(buffer);
}
@Override
protected final ParserException decode(SubtitleInputBuffer inputBuffer,
SubtitleOutputBuffer outputBuffer) {
try {
Subtitle subtitle = decode(inputBuffer.sampleHolder.data.array(),
inputBuffer.sampleHolder.size);
outputBuffer.setOutput(inputBuffer.sampleHolder.timeUs, subtitle,
inputBuffer.subsampleOffsetUs);
return null;
} catch (ParserException e) {
return e;
}
}
protected abstract Subtitle decode(byte[] data, int size) throws ParserException;
}
/*
* 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;
import com.google.android.exoplayer.Format;
import com.google.android.exoplayer.util.MimeTypes;
/**
* A factory for {@link SubtitleParser} instances.
*/
public interface SubtitleParserFactory {
/**
* Returns whether the factory is able to instantiate a {@link SubtitleParser} for the given
* {@link Format}.
*
* @param format The {@link Format}.
* @return True if the factory can instantiate a suitable {@link SubtitleParser}. False otherwise.
*/
boolean supportsFormat(Format format);
/**
* Creates a {@link SubtitleParser} for the given {@link Format}.
*
* @param format The {@link Format}.
* @return A new {@link SubtitleParser}.
* @throws IllegalArgumentException If the {@link Format} is not supported.
*/
SubtitleParser createParser(Format format);
/**
* Default {@link SubtitleParserFactory} implementation.
* <p>
* The formats supported by this factory are:
* <ul>
* <li>WebVTT ({@link com.google.android.exoplayer.text.webvtt.WebvttParser})</li>
* <li>WebVTT (MP4) ({@link com.google.android.exoplayer.text.webvtt.Mp4WebvttParser})</li>
* <li>TTML ({@link com.google.android.exoplayer.text.ttml.TtmlParser})</li>
* <li>SubRip ({@link com.google.android.exoplayer.text.subrip.SubripParser})</li>
* <li>TX3G ({@link com.google.android.exoplayer.text.tx3g.Tx3gParser})</li>
* </ul>
*/
final SubtitleParserFactory DEFAULT = new SubtitleParserFactory() {
@Override
public boolean supportsFormat(Format format) {
return getParserClass(format.sampleMimeType) != null;
}
@Override
public SubtitleParser createParser(Format format) {
try {
Class<?> clazz = getParserClass(format.sampleMimeType);
if (clazz == null) {
throw new IllegalArgumentException("Attempted to create parser for unsupported format");
}
return clazz.asSubclass(SubtitleParser.class).newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new IllegalStateException("Unexpected error instantiating parser", e);
}
}
private Class<?> getParserClass(String mimeType) {
try {
switch (mimeType) {
case MimeTypes.TEXT_VTT:
return Class.forName("com.google.android.exoplayer.text.webvtt.WebvttParser");
case MimeTypes.APPLICATION_TTML:
return Class.forName("com.google.android.exoplayer.text.ttml.TtmlParser");
case MimeTypes.APPLICATION_MP4VTT:
return Class.forName("com.google.android.exoplayer.text.webvtt.Mp4WebvttParser");
case MimeTypes.APPLICATION_SUBRIP:
return Class.forName("com.google.android.exoplayer.text.subrip.SubripParser");
case MimeTypes.APPLICATION_TX3G:
return Class.forName("com.google.android.exoplayer.text.tx3g.Tx3gParser");
default:
return null;
}
} catch (ClassNotFoundException e) {
return null;
}
}
};
}
/*
* 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;
import com.google.android.exoplayer.Format;
import com.google.android.exoplayer.ParserException;
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;
import android.os.Looper;
import android.os.Message;
import java.io.IOException;
/**
* Wraps a {@link SubtitleParser}, exposing an interface similar to {@link MediaCodec} for
* asynchronous parsing of subtitles.
*/
/* package */ final class SubtitleParserHelper implements Handler.Callback {
private static final int MSG_FORMAT = 0;
private static final int MSG_SAMPLE = 1;
private final SubtitleParser parser;
private final Handler handler;
private SampleHolder sampleHolder;
private boolean parsing;
private PlayableSubtitle result;
private IOException error;
private RuntimeException runtimeError;
private boolean subtitlesAreRelative;
private long subtitleOffsetUs;
/**
* @param looper The {@link Looper} associated with the thread on which parsing should occur.
* @param parser The parser that should be used to parse the raw data.
*/
public SubtitleParserHelper(Looper looper, SubtitleParser parser) {
this.handler = new Handler(looper, this);
this.parser = parser;
flush();
}
/**
* Flushes the helper, canceling the current parsing operation, if there is one.
*/
public synchronized void flush() {
sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
parsing = false;
result = null;
error = null;
runtimeError = null;
}
/**
* Whether the helper is currently performing a parsing operation.
*
* @return True if the helper is currently performing a parsing operation. False otherwise.
*/
public synchronized boolean isParsing() {
return parsing;
}
/**
* Gets the holder that should be populated with data to be parsed.
* <p>
* The returned holder will remain valid unless {@link #flush()} is called. If {@link #flush()}
* is called the holder is replaced, and this method should be called again to obtain the new
* holder.
*
* @return The holder that should be populated with data to be parsed.
*/
public synchronized SampleHolder getSampleHolder() {
return sampleHolder;
}
/**
* Sets the format of subsequent samples.
*
* @param format The format.
*/
public void setFormat(Format format) {
handler.obtainMessage(MSG_FORMAT, format).sendToTarget();
}
/**
* Start a parsing operation.
* <p>
* The holder returned by {@link #getSampleHolder()} should be populated with the data to be
* parsed prior to calling this method.
*/
public synchronized void startParseOperation() {
Assertions.checkState(!parsing);
parsing = true;
result = null;
error = null;
runtimeError = null;
handler.obtainMessage(MSG_SAMPLE, Util.getTopInt(sampleHolder.timeUs),
Util.getBottomInt(sampleHolder.timeUs), sampleHolder).sendToTarget();
}
/**
* Gets the result of the most recent parsing operation.
* <p>
* The result is cleared as a result of calling this method, and so subsequent calls will return
* null until a subsequent parsing operation has finished.
*
* @return The result of the parsing operation, or null.
* @throws IOException If the parsing operation failed.
*/
public synchronized PlayableSubtitle getAndClearResult() throws IOException {
try {
if (error != null) {
throw error;
} else if (runtimeError != null) {
throw runtimeError;
} else {
return result;
}
} finally {
result = null;
error = null;
runtimeError = null;
}
}
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MSG_FORMAT:
handleFormat((Format) msg.obj);
break;
case MSG_SAMPLE:
long sampleTimeUs = Util.getLong(msg.arg1, msg.arg2);
SampleHolder holder = (SampleHolder) msg.obj;
handleSample(sampleTimeUs, holder);
break;
}
return true;
}
private void handleFormat(Format format) {
subtitlesAreRelative = format.subsampleOffsetUs == Format.OFFSET_SAMPLE_RELATIVE;
subtitleOffsetUs = subtitlesAreRelative ? 0 : format.subsampleOffsetUs;
}
private void handleSample(long sampleTimeUs, SampleHolder holder) {
Subtitle parsedSubtitle = null;
ParserException error = null;
RuntimeException runtimeError = null;
try {
parsedSubtitle = parser.parse(holder.data.array(), 0, holder.size);
} catch (ParserException e) {
error = e;
} catch (RuntimeException e) {
runtimeError = e;
}
synchronized (this) {
if (sampleHolder != holder) {
// A flush has occurred since this holder was posted. Do nothing.
} else {
this.result = new PlayableSubtitle(parsedSubtitle, subtitlesAreRelative, sampleTimeUs,
subtitleOffsetUs);
this.error = error;
this.runtimeError = runtimeError;
this.parsing = false;
}
}
}
}
......@@ -18,6 +18,7 @@ package com.google.android.exoplayer.text;
import com.google.android.exoplayer.ExoPlaybackException;
import com.google.android.exoplayer.Format;
import com.google.android.exoplayer.FormatHolder;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSourceTrackRenderer;
import com.google.android.exoplayer.TrackRenderer;
......@@ -28,158 +29,97 @@ import com.google.android.exoplayer.util.MimeTypes;
import android.annotation.TargetApi;
import android.os.Handler;
import android.os.Handler.Callback;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* A {@link TrackRenderer} for subtitles. Text is parsed from sample data using a
* {@link SubtitleParser}. The actual rendering of each line of text is delegated to a
* {@link TextRenderer}.
* A {@link TrackRenderer} for subtitles.
* <p>
* If no {@link SubtitleParser} instances are passed to the constructor, the subtitle type will be
* detected automatically for the following supported formats:
*
* <ul>
* <li>WebVTT ({@link com.google.android.exoplayer.text.webvtt.WebvttParser})</li>
* <li>TTML
* ({@link com.google.android.exoplayer.text.ttml.TtmlParser})</li>
* <li>SubRip
* ({@link com.google.android.exoplayer.text.subrip.SubripParser})</li>
* <li>TX3G
* ({@link com.google.android.exoplayer.text.tx3g.Tx3gParser})</li>
* </ul>
*
* <p>To override the default parsers, pass one or more {@link SubtitleParser} instances to the
* constructor. The first {@link SubtitleParser} that returns {@code true} from
* {@link SubtitleParser#canParse(String)} will be used.
* Text is parsed from sample data using {@link SubtitleParser} instances obtained from a
* {@link SubtitleParserFactory}. The actual rendering of each line of text is delegated to a
* {@link TextRenderer}.
*/
@TargetApi(16)
public final class TextTrackRenderer extends SampleSourceTrackRenderer implements Callback {
private static final int MSG_UPDATE_OVERLAY = 0;
/**
* Default parser classes in priority order. They are referred to indirectly so that it is
* possible to remove unused parsers.
*/
private static final List<Class<? extends SubtitleParser>> DEFAULT_PARSER_CLASSES;
static {
DEFAULT_PARSER_CLASSES = new ArrayList<>();
// Load parsers using reflection so that they can be deleted cleanly.
// Class.forName(<class name>) appears for each parser so that automated tools like proguard
// can detect the use of reflection (see http://proguard.sourceforge.net/FAQ.html#forname).
try {
DEFAULT_PARSER_CLASSES.add(
Class.forName("com.google.android.exoplayer.text.webvtt.WebvttParser")
.asSubclass(SubtitleParser.class));
} catch (ClassNotFoundException e) {
// Parser not found.
}
try {
DEFAULT_PARSER_CLASSES.add(
Class.forName("com.google.android.exoplayer.text.ttml.TtmlParser")
.asSubclass(SubtitleParser.class));
} catch (ClassNotFoundException e) {
// Parser not found.
}
try {
DEFAULT_PARSER_CLASSES.add(
Class.forName("com.google.android.exoplayer.text.webvtt.Mp4WebvttParser")
.asSubclass(SubtitleParser.class));
} catch (ClassNotFoundException e) {
// Parser not found.
}
try {
DEFAULT_PARSER_CLASSES.add(
Class.forName("com.google.android.exoplayer.text.subrip.SubripParser")
.asSubclass(SubtitleParser.class));
} catch (ClassNotFoundException e) {
// Parser not found.
}
try {
DEFAULT_PARSER_CLASSES.add(
Class.forName("com.google.android.exoplayer.text.tx3g.Tx3gParser")
.asSubclass(SubtitleParser.class));
} catch (ClassNotFoundException e) {
// Parser not found.
}
}
private final Handler textRendererHandler;
private final TextRenderer textRenderer;
private final SubtitleParserFactory parserFactory;
private final FormatHolder formatHolder;
private final SubtitleParser[] subtitleParsers;
private int parserIndex;
private boolean inputStreamEnded;
private PlayableSubtitle subtitle;
private PlayableSubtitle nextSubtitle;
private SubtitleParserHelper parserHelper;
private HandlerThread parserThread;
private SubtitleParser parser;
private SubtitleInputBuffer nextInputBuffer;
private SubtitleOutputBuffer subtitle;
private SubtitleOutputBuffer nextSubtitle;
private int nextSubtitleEventIndex;
/**
* @param textRenderer The text renderer.
* @param textRendererLooper The looper associated with the thread on which textRenderer should be
* invoked. If the renderer makes use of standard Android UI components, then this should
* normally be the looper associated with the applications' main thread, which can be
* obtained using {@link android.app.Activity#getMainLooper()}. Null may be passed if the
* renderer should be invoked directly on the player's internal rendering thread.
* @param subtitleParsers {@link SubtitleParser}s to parse text samples, in order of decreasing
* priority. If omitted, the default parsers will be used.
* normally be the looper associated with the application's main thread, which can be obtained
* using {@link android.app.Activity#getMainLooper()}. Null may be passed if the renderer
* should be invoked directly on the player's internal rendering thread.
*/
public TextTrackRenderer(TextRenderer textRenderer, Looper textRendererLooper) {
this(textRenderer, textRendererLooper, SubtitleParserFactory.DEFAULT);
}
/**
* @param textRenderer The text renderer.
* @param textRendererLooper The looper associated with the thread on which textRenderer should be
* invoked. If the renderer makes use of standard Android UI components, then this should
* normally be the looper associated with the application's main thread, which can be obtained
* using {@link android.app.Activity#getMainLooper()}. Null may be passed if the renderer
* should be invoked directly on the player's internal rendering thread.
* @param parserFactory A factory from which to obtain {@link SubtitleParser} instances.
*/
public TextTrackRenderer(TextRenderer textRenderer, Looper textRendererLooper,
SubtitleParser... subtitleParsers) {
SubtitleParserFactory parserFactory) {
this.textRenderer = Assertions.checkNotNull(textRenderer);
this.textRendererHandler = textRendererLooper == null ? null
: new Handler(textRendererLooper, this);
if (subtitleParsers == null || subtitleParsers.length == 0) {
subtitleParsers = new SubtitleParser[DEFAULT_PARSER_CLASSES.size()];
for (int i = 0; i < subtitleParsers.length; i++) {
try {
subtitleParsers[i] = DEFAULT_PARSER_CLASSES.get(i).newInstance();
} catch (InstantiationException e) {
throw new IllegalStateException("Unexpected error creating default parser", e);
} catch (IllegalAccessException e) {
throw new IllegalStateException("Unexpected error creating default parser", e);
}
}
}
this.subtitleParsers = subtitleParsers;
this.parserFactory = parserFactory;
formatHolder = new FormatHolder();
}
@Override
protected int supportsFormat(Format format) {
return getParserIndex(format.sampleMimeType) != -1 ? TrackRenderer.FORMAT_HANDLED
return parserFactory.supportsFormat(format) ? TrackRenderer.FORMAT_HANDLED
: (MimeTypes.isText(format.sampleMimeType) ? FORMAT_UNSUPPORTED_SUBTYPE
: FORMAT_UNSUPPORTED_TYPE);
: FORMAT_UNSUPPORTED_TYPE);
}
@Override
protected void onEnabled(Format[] formats, TrackStream trackStream, long positionUs,
boolean joining) throws ExoPlaybackException {
super.onEnabled(formats, trackStream, positionUs, joining);
parserIndex = getParserIndex(formats[0].sampleMimeType);
parserThread = new HandlerThread("textParser");
parserThread.start();
parserHelper = new SubtitleParserHelper(parserThread.getLooper(), subtitleParsers[parserIndex]);
parser = parserFactory.createParser(formats[0]);
parser.start();
}
@Override
protected void reset(long positionUs) {
inputStreamEnded = false;
subtitle = null;
nextSubtitle = null;
if (subtitle != null) {
subtitle.release();
subtitle = null;
}
if (nextSubtitle != null) {
nextSubtitle.release();
nextSubtitle = null;
}
nextInputBuffer = null;
clearTextRenderer();
if (parserHelper != null) {
parserHelper.flush();
if (parser != null) {
parser.flush();
}
}
......@@ -188,7 +128,7 @@ public final class TextTrackRenderer extends SampleSourceTrackRenderer implement
throws ExoPlaybackException {
if (nextSubtitle == null) {
try {
nextSubtitle = parserHelper.getAndClearResult();
nextSubtitle = parser.dequeueOutputBuffer();
} catch (IOException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
......@@ -211,8 +151,11 @@ public final class TextTrackRenderer extends SampleSourceTrackRenderer implement
}
}
if (nextSubtitle != null && nextSubtitle.startTimeUs <= positionUs) {
if (nextSubtitle != null && nextSubtitle.timestampUs <= positionUs) {
// Advance to the next subtitle. Sync the next event index and trigger an update.
if (subtitle != null) {
subtitle.release();
}
subtitle = nextSubtitle;
nextSubtitle = null;
nextSubtitleEventIndex = subtitle.getNextEventTimeIndex(positionUs);
......@@ -224,26 +167,45 @@ public final class TextTrackRenderer extends SampleSourceTrackRenderer implement
updateTextRenderer(subtitle.getCues(positionUs));
}
if (!inputStreamEnded && nextSubtitle == null && !parserHelper.isParsing()) {
// Try and read the next subtitle from the source.
SampleHolder sampleHolder = parserHelper.getSampleHolder();
sampleHolder.clearData();
int result = readSource(formatHolder, sampleHolder);
if (result == TrackStream.SAMPLE_READ) {
parserHelper.startParseOperation();
} else if (result == TrackStream.END_OF_STREAM) {
inputStreamEnded = true;
try {
if (!inputStreamEnded && nextSubtitle == null) {
if (nextInputBuffer == null) {
nextInputBuffer = parser.dequeueInputBuffer();
if (nextInputBuffer == null) {
return;
}
}
// Try and read the next subtitle from the source.
SampleHolder sampleHolder = nextInputBuffer.sampleHolder;
sampleHolder.clearData();
int result = readSource(formatHolder, sampleHolder);
if (result == TrackStream.SAMPLE_READ) {
nextInputBuffer.subsampleOffsetUs = formatHolder.format.subsampleOffsetUs;
parser.queueInputBuffer(nextInputBuffer);
} else if (result == TrackStream.END_OF_STREAM) {
inputStreamEnded = true;
}
}
} catch (ParserException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
}
@Override
protected void onDisabled() throws ExoPlaybackException {
subtitle = null;
nextSubtitle = null;
parserThread.quit();
parserThread = null;
parserHelper = null;
if (subtitle != null) {
subtitle.release();
subtitle = null;
}
if (nextSubtitle != null) {
nextSubtitle.release();
nextSubtitle = null;
}
if (parser != null) {
parser.release();
parser = null;
}
nextInputBuffer = null;
clearTextRenderer();
super.onDisabled();
}
......@@ -293,13 +255,4 @@ public final class TextTrackRenderer extends SampleSourceTrackRenderer implement
textRenderer.onCues(cues);
}
private int getParserIndex(String sampleMimeType) {
for (int i = 0; i < subtitleParsers.length; i++) {
if (subtitleParsers[i].canParse(sampleMimeType)) {
return i;
}
}
return -1;
}
}
......@@ -18,7 +18,6 @@ package com.google.android.exoplayer.text.subrip;
import com.google.android.exoplayer.text.Cue;
import com.google.android.exoplayer.text.SubtitleParser;
import com.google.android.exoplayer.util.LongArray;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableByteArray;
import android.text.Html;
......@@ -33,7 +32,7 @@ import java.util.regex.Pattern;
/**
* A simple SubRip parser.
*/
public final class SubripParser implements SubtitleParser {
public final class SubripParser extends SubtitleParser {
private static final String TAG = "SubripParser";
......@@ -48,16 +47,10 @@ public final class SubripParser implements SubtitleParser {
}
@Override
public boolean canParse(String mimeType) {
return MimeTypes.APPLICATION_SUBRIP.equals(mimeType);
}
@Override
public SubripSubtitle parse(byte[] bytes, int offset, int length) {
protected SubripSubtitle decode(byte[] bytes, int length) {
ArrayList<Cue> cues = new ArrayList<>();
LongArray cueTimesUs = new LongArray();
ParsableByteArray subripData = new ParsableByteArray(bytes, offset + length);
subripData.setPosition(offset);
ParsableByteArray subripData = new ParsableByteArray(bytes, length);
boolean haveEndTimecode;
String currentLine;
......
......@@ -18,7 +18,6 @@ package com.google.android.exoplayer.text.ttml;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.text.SubtitleParser;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParserUtil;
import com.google.android.exoplayer.util.Util;
......@@ -58,7 +57,7 @@ import java.util.regex.Pattern;
* </p>
* @see <a href="http://www.w3.org/TR/ttaf1-dfxp/">TTML specification</a>
*/
public final class TtmlParser implements SubtitleParser {
public final class TtmlParser extends SubtitleParser {
private static final String TAG = "TtmlParser";
......@@ -91,16 +90,11 @@ public final class TtmlParser implements SubtitleParser {
}
@Override
public boolean canParse(String mimeType) {
return MimeTypes.APPLICATION_TTML.equals(mimeType);
}
@Override
public TtmlSubtitle parse(byte[] bytes, int offset, int length) throws ParserException {
protected TtmlSubtitle decode(byte[] bytes, int length) throws ParserException {
try {
XmlPullParser xmlParser = xmlParserFactory.newPullParser();
Map<String, TtmlStyle> globalStyles = new HashMap<>();
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes, offset, length);
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes, 0, length);
xmlParser.setInput(inputStream, null);
TtmlSubtitle ttmlSubtitle = null;
LinkedList<TtmlNode> nodeStack = new LinkedList<>();
......
......@@ -18,23 +18,17 @@ package com.google.android.exoplayer.text.tx3g;
import com.google.android.exoplayer.text.Cue;
import com.google.android.exoplayer.text.Subtitle;
import com.google.android.exoplayer.text.SubtitleParser;
import com.google.android.exoplayer.util.MimeTypes;
/**
* A {@link SubtitleParser} for tx3g.
* <p>
* Currently only supports parsing of a single text track.
*/
public final class Tx3gParser implements SubtitleParser {
public final class Tx3gParser extends SubtitleParser {
@Override
public boolean canParse(String mimeType) {
return MimeTypes.APPLICATION_TX3G.equals(mimeType);
}
@Override
public Subtitle parse(byte[] bytes, int offset, int length) {
String cueText = new String(bytes, offset, length);
protected Subtitle decode(byte[] bytes, int length) {
String cueText = new String(bytes, 0, length);
return new Tx3gSubtitle(new Cue(cueText));
}
......
......@@ -18,7 +18,6 @@ package com.google.android.exoplayer.text.webvtt;
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 com.google.android.exoplayer.util.ParsableByteArray;
import com.google.android.exoplayer.util.Util;
......@@ -28,7 +27,7 @@ import java.util.List;
/**
* A {@link SubtitleParser} for Webvtt embedded in a Mp4 container file.
*/
public final class Mp4WebvttParser implements SubtitleParser {
public final class Mp4WebvttParser extends SubtitleParser {
private static final int BOX_HEADER_SIZE = 8;
......@@ -45,16 +44,10 @@ public final class Mp4WebvttParser implements SubtitleParser {
}
@Override
public boolean canParse(String mimeType) {
return MimeTypes.APPLICATION_MP4VTT.equals(mimeType);
}
@Override
public Mp4WebvttSubtitle parse(byte[] bytes, int offset, int length) throws ParserException {
protected Mp4WebvttSubtitle decode(byte[] bytes, int length) throws ParserException {
// Webvtt in Mp4 samples have boxes inside of them, so we have to do a traditional box parsing:
// first 4 bytes size and then 4 bytes type.
sampleData.reset(bytes, offset + length);
sampleData.setPosition(offset);
sampleData.reset(bytes, length);
List<Cue> resultingCueList = new ArrayList<>();
while (sampleData.bytesLeft() > 0) {
if (sampleData.bytesLeft() < BOX_HEADER_SIZE) {
......
......@@ -17,7 +17,6 @@ package com.google.android.exoplayer.text.webvtt;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.text.SubtitleParser;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableByteArray;
import android.text.TextUtils;
......@@ -29,7 +28,7 @@ import java.util.ArrayList;
* <p>
* @see <a href="http://dev.w3.org/html5/webvtt">WebVTT specification</a>
*/
public final class WebvttParser implements SubtitleParser {
public final class WebvttParser extends SubtitleParser {
private final WebvttCueParser cueParser;
private final ParsableByteArray parsableWebvttData;
......@@ -42,14 +41,8 @@ public final class WebvttParser implements SubtitleParser {
}
@Override
public final boolean canParse(String mimeType) {
return MimeTypes.TEXT_VTT.equals(mimeType);
}
@Override
public final WebvttSubtitle parse(byte[] bytes, int offset, int length) throws ParserException {
parsableWebvttData.reset(bytes, offset + length);
parsableWebvttData.setPosition(offset);
protected final WebvttSubtitle decode(byte[] bytes, int length) throws ParserException {
parsableWebvttData.reset(bytes, length);
webvttCueBuilder.reset(); // In case a previous parse run failed with a ParserException.
// Validate the first line of the header, and skip the remainder.
......
......@@ -25,7 +25,11 @@ public class InputBuffer extends Buffer {
public final SampleHolder sampleHolder;
public InputBuffer() {
sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DIRECT);
this(SampleHolder.BUFFER_REPLACEMENT_MODE_DIRECT);
}
public InputBuffer(int bufferReplacementMode) {
sampleHolder = new SampleHolder(bufferReplacementMode);
}
@Override
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册