mirror of
https://github.com/androidx/media.git
synced 2025-05-11 17:49:52 +08:00
Use SimpleDecoder for subtitles.
------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=117543706
This commit is contained in:
parent
ed4f83979e
commit
0dc0d70397
@ -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());
|
||||
|
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,3 @@
|
||||
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 {
|
||||
public abstract class SubtitleParser extends
|
||||
SimpleDecoder<SubtitleInputBuffer, SubtitleOutputBuffer, ParserException> {
|
||||
|
||||
/**
|
||||
* 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);
|
||||
protected SubtitleParser() {
|
||||
super(new SubtitleInputBuffer[2], new SubtitleOutputBuffer[2]);
|
||||
setInitialInputBufferSize(1024);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
@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;
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
@ -1,189 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
|
Loading…
x
Reference in New Issue
Block a user