mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Support MKV embedded SubRip captions.
This commit is contained in:
parent
009d4d0c2c
commit
83568ca52f
5 changed files with 161 additions and 21 deletions
11
library/src/androidTest/assets/subrip/no_end_timecodes
Normal file
11
library/src/androidTest/assets/subrip/no_end_timecodes
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
1
|
||||||
|
00:00:00,000 -->
|
||||||
|
SubRip doesn't technically allow missing end timecodes.
|
||||||
|
|
||||||
|
2
|
||||||
|
00:00:02,345 -->
|
||||||
|
We interpret it to mean that a subtitle extends to the start of the next one.
|
||||||
|
|
||||||
|
3
|
||||||
|
00:00:03,456 -->
|
||||||
|
Or to the end of the media.
|
||||||
|
|
@ -25,13 +25,14 @@ import java.io.InputStream;
|
||||||
*/
|
*/
|
||||||
public final class SubripParserTest extends InstrumentationTestCase {
|
public final class SubripParserTest extends InstrumentationTestCase {
|
||||||
|
|
||||||
private static final String TYPICAL_SUBRIP_FILE = "subrip/typical";
|
private static final String EMPTY_FILE = "subrip/empty";
|
||||||
private static final String EMPTY_SUBRIP_FILE = "subrip/empty";
|
private static final String TYPICAL_FILE = "subrip/typical";
|
||||||
|
private static final String NO_END_TIMECODES_FILE = "subrip/no_end_timecodes";
|
||||||
|
|
||||||
public void testParseNullSubripFile() throws IOException {
|
public void testParseEmptySubripFile() throws IOException {
|
||||||
SubripParser parser = new SubripParser();
|
SubripParser parser = new SubripParser();
|
||||||
InputStream inputStream =
|
InputStream inputStream =
|
||||||
getInstrumentation().getContext().getResources().getAssets().open(EMPTY_SUBRIP_FILE);
|
getInstrumentation().getContext().getResources().getAssets().open(EMPTY_FILE);
|
||||||
SubripSubtitle subtitle = parser.parse(inputStream);
|
SubripSubtitle subtitle = parser.parse(inputStream);
|
||||||
// Assert that the subtitle is empty.
|
// Assert that the subtitle is empty.
|
||||||
assertEquals(0, subtitle.getEventTimeCount());
|
assertEquals(0, subtitle.getEventTimeCount());
|
||||||
|
|
@ -41,7 +42,7 @@ public final class SubripParserTest extends InstrumentationTestCase {
|
||||||
public void testParseTypicalSubripFile() throws IOException {
|
public void testParseTypicalSubripFile() throws IOException {
|
||||||
SubripParser parser = new SubripParser();
|
SubripParser parser = new SubripParser();
|
||||||
InputStream inputStream =
|
InputStream inputStream =
|
||||||
getInstrumentation().getContext().getResources().getAssets().open(TYPICAL_SUBRIP_FILE);
|
getInstrumentation().getContext().getResources().getAssets().open(TYPICAL_FILE);
|
||||||
SubripSubtitle subtitle = parser.parse(inputStream);
|
SubripSubtitle subtitle = parser.parse(inputStream);
|
||||||
|
|
||||||
// Test event count.
|
// Test event count.
|
||||||
|
|
@ -60,4 +61,29 @@ public final class SubripParserTest extends InstrumentationTestCase {
|
||||||
assertEquals(3456000, subtitle.getEventTime(3));
|
assertEquals(3456000, subtitle.getEventTime(3));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void testParseNoEndTimecodes() throws IOException {
|
||||||
|
SubripParser parser = new SubripParser();
|
||||||
|
InputStream inputStream = getInstrumentation().getContext().getResources().getAssets()
|
||||||
|
.open(NO_END_TIMECODES_FILE);
|
||||||
|
SubripSubtitle subtitle = parser.parse(inputStream);
|
||||||
|
|
||||||
|
// Test event count.
|
||||||
|
assertEquals(3, subtitle.getEventTimeCount());
|
||||||
|
|
||||||
|
// Test first cue.
|
||||||
|
assertEquals(0, subtitle.getEventTime(0));
|
||||||
|
assertEquals("SubRip doesn't technically allow missing end timecodes.",
|
||||||
|
subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString());
|
||||||
|
|
||||||
|
// Test second cue.
|
||||||
|
assertEquals(2345000, subtitle.getEventTime(1));
|
||||||
|
assertEquals("We interpret it to mean that a subtitle extends to the start of the next one.",
|
||||||
|
subtitle.getCues(subtitle.getEventTime(1)).get(0).text.toString());
|
||||||
|
|
||||||
|
// Test third cue.
|
||||||
|
assertEquals(3456000, subtitle.getEventTime(2));
|
||||||
|
assertEquals("Or to the end of the media.",
|
||||||
|
subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,8 @@ public final class WebmExtractor implements Extractor {
|
||||||
private static final String CODEC_ID_AAC = "A_AAC";
|
private static final String CODEC_ID_AAC = "A_AAC";
|
||||||
private static final String CODEC_ID_MP3 = "A_MPEG/L3";
|
private static final String CODEC_ID_MP3 = "A_MPEG/L3";
|
||||||
private static final String CODEC_ID_AC3 = "A_AC3";
|
private static final String CODEC_ID_AC3 = "A_AC3";
|
||||||
|
private static final String CODEC_ID_SUBRIP = "S_TEXT/UTF8";
|
||||||
|
|
||||||
private static final int VORBIS_MAX_INPUT_SIZE = 8192;
|
private static final int VORBIS_MAX_INPUT_SIZE = 8192;
|
||||||
private static final int OPUS_MAX_INPUT_SIZE = 5760;
|
private static final int OPUS_MAX_INPUT_SIZE = 5760;
|
||||||
private static final int MP3_MAX_INPUT_SIZE = 4096;
|
private static final int MP3_MAX_INPUT_SIZE = 4096;
|
||||||
|
|
@ -98,6 +100,7 @@ public final class WebmExtractor implements Extractor {
|
||||||
private static final int ID_SIMPLE_BLOCK = 0xA3;
|
private static final int ID_SIMPLE_BLOCK = 0xA3;
|
||||||
private static final int ID_BLOCK_GROUP = 0xA0;
|
private static final int ID_BLOCK_GROUP = 0xA0;
|
||||||
private static final int ID_BLOCK = 0xA1;
|
private static final int ID_BLOCK = 0xA1;
|
||||||
|
private static final int ID_BLOCK_DURATION = 0x9B;
|
||||||
private static final int ID_REFERENCE_BLOCK = 0xFB;
|
private static final int ID_REFERENCE_BLOCK = 0xFB;
|
||||||
private static final int ID_TRACKS = 0x1654AE6B;
|
private static final int ID_TRACKS = 0x1654AE6B;
|
||||||
private static final int ID_TRACK_ENTRY = 0xAE;
|
private static final int ID_TRACK_ENTRY = 0xAE;
|
||||||
|
|
@ -131,12 +134,39 @@ public final class WebmExtractor implements Extractor {
|
||||||
private static final int ID_CUE_TIME = 0xB3;
|
private static final int ID_CUE_TIME = 0xB3;
|
||||||
private static final int ID_CUE_TRACK_POSITIONS = 0xB7;
|
private static final int ID_CUE_TRACK_POSITIONS = 0xB7;
|
||||||
private static final int ID_CUE_CLUSTER_POSITION = 0xF1;
|
private static final int ID_CUE_CLUSTER_POSITION = 0xF1;
|
||||||
|
private static final int ID_LANGUAGE = 0x22B59C;
|
||||||
|
|
||||||
private static final int LACING_NONE = 0;
|
private static final int LACING_NONE = 0;
|
||||||
private static final int LACING_XIPH = 1;
|
private static final int LACING_XIPH = 1;
|
||||||
private static final int LACING_FIXED_SIZE = 2;
|
private static final int LACING_FIXED_SIZE = 2;
|
||||||
private static final int LACING_EBML = 3;
|
private static final int LACING_EBML = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A template for the prefix that must be added to each subrip sample. The 12 byte end timecode
|
||||||
|
* starting at {@link #SUBRIP_PREFIX_END_TIMECODE_OFFSET} is set to a dummy value, and must be
|
||||||
|
* replaced with the duration of the subtitle.
|
||||||
|
* <p>
|
||||||
|
* Equivalent to the UTF-8 string: "1\n00:00:00,000 --> 00:00:00,000\n".
|
||||||
|
*/
|
||||||
|
private static final byte[] SUBRIP_PREFIX = new byte[] {49, 10, 48, 48, 58, 48, 48, 58, 48, 48,
|
||||||
|
44, 48, 48, 48, 32, 45, 45, 62, 32, 48, 48, 58, 48, 48, 58, 48, 48, 44, 48, 48, 48, 10};
|
||||||
|
/**
|
||||||
|
* A special end timecode indicating that a subtitle should be displayed until the next subtitle,
|
||||||
|
* or until the end of the media in the case of the last subtitle.
|
||||||
|
* <p>
|
||||||
|
* Equivalent to the UTF-8 string: " ".
|
||||||
|
*/
|
||||||
|
private static final byte[] SUBRIP_TIMECODE_EMPTY =
|
||||||
|
new byte[] {32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32};
|
||||||
|
/**
|
||||||
|
* The byte offset of the end timecode in {@link #SUBRIP_PREFIX}.
|
||||||
|
*/
|
||||||
|
private static final int SUBRIP_PREFIX_END_TIMECODE_OFFSET = 19;
|
||||||
|
/**
|
||||||
|
* The length in bytes of a timecode in a subrip prefix.
|
||||||
|
*/
|
||||||
|
private static final int SUBRIP_TIMECODE_LENGTH = 12;
|
||||||
|
|
||||||
private final EbmlReader reader;
|
private final EbmlReader reader;
|
||||||
private final VarintReader varintReader;
|
private final VarintReader varintReader;
|
||||||
private final SparseArray<Track> tracks;
|
private final SparseArray<Track> tracks;
|
||||||
|
|
@ -148,6 +178,7 @@ public final class WebmExtractor implements Extractor {
|
||||||
private final ParsableByteArray vorbisNumPageSamples;
|
private final ParsableByteArray vorbisNumPageSamples;
|
||||||
private final ParsableByteArray seekEntryIdBytes;
|
private final ParsableByteArray seekEntryIdBytes;
|
||||||
private final ParsableByteArray sampleStrippedBytes;
|
private final ParsableByteArray sampleStrippedBytes;
|
||||||
|
private final ParsableByteArray subripSample;
|
||||||
|
|
||||||
private long segmentContentPosition = UNKNOWN;
|
private long segmentContentPosition = UNKNOWN;
|
||||||
private long segmentContentSize = UNKNOWN;
|
private long segmentContentSize = UNKNOWN;
|
||||||
|
|
@ -178,13 +209,13 @@ public final class WebmExtractor implements Extractor {
|
||||||
// Block reading state.
|
// Block reading state.
|
||||||
private int blockState;
|
private int blockState;
|
||||||
private long blockTimeUs;
|
private long blockTimeUs;
|
||||||
|
private long blockDurationUs;
|
||||||
private int blockLacingSampleIndex;
|
private int blockLacingSampleIndex;
|
||||||
private int blockLacingSampleCount;
|
private int blockLacingSampleCount;
|
||||||
private int[] blockLacingSampleSizes;
|
private int[] blockLacingSampleSizes;
|
||||||
private int blockTrackNumber;
|
private int blockTrackNumber;
|
||||||
private int blockTrackNumberLength;
|
private int blockTrackNumberLength;
|
||||||
private int blockFlags;
|
private int blockFlags;
|
||||||
private byte[] blockEncryptionKeyId;
|
|
||||||
|
|
||||||
// Sample reading state.
|
// Sample reading state.
|
||||||
private int sampleBytesRead;
|
private int sampleBytesRead;
|
||||||
|
|
@ -212,6 +243,7 @@ public final class WebmExtractor implements Extractor {
|
||||||
nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
|
nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
|
||||||
nalLength = new ParsableByteArray(4);
|
nalLength = new ParsableByteArray(4);
|
||||||
sampleStrippedBytes = new ParsableByteArray();
|
sampleStrippedBytes = new ParsableByteArray();
|
||||||
|
subripSample = new ParsableByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -274,6 +306,7 @@ public final class WebmExtractor implements Extractor {
|
||||||
case ID_SEEK_POSITION:
|
case ID_SEEK_POSITION:
|
||||||
case ID_TIMECODE_SCALE:
|
case ID_TIMECODE_SCALE:
|
||||||
case ID_TIME_CODE:
|
case ID_TIME_CODE:
|
||||||
|
case ID_BLOCK_DURATION:
|
||||||
case ID_PIXEL_WIDTH:
|
case ID_PIXEL_WIDTH:
|
||||||
case ID_PIXEL_HEIGHT:
|
case ID_PIXEL_HEIGHT:
|
||||||
case ID_TRACK_NUMBER:
|
case ID_TRACK_NUMBER:
|
||||||
|
|
@ -293,6 +326,7 @@ public final class WebmExtractor implements Extractor {
|
||||||
return EbmlReader.TYPE_UNSIGNED_INT;
|
return EbmlReader.TYPE_UNSIGNED_INT;
|
||||||
case ID_DOC_TYPE:
|
case ID_DOC_TYPE:
|
||||||
case ID_CODEC_ID:
|
case ID_CODEC_ID:
|
||||||
|
case ID_LANGUAGE:
|
||||||
return EbmlReader.TYPE_STRING;
|
return EbmlReader.TYPE_STRING;
|
||||||
case ID_SEEK_ID:
|
case ID_SEEK_ID:
|
||||||
case ID_CONTENT_COMPRESSION_SETTINGS:
|
case ID_CONTENT_COMPRESSION_SETTINGS:
|
||||||
|
|
@ -397,7 +431,7 @@ public final class WebmExtractor implements Extractor {
|
||||||
if (!sampleSeenReferenceBlock) {
|
if (!sampleSeenReferenceBlock) {
|
||||||
blockFlags |= C.SAMPLE_FLAG_SYNC;
|
blockFlags |= C.SAMPLE_FLAG_SYNC;
|
||||||
}
|
}
|
||||||
outputSampleMetadata(tracks.get(blockTrackNumber), blockTimeUs);
|
commitSampleToOutput(tracks.get(blockTrackNumber), blockTimeUs);
|
||||||
blockState = BLOCK_STATE_START;
|
blockState = BLOCK_STATE_START;
|
||||||
return;
|
return;
|
||||||
case ID_CONTENT_ENCODING:
|
case ID_CONTENT_ENCODING:
|
||||||
|
|
@ -531,6 +565,9 @@ public final class WebmExtractor implements Extractor {
|
||||||
case ID_TIME_CODE:
|
case ID_TIME_CODE:
|
||||||
clusterTimecodeUs = scaleTimecodeToUs(value);
|
clusterTimecodeUs = scaleTimecodeToUs(value);
|
||||||
return;
|
return;
|
||||||
|
case ID_BLOCK_DURATION:
|
||||||
|
blockDurationUs = scaleTimecodeToUs(value);
|
||||||
|
return;
|
||||||
default:
|
default:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -560,6 +597,9 @@ public final class WebmExtractor implements Extractor {
|
||||||
case ID_CODEC_ID:
|
case ID_CODEC_ID:
|
||||||
currentTrack.codecId = value;
|
currentTrack.codecId = value;
|
||||||
return;
|
return;
|
||||||
|
case ID_LANGUAGE:
|
||||||
|
currentTrack.language = value;
|
||||||
|
return;
|
||||||
default:
|
default:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -597,6 +637,7 @@ public final class WebmExtractor implements Extractor {
|
||||||
if (blockState == BLOCK_STATE_START) {
|
if (blockState == BLOCK_STATE_START) {
|
||||||
blockTrackNumber = (int) varintReader.readUnsignedVarint(input, false, true);
|
blockTrackNumber = (int) varintReader.readUnsignedVarint(input, false, true);
|
||||||
blockTrackNumberLength = varintReader.getLastLength();
|
blockTrackNumberLength = varintReader.getLastLength();
|
||||||
|
blockDurationUs = UNKNOWN;
|
||||||
blockState = BLOCK_STATE_HEADER;
|
blockState = BLOCK_STATE_HEADER;
|
||||||
scratch.reset();
|
scratch.reset();
|
||||||
}
|
}
|
||||||
|
|
@ -698,7 +739,6 @@ public final class WebmExtractor implements Extractor {
|
||||||
|| (id == ID_SIMPLE_BLOCK && (scratch.data[2] & 0x80) == 0x80);
|
|| (id == ID_SIMPLE_BLOCK && (scratch.data[2] & 0x80) == 0x80);
|
||||||
blockFlags = (isKeyframe ? C.SAMPLE_FLAG_SYNC : 0)
|
blockFlags = (isKeyframe ? C.SAMPLE_FLAG_SYNC : 0)
|
||||||
| (isInvisible ? C.SAMPLE_FLAG_DECODE_ONLY : 0);
|
| (isInvisible ? C.SAMPLE_FLAG_DECODE_ONLY : 0);
|
||||||
blockEncryptionKeyId = track.encryptionKeyId;
|
|
||||||
blockState = BLOCK_STATE_DATA;
|
blockState = BLOCK_STATE_DATA;
|
||||||
blockLacingSampleIndex = 0;
|
blockLacingSampleIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
@ -709,7 +749,7 @@ public final class WebmExtractor implements Extractor {
|
||||||
writeSampleData(input, track, blockLacingSampleSizes[blockLacingSampleIndex]);
|
writeSampleData(input, track, blockLacingSampleSizes[blockLacingSampleIndex]);
|
||||||
long sampleTimeUs = this.blockTimeUs
|
long sampleTimeUs = this.blockTimeUs
|
||||||
+ (blockLacingSampleIndex * track.defaultSampleDurationNs) / 1000;
|
+ (blockLacingSampleIndex * track.defaultSampleDurationNs) / 1000;
|
||||||
outputSampleMetadata(track, sampleTimeUs);
|
commitSampleToOutput(track, sampleTimeUs);
|
||||||
blockLacingSampleIndex++;
|
blockLacingSampleIndex++;
|
||||||
}
|
}
|
||||||
blockState = BLOCK_STATE_START;
|
blockState = BLOCK_STATE_START;
|
||||||
|
|
@ -725,8 +765,11 @@ public final class WebmExtractor implements Extractor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void outputSampleMetadata(Track track, long timeUs) {
|
private void commitSampleToOutput(Track track, long timeUs) {
|
||||||
track.output.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, blockEncryptionKeyId);
|
if (CODEC_ID_SUBRIP.equals(track.codecId)) {
|
||||||
|
writeSubripSample(track);
|
||||||
|
}
|
||||||
|
track.output.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, track.encryptionKeyId);
|
||||||
sampleRead = true;
|
sampleRead = true;
|
||||||
resetSample();
|
resetSample();
|
||||||
}
|
}
|
||||||
|
|
@ -758,6 +801,21 @@ public final class WebmExtractor implements Extractor {
|
||||||
|
|
||||||
private void writeSampleData(ExtractorInput input, Track track, int size)
|
private void writeSampleData(ExtractorInput input, Track track, int size)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
|
if (CODEC_ID_SUBRIP.equals(track.codecId)) {
|
||||||
|
int sizeWithPrefix = SUBRIP_PREFIX.length + size;
|
||||||
|
if (subripSample.capacity() < sizeWithPrefix) {
|
||||||
|
// Initialize subripSample to contain the required prefix and have space to hold a subtitle
|
||||||
|
// twice as long as this one.
|
||||||
|
subripSample.data = Arrays.copyOf(SUBRIP_PREFIX, sizeWithPrefix + size);
|
||||||
|
}
|
||||||
|
input.readFully(subripSample.data, SUBRIP_PREFIX.length, size);
|
||||||
|
subripSample.setPosition(0);
|
||||||
|
subripSample.setLimit(sizeWithPrefix);
|
||||||
|
// Defer writing the data to the track output. We need to modify the sample data by setting
|
||||||
|
// the correct end timecode, which we might not have yet.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
TrackOutput output = track.output;
|
TrackOutput output = track.output;
|
||||||
if (!sampleEncodingHandled) {
|
if (!sampleEncodingHandled) {
|
||||||
if (track.hasContentEncryption) {
|
if (track.hasContentEncryption) {
|
||||||
|
|
@ -834,6 +892,33 @@ public final class WebmExtractor implements Extractor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void writeSubripSample(Track track) {
|
||||||
|
setSubripSampleEndTimecode(subripSample.data, blockDurationUs);
|
||||||
|
// Note: If we ever want to support DRM protected subtitles then we'll need to output the
|
||||||
|
// appropriate encryption data here.
|
||||||
|
track.output.sampleData(subripSample, subripSample.limit());
|
||||||
|
sampleBytesWritten += subripSample.limit();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setSubripSampleEndTimecode(byte[] subripSampleData, long timeUs) {
|
||||||
|
byte[] timeCodeData;
|
||||||
|
if (timeUs == UNKNOWN) {
|
||||||
|
timeCodeData = SUBRIP_TIMECODE_EMPTY;
|
||||||
|
} else {
|
||||||
|
int hours = (int) (timeUs / 3600000000L);
|
||||||
|
timeUs -= (hours * 3600000000L);
|
||||||
|
int minutes = (int) (timeUs / 60000000);
|
||||||
|
timeUs -= (minutes * 60000000);
|
||||||
|
int seconds = (int) (timeUs / 1000000);
|
||||||
|
timeUs -= (seconds * 1000000);
|
||||||
|
int milliseconds = (int) (timeUs / 1000);
|
||||||
|
timeCodeData = String.format("%02d:%02d:%02d,%03d", hours, minutes, seconds, milliseconds)
|
||||||
|
.getBytes();
|
||||||
|
}
|
||||||
|
System.arraycopy(timeCodeData, 0, subripSampleData, SUBRIP_PREFIX_END_TIMECODE_OFFSET,
|
||||||
|
SUBRIP_TIMECODE_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes {@code length} bytes of sample data into {@code target} at {@code offset}, consisting of
|
* Writes {@code length} bytes of sample data into {@code target} at {@code offset}, consisting of
|
||||||
* pending {@link #sampleStrippedBytes} and any remaining data read from {@code input}.
|
* pending {@link #sampleStrippedBytes} and any remaining data read from {@code input}.
|
||||||
|
|
@ -948,7 +1033,8 @@ public final class WebmExtractor implements Extractor {
|
||||||
|| CODEC_ID_VORBIS.equals(codecId)
|
|| CODEC_ID_VORBIS.equals(codecId)
|
||||||
|| CODEC_ID_AAC.equals(codecId)
|
|| CODEC_ID_AAC.equals(codecId)
|
||||||
|| CODEC_ID_MP3.equals(codecId)
|
|| CODEC_ID_MP3.equals(codecId)
|
||||||
|| CODEC_ID_AC3.equals(codecId);
|
|| CODEC_ID_AC3.equals(codecId)
|
||||||
|
|| CODEC_ID_SUBRIP.equals(codecId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1032,6 +1118,9 @@ public final class WebmExtractor implements Extractor {
|
||||||
public long codecDelayNs = 0;
|
public long codecDelayNs = 0;
|
||||||
public long seekPreRollNs = 0;
|
public long seekPreRollNs = 0;
|
||||||
|
|
||||||
|
// Text elements.
|
||||||
|
private String language = "eng";
|
||||||
|
|
||||||
// Set when the output is initialized. nalUnitLengthFieldLength is only set for H264/H265.
|
// Set when the output is initialized. nalUnitLengthFieldLength is only set for H264/H265.
|
||||||
public TrackOutput output;
|
public TrackOutput output;
|
||||||
public int nalUnitLengthFieldLength;
|
public int nalUnitLengthFieldLength;
|
||||||
|
|
@ -1097,6 +1186,9 @@ public final class WebmExtractor implements Extractor {
|
||||||
case CODEC_ID_AC3:
|
case CODEC_ID_AC3:
|
||||||
mimeType = MimeTypes.AUDIO_AC3;
|
mimeType = MimeTypes.AUDIO_AC3;
|
||||||
break;
|
break;
|
||||||
|
case CODEC_ID_SUBRIP:
|
||||||
|
mimeType = MimeTypes.APPLICATION_SUBRIP;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new ParserException("Unrecognized codec identifier.");
|
throw new ParserException("Unrecognized codec identifier.");
|
||||||
}
|
}
|
||||||
|
|
@ -1108,6 +1200,8 @@ public final class WebmExtractor implements Extractor {
|
||||||
} else if (MimeTypes.isVideo(mimeType)) {
|
} else if (MimeTypes.isVideo(mimeType)) {
|
||||||
format = MediaFormat.createVideoFormat(mimeType, MediaFormat.NO_VALUE, maxInputSize,
|
format = MediaFormat.createVideoFormat(mimeType, MediaFormat.NO_VALUE, maxInputSize,
|
||||||
durationUs, width, height, 0, initializationData);
|
durationUs, width, height, 0, initializationData);
|
||||||
|
} else if (MimeTypes.APPLICATION_SUBRIP.equals(mimeType)) {
|
||||||
|
format = MediaFormat.createTextFormat(mimeType, MediaFormat.NO_VALUE, language, durationUs);
|
||||||
} else {
|
} else {
|
||||||
throw new ParserException("Unexpected MIME type.");
|
throw new ParserException("Unexpected MIME type.");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ import java.util.regex.Pattern;
|
||||||
*/
|
*/
|
||||||
public final class SubripParser implements SubtitleParser {
|
public final class SubripParser implements SubtitleParser {
|
||||||
|
|
||||||
private static final Pattern SUBRIP_TIMING_LINE = Pattern.compile("(.*)\\s+-->\\s+(.*)");
|
private static final Pattern SUBRIP_TIMING_LINE = Pattern.compile("(\\S*)\\s*-->\\s*(\\S*)");
|
||||||
private static final Pattern SUBRIP_TIMESTAMP =
|
private static final Pattern SUBRIP_TIMESTAMP =
|
||||||
Pattern.compile("(?:(\\d+):)?(\\d+):(\\d+),(\\d+)");
|
Pattern.compile("(?:(\\d+):)?(\\d+):(\\d+),(\\d+)");
|
||||||
|
|
||||||
|
|
@ -54,6 +54,7 @@ public final class SubripParser implements SubtitleParser {
|
||||||
ArrayList<Cue> cues = new ArrayList<>();
|
ArrayList<Cue> cues = new ArrayList<>();
|
||||||
LongArray cueTimesUs = new LongArray();
|
LongArray cueTimesUs = new LongArray();
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, C.UTF8_NAME));
|
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, C.UTF8_NAME));
|
||||||
|
boolean haveEndTimecode;
|
||||||
String currentLine;
|
String currentLine;
|
||||||
|
|
||||||
while ((currentLine = reader.readLine()) != null) {
|
while ((currentLine = reader.readLine()) != null) {
|
||||||
|
|
@ -65,11 +66,16 @@ public final class SubripParser implements SubtitleParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read and parse the timing line.
|
// Read and parse the timing line.
|
||||||
|
haveEndTimecode = false;
|
||||||
currentLine = reader.readLine();
|
currentLine = reader.readLine();
|
||||||
Matcher matcher = SUBRIP_TIMING_LINE.matcher(currentLine);
|
Matcher matcher = SUBRIP_TIMING_LINE.matcher(currentLine);
|
||||||
if (matcher.find()) {
|
if (matcher.find()) {
|
||||||
cueTimesUs.add(parseTimestampUs(matcher.group(1)));
|
cueTimesUs.add(parseTimecode(matcher.group(1)));
|
||||||
cueTimesUs.add(parseTimestampUs(matcher.group(2)));
|
String endTimecode = matcher.group(2);
|
||||||
|
if (!TextUtils.isEmpty(endTimecode)) {
|
||||||
|
haveEndTimecode = true;
|
||||||
|
cueTimesUs.add(parseTimecode(matcher.group(2)));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new ParserException("Expected timing line: " + currentLine);
|
throw new ParserException("Expected timing line: " + currentLine);
|
||||||
}
|
}
|
||||||
|
|
@ -85,6 +91,9 @@ public final class SubripParser implements SubtitleParser {
|
||||||
|
|
||||||
Spanned text = Html.fromHtml(textBuilder.toString());
|
Spanned text = Html.fromHtml(textBuilder.toString());
|
||||||
cues.add(new Cue(text));
|
cues.add(new Cue(text));
|
||||||
|
if (haveEndTimecode) {
|
||||||
|
cues.add(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Cue[] cuesArray = new Cue[cues.size()];
|
Cue[] cuesArray = new Cue[cues.size()];
|
||||||
|
|
@ -98,7 +107,7 @@ public final class SubripParser implements SubtitleParser {
|
||||||
return MimeTypes.APPLICATION_SUBRIP.equals(mimeType);
|
return MimeTypes.APPLICATION_SUBRIP.equals(mimeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static long parseTimestampUs(String s) throws NumberFormatException {
|
private static long parseTimecode(String s) throws NumberFormatException {
|
||||||
Matcher matcher = SUBRIP_TIMESTAMP.matcher(s);
|
Matcher matcher = SUBRIP_TIMESTAMP.matcher(s);
|
||||||
if (!matcher.matches()) {
|
if (!matcher.matches()) {
|
||||||
throw new NumberFormatException("has invalid format");
|
throw new NumberFormatException("has invalid format");
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,8 @@ import java.util.List;
|
||||||
private final long[] cueTimesUs;
|
private final long[] cueTimesUs;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param cues The cues in the subtitle.
|
* @param cues The cues in the subtitle. Null entries may be used to represent empty cues.
|
||||||
* @param cueTimesUs Interleaved cue start and end times, in microseconds.
|
* @param cueTimesUs The cue times, in microseconds.
|
||||||
*/
|
*/
|
||||||
public SubripSubtitle(Cue[] cues, long[] cueTimesUs) {
|
public SubripSubtitle(Cue[] cues, long[] cueTimesUs) {
|
||||||
this.cues = cues;
|
this.cues = cues;
|
||||||
|
|
@ -69,11 +69,11 @@ import java.util.List;
|
||||||
@Override
|
@Override
|
||||||
public List<Cue> getCues(long timeUs) {
|
public List<Cue> getCues(long timeUs) {
|
||||||
int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false);
|
int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false);
|
||||||
if (index == -1 || index % 2 == 1) {
|
if (index == -1 || cues[index] == null) {
|
||||||
// timeUs is earlier than the start of the first cue, or corresponds to a gap between cues.
|
// timeUs is earlier than the start of the first cue, or we have an empty cue.
|
||||||
return Collections.<Cue>emptyList();
|
return Collections.<Cue>emptyList();
|
||||||
} else {
|
} else {
|
||||||
return Collections.singletonList(cues[index / 2]);
|
return Collections.singletonList(cues[index]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue