Merge branch 'jcable-dev-v2' into dev-v2

This commit is contained in:
Oliver Woodman 2017-07-10 12:29:16 +01:00
commit 5ebbb6ef45
14 changed files with 579 additions and 46 deletions

View file

@ -0,0 +1,12 @@
[Script Info]
Title: SomeTitle
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,Open Sans Semibold,36,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,2,0,0,28,1
[Events]
Format: Layer, Start, End, Style, Name, Text
Dialogue: 0,Invalid,0:00:01.23,Default,Olly,This is the first subtitle{ignored}.
Dialogue: 0,0:00:02.34,Invalid,Default,Olly,This is the second subtitle \nwith a newline \Nand another.
Dialogue: 0,0:00:04:56,0:00:08:90,Default,Olly,This is the third subtitle, with a comma.

View file

@ -0,0 +1,12 @@
[Script Info]
Title: SomeTitle
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,Open Sans Semibold,36,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,2,0,0,28,1
[Events]
Format: Layer, Start, End, Style, Name, Text
Dialogue: 0,0:00:00.00, ,Default,Olly,This is the first subtitle.
Dialogue: 0,0:00:02.34, ,Default,Olly,This is the second subtitle \nwith a newline \Nand another.
Dialogue: 0,0:00:04.56, ,Default,Olly,This is the third subtitle, with a comma.

View file

@ -0,0 +1,12 @@
[Script Info]
Title: SomeTitle
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,Open Sans Semibold,36,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,2,0,0,28,1
[Events]
Format: Layer, Start, End, Style, Name, Text
Dialogue: 0,0:00:00.00,0:00:01.23,Default,Olly,This is the first subtitle{ignored}.
Dialogue: 0,0:00:02.34,0:00:03.45,Default,Olly,This is the second subtitle \nwith a newline \Nand another.
Dialogue: 0,0:00:04:56,0:00:08:90,Default,Olly,This is the third subtitle, with a comma.

View file

@ -0,0 +1,3 @@
Dialogue: 0,0:00:00.00,0:00:01.23,Default,Olly,This is the first subtitle{ignored}.
Dialogue: 0,0:00:02.34,0:00:03.45,Default,Olly,This is the second subtitle \nwith a newline \Nand another.
Dialogue: 0,0:00:04:56,0:00:08:90,Default,Olly,This is the third subtitle, with a comma.

View file

@ -0,0 +1 @@
Format: Layer, Start, End, Style, Name, Text

View file

@ -0,0 +1,6 @@
[Script Info]
Title: SomeTitle
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,Open Sans Semibold,36,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,2,0,0,28,1

View file

@ -0,0 +1,123 @@
/*
* Copyright (C) 2016 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.exoplayer2.text.ssa;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.IOException;
import java.util.ArrayList;
/**
* Unit test for {@link SsaDecoder}.
*/
public final class SsaDecoderTest extends InstrumentationTestCase {
private static final String EMPTY = "ssa/empty";
private static final String TYPICAL = "ssa/typical";
private static final String TYPICAL_HEADER_ONLY = "ssa/typical_header";
private static final String TYPICAL_DIALOGUE_ONLY = "ssa/typical_dialogue";
private static final String TYPICAL_FORMAT_ONLY = "ssa/typical_format";
private static final String INVALID_TIMECODES = "ssa/invalid_timecodes";
private static final String NO_END_TIMECODES = "ssa/no_end_timecodes";
public void testDecodeEmpty() throws IOException {
SsaDecoder decoder = new SsaDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), EMPTY);
SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
assertEquals(0, subtitle.getEventTimeCount());
assertTrue(subtitle.getCues(0).isEmpty());
}
public void testDecodeTypical() throws IOException {
SsaDecoder decoder = new SsaDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL);
SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
assertEquals(6, subtitle.getEventTimeCount());
assertTypicalCue1(subtitle, 0);
assertTypicalCue2(subtitle, 2);
assertTypicalCue3(subtitle, 4);
}
public void testDecodeTypicalWithInitializationData() throws IOException {
byte[] headerBytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_HEADER_ONLY);
byte[] formatBytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_FORMAT_ONLY);
ArrayList<byte[]> initializationData = new ArrayList<>();
initializationData.add(formatBytes);
initializationData.add(headerBytes);
SsaDecoder decoder = new SsaDecoder(initializationData);
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_DIALOGUE_ONLY);
SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
assertEquals(6, subtitle.getEventTimeCount());
assertTypicalCue1(subtitle, 0);
assertTypicalCue2(subtitle, 2);
assertTypicalCue3(subtitle, 4);
}
public void testDecodeInvalidTimecodes() throws IOException {
// Parsing should succeed, parsing the third cue only.
SsaDecoder decoder = new SsaDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), INVALID_TIMECODES);
SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
assertEquals(2, subtitle.getEventTimeCount());
assertTypicalCue3(subtitle, 0);
}
public void testDecodeNoEndTimecodes() throws IOException {
SsaDecoder decoder = new SsaDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), NO_END_TIMECODES);
SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
assertEquals(3, subtitle.getEventTimeCount());
assertEquals(0, subtitle.getEventTime(0));
assertEquals("This is the first subtitle.",
subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString());
assertEquals(2340000, subtitle.getEventTime(1));
assertEquals("This is the second subtitle \nwith a newline \nand another.",
subtitle.getCues(subtitle.getEventTime(1)).get(0).text.toString());
assertEquals(4560000, subtitle.getEventTime(2));
assertEquals("This is the third subtitle, with a comma.",
subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString());
}
private static void assertTypicalCue1(SsaSubtitle subtitle, int eventIndex) {
assertEquals(0, subtitle.getEventTime(eventIndex));
assertEquals("This is the first subtitle.",
subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString());
assertEquals(1230000, subtitle.getEventTime(eventIndex + 1));
}
private static void assertTypicalCue2(SsaSubtitle subtitle, int eventIndex) {
assertEquals(2340000, subtitle.getEventTime(eventIndex));
assertEquals("This is the second subtitle \nwith a newline \nand another.",
subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString());
assertEquals(3450000, subtitle.getEventTime(eventIndex + 1));
}
private static void assertTypicalCue3(SsaSubtitle subtitle, int eventIndex) {
assertEquals(4560000, subtitle.getEventTime(eventIndex));
assertEquals("This is the third subtitle, with a comma.",
subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString());
assertEquals(8900000, subtitle.getEventTime(eventIndex + 1));
}
}

View file

@ -37,7 +37,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase {
SubripDecoder decoder = new SubripDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), EMPTY_FILE);
SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
// Assert that the subtitle is empty.
assertEquals(0, subtitle.getEventTimeCount());
assertTrue(subtitle.getCues(0).isEmpty());
}
@ -46,6 +46,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase {
SubripDecoder decoder = new SubripDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_FILE);
SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
assertEquals(6, subtitle.getEventTimeCount());
assertTypicalCue1(subtitle, 0);
assertTypicalCue2(subtitle, 2);
@ -56,6 +57,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase {
SubripDecoder decoder = new SubripDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_WITH_BYTE_ORDER_MARK);
SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
assertEquals(6, subtitle.getEventTimeCount());
assertTypicalCue1(subtitle, 0);
assertTypicalCue2(subtitle, 2);
@ -66,6 +68,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase {
SubripDecoder decoder = new SubripDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_EXTRA_BLANK_LINE);
SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
assertEquals(6, subtitle.getEventTimeCount());
assertTypicalCue1(subtitle, 0);
assertTypicalCue2(subtitle, 2);
@ -77,6 +80,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase {
SubripDecoder decoder = new SubripDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_MISSING_TIMECODE);
SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
assertEquals(4, subtitle.getEventTimeCount());
assertTypicalCue1(subtitle, 0);
assertTypicalCue3(subtitle, 2);
@ -87,6 +91,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase {
SubripDecoder decoder = new SubripDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_MISSING_SEQUENCE);
SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
assertEquals(4, subtitle.getEventTimeCount());
assertTypicalCue1(subtitle, 0);
assertTypicalCue3(subtitle, 2);
@ -97,6 +102,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase {
SubripDecoder decoder = new SubripDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_NEGATIVE_TIMESTAMPS);
SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
assertEquals(2, subtitle.getEventTimeCount());
assertTypicalCue3(subtitle, 0);
}
@ -106,20 +112,16 @@ public final class SubripDecoderTest extends InstrumentationTestCase {
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), NO_END_TIMECODES_FILE);
SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false);
// 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());

View file

@ -120,6 +120,7 @@ public final class MatroskaExtractor implements Extractor {
private static final String CODEC_ID_ACM = "A_MS/ACM";
private static final String CODEC_ID_PCM_INT_LIT = "A_PCM/INT/LIT";
private static final String CODEC_ID_SUBRIP = "S_TEXT/UTF8";
private static final String CODEC_ID_ASS = "S_TEXT/ASS";
private static final String CODEC_ID_VOBSUB = "S_VOBSUB";
private static final String CODEC_ID_PGS = "S_HDMV/PGS";
private static final String CODEC_ID_DVBSUB = "S_DVBSUB";
@ -226,21 +227,62 @@ public final class MatroskaExtractor implements Extractor {
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.
* The byte offset of the end timecode in {@link #SUBRIP_PREFIX}.
*/
private static final int SUBRIP_PREFIX_END_TIMECODE_OFFSET = 19;
/**
* A special end timecode indicating that a subrip 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}.
* The value by which to divide a time in microseconds to convert it to the unit of the last value
* in a subrip timecode (milliseconds).
*/
private static final int SUBRIP_PREFIX_END_TIMECODE_OFFSET = 19;
private static long SUBRIP_TIMECODE_LAST_VALUE_SCALING_FACTOR = 1000;
/**
* The length in bytes of a timecode in a subrip prefix.
* The format of a subrip timecode.
*/
private static final int SUBRIP_TIMECODE_LENGTH = 12;
private static final String SUBRIP_TIMECODE_FORMAT = "%02d:%02d:%02d,%03d";
/**
* Matroska specific format line for SSA subtitles.
*/
private static final byte[] SSA_DIALOGUE_FORMAT = Util.getUtf8Bytes("Format: Start, End, "
+ "ReadOrder, Layer, Style, Name, MarginL, MarginR, MarginV, Effect, Text");
/**
* A template for the prefix that must be added to each SSA sample. The 10 byte end timecode
* starting at {@link #SSA_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: "Dialogue: 0:00:00:00,0:00:00:00,".
*/
private static final byte[] SSA_PREFIX = new byte[] {68, 105, 97, 108, 111, 103, 117, 101, 58, 32,
48, 58, 48, 48, 58, 48, 48, 58, 48, 48, 44, 48, 58, 48, 48, 58, 48, 48, 58, 48, 48, 44};
/**
* The byte offset of the end timecode in {@link #SSA_PREFIX}.
*/
private static final int SSA_PREFIX_END_TIMECODE_OFFSET = 21;
/**
* The value by which to divide a time in microseconds to convert it to the unit of the last value
* in an SSA timecode (1/100ths of a second).
*/
private static long SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR = 10000;
/**
* A special end timecode indicating that an SSA 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[] SSA_TIMECODE_EMPTY =
new byte[] {32, 32, 32, 32, 32, 32, 32, 32, 32, 32};
/**
* The format of an SSA timecode.
*/
private static final String SSA_TIMECODE_FORMAT = "%01d:%02d:%02d:%02d";
/**
* The length in bytes of a WAVEFORMATEX structure.
@ -271,7 +313,7 @@ public final class MatroskaExtractor implements Extractor {
private final ParsableByteArray vorbisNumPageSamples;
private final ParsableByteArray seekEntryIdBytes;
private final ParsableByteArray sampleStrippedBytes;
private final ParsableByteArray subripSample;
private final ParsableByteArray subtitleSample;
private final ParsableByteArray encryptionInitializationVector;
private final ParsableByteArray encryptionSubsampleData;
private ByteBuffer encryptionSubsampleDataBuffer;
@ -349,7 +391,7 @@ public final class MatroskaExtractor implements Extractor {
nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
nalLength = new ParsableByteArray(4);
sampleStrippedBytes = new ParsableByteArray();
subripSample = new ParsableByteArray();
subtitleSample = new ParsableByteArray();
encryptionInitializationVector = new ParsableByteArray(ENCRYPTION_IV_SIZE);
encryptionSubsampleData = new ParsableByteArray();
}
@ -1016,7 +1058,7 @@ public final class MatroskaExtractor implements Extractor {
// For SimpleBlock, we have metadata for each sample here.
while (blockLacingSampleIndex < blockLacingSampleCount) {
writeSampleData(input, track, blockLacingSampleSizes[blockLacingSampleIndex]);
long sampleTimeUs = this.blockTimeUs
long sampleTimeUs = blockTimeUs
+ (blockLacingSampleIndex * track.defaultSampleDurationNs) / 1000;
commitSampleToOutput(track, sampleTimeUs);
blockLacingSampleIndex++;
@ -1036,7 +1078,11 @@ public final class MatroskaExtractor implements Extractor {
private void commitSampleToOutput(Track track, long timeUs) {
if (CODEC_ID_SUBRIP.equals(track.codecId)) {
writeSubripSample(track);
commitSubtitleSample(track, SUBRIP_TIMECODE_FORMAT, SUBRIP_PREFIX_END_TIMECODE_OFFSET,
SUBRIP_TIMECODE_LAST_VALUE_SCALING_FACTOR, SUBRIP_TIMECODE_EMPTY);
} else if (CODEC_ID_ASS.equals(track.codecId)) {
commitSubtitleSample(track, SSA_TIMECODE_FORMAT, SSA_PREFIX_END_TIMECODE_OFFSET,
SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR, SSA_TIMECODE_EMPTY);
}
track.output.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, track.cryptoData);
sampleRead = true;
@ -1076,17 +1122,10 @@ public final class MatroskaExtractor implements Extractor {
private void writeSampleData(ExtractorInput input, Track track, int size)
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.
writeSubtitleSampleData(input, SUBRIP_PREFIX, size);
return;
} else if (CODEC_ID_ASS.equals(track.codecId)) {
writeSubtitleSampleData(input, SSA_PREFIX, size);
return;
}
@ -1230,31 +1269,50 @@ public final class MatroskaExtractor 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 void writeSubtitleSampleData(ExtractorInput input, byte[] samplePrefix, int size)
throws IOException, InterruptedException {
int sizeWithPrefix = samplePrefix.length + size;
if (subtitleSample.capacity() < sizeWithPrefix) {
// Initialize subripSample to contain the required prefix and have space to hold a subtitle
// twice as long as this one.
subtitleSample.data = Arrays.copyOf(samplePrefix, sizeWithPrefix + size);
} else {
System.arraycopy(samplePrefix, 0, subtitleSample.data, 0, samplePrefix.length);
}
input.readFully(subtitleSample.data, samplePrefix.length, size);
subtitleSample.reset(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.
}
private static void setSubripSampleEndTimecode(byte[] subripSampleData, long timeUs) {
private void commitSubtitleSample(Track track, String timecodeFormat, int endTimecodeOffset,
long lastTimecodeValueScalingFactor, byte[] emptyTimecode) {
setSampleDuration(subtitleSample.data, blockDurationUs, timecodeFormat, endTimecodeOffset,
lastTimecodeValueScalingFactor, emptyTimecode);
// Note: If we ever want to support DRM protected subtitles then we'll need to output the
// appropriate encryption data here.
track.output.sampleData(subtitleSample, subtitleSample.limit());
sampleBytesWritten += subtitleSample.limit();
}
private static void setSampleDuration(byte[] subripSampleData, long durationUs,
String timecodeFormat, int endTimecodeOffset, long lastTimecodeValueScalingFactor,
byte[] emptyTimecode) {
byte[] timeCodeData;
if (timeUs == C.TIME_UNSET) {
timeCodeData = SUBRIP_TIMECODE_EMPTY;
if (durationUs == C.TIME_UNSET) {
timeCodeData = emptyTimecode;
} 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 = Util.getUtf8Bytes(String.format(Locale.US, "%02d:%02d:%02d,%03d", hours,
minutes, seconds, milliseconds));
int hours = (int) (durationUs / (3600 * C.MICROS_PER_SECOND));
durationUs -= (hours * 3600 * C.MICROS_PER_SECOND);
int minutes = (int) (durationUs / (60 * C.MICROS_PER_SECOND));
durationUs -= (minutes * 60 * C.MICROS_PER_SECOND);
int seconds = (int) (durationUs / C.MICROS_PER_SECOND);
durationUs -= (seconds * C.MICROS_PER_SECOND);
int lastValue = (int) (durationUs / lastTimecodeValueScalingFactor);
timeCodeData = Util.getUtf8Bytes(String.format(Locale.US, timecodeFormat, hours, minutes,
seconds, lastValue));
}
System.arraycopy(timeCodeData, 0, subripSampleData, SUBRIP_PREFIX_END_TIMECODE_OFFSET,
SUBRIP_TIMECODE_LENGTH);
System.arraycopy(timeCodeData, 0, subripSampleData, endTimecodeOffset, emptyTimecode.length);
}
/**
@ -1385,6 +1443,7 @@ public final class MatroskaExtractor implements Extractor {
|| CODEC_ID_ACM.equals(codecId)
|| CODEC_ID_PCM_INT_LIT.equals(codecId)
|| CODEC_ID_SUBRIP.equals(codecId)
|| CODEC_ID_ASS.equals(codecId)
|| CODEC_ID_VOBSUB.equals(codecId)
|| CODEC_ID_PGS.equals(codecId)
|| CODEC_ID_DVBSUB.equals(codecId);
@ -1650,6 +1709,9 @@ public final class MatroskaExtractor implements Extractor {
case CODEC_ID_SUBRIP:
mimeType = MimeTypes.APPLICATION_SUBRIP;
break;
case CODEC_ID_ASS:
mimeType = MimeTypes.TEXT_SSA;
break;
case CODEC_ID_VOBSUB:
mimeType = MimeTypes.APPLICATION_VOBSUB;
initializationData = Collections.singletonList(codecPrivate);
@ -1702,6 +1764,14 @@ public final class MatroskaExtractor implements Extractor {
type = C.TRACK_TYPE_TEXT;
format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null,
Format.NO_VALUE, selectionFlags, language, drmInitData);
} else if (MimeTypes.TEXT_SSA.equals(mimeType)) {
type = C.TRACK_TYPE_TEXT;
initializationData = new ArrayList<>(2);
initializationData.add(SSA_DIALOGUE_FORMAT);
initializationData.add(codecPrivate);
format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null,
Format.NO_VALUE, selectionFlags, language, Format.NO_VALUE, drmInitData,
Format.OFFSET_SAMPLE_RELATIVE, initializationData);
} else if (MimeTypes.APPLICATION_VOBSUB.equals(mimeType)
|| MimeTypes.APPLICATION_PGS.equals(mimeType)
|| MimeTypes.APPLICATION_DVBSUBS.equals(mimeType)) {

View file

@ -19,6 +19,7 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.text.cea.Cea608Decoder;
import com.google.android.exoplayer2.text.cea.Cea708Decoder;
import com.google.android.exoplayer2.text.dvb.DvbDecoder;
import com.google.android.exoplayer2.text.ssa.SsaDecoder;
import com.google.android.exoplayer2.text.subrip.SubripDecoder;
import com.google.android.exoplayer2.text.ttml.TtmlDecoder;
import com.google.android.exoplayer2.text.tx3g.Tx3gDecoder;
@ -58,6 +59,7 @@ public interface SubtitleDecoderFactory {
* <li>WebVTT (MP4) ({@link Mp4WebvttDecoder})</li>
* <li>TTML ({@link TtmlDecoder})</li>
* <li>SubRip ({@link SubripDecoder})</li>
* <li>SSA/ASS ({@link SsaDecoder})</li>
* <li>TX3G ({@link Tx3gDecoder})</li>
* <li>Cea608 ({@link Cea608Decoder})</li>
* <li>Cea708 ({@link Cea708Decoder})</li>
@ -70,6 +72,7 @@ public interface SubtitleDecoderFactory {
public boolean supportsFormat(Format format) {
String mimeType = format.sampleMimeType;
return MimeTypes.TEXT_VTT.equals(mimeType)
|| MimeTypes.TEXT_SSA.equals(mimeType)
|| MimeTypes.APPLICATION_TTML.equals(mimeType)
|| MimeTypes.APPLICATION_MP4VTT.equals(mimeType)
|| MimeTypes.APPLICATION_SUBRIP.equals(mimeType)
@ -85,6 +88,8 @@ public interface SubtitleDecoderFactory {
switch (format.sampleMimeType) {
case MimeTypes.TEXT_VTT:
return new WebvttDecoder();
case MimeTypes.TEXT_SSA:
return new SsaDecoder(format.initializationData);
case MimeTypes.APPLICATION_MP4VTT:
return new Mp4WebvttDecoder();
case MimeTypes.APPLICATION_TTML:

View file

@ -0,0 +1,214 @@
/*
* Copyright (C) 2017 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.exoplayer2.text.ssa;
import android.text.TextUtils;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.LongArray;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A {@link SimpleSubtitleDecoder} for SSA/ASS.
*/
public final class SsaDecoder extends SimpleSubtitleDecoder {
private static final String TAG = "SsaDecoder";
private static final Pattern SSA_TIMECODE_PATTERN = Pattern.compile(
"(?:(\\d+):)?(\\d+):(\\d+)(?::|\\.)(\\d+)");
private static final String FORMAT_LINE_PREFIX = "Format: ";
private static final String DIALOGUE_LINE_PREFIX = "Dialogue: ";
private final boolean haveInitializationData;
private int formatKeyCount;
private int formatStartIndex;
private int formatEndIndex;
private int formatTextIndex;
public SsaDecoder() {
this(null);
}
/**
* @param initializationData Optional initialization data for the decoder. If not null, the
* initialization data must consist of two byte arrays. The first must contain an SSA format
* line. The second must contain an SSA header that will be assumed common to all samples.
*/
public SsaDecoder(List<byte[]> initializationData) {
super("SsaDecoder");
if (initializationData != null) {
haveInitializationData = true;
String formatLine = new String(initializationData.get(0));
Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX));
parseFormatLine(formatLine);
parseHeader(new ParsableByteArray(initializationData.get(1)));
} else {
haveInitializationData = false;
}
}
@Override
protected SsaSubtitle decode(byte[] bytes, int length, boolean reset) {
ArrayList<Cue> cues = new ArrayList<>();
LongArray cueTimesUs = new LongArray();
ParsableByteArray data = new ParsableByteArray(bytes, length);
if (!haveInitializationData) {
parseHeader(data);
}
parseEventBody(data, cues, cueTimesUs);
Cue[] cuesArray = new Cue[cues.size()];
cues.toArray(cuesArray);
long[] cueTimesUsArray = cueTimesUs.toArray();
return new SsaSubtitle(cuesArray, cueTimesUsArray);
}
/**
* Parses the header of the subtitle.
*
* @param data A {@link ParsableByteArray} from which the header should be read.
*/
private void parseHeader(ParsableByteArray data) {
String currentLine;
while ((currentLine = data.readLine()) != null) {
// TODO: Parse useful data from the header.
if (currentLine.startsWith("[Events]")) {
// We've reached the event body.
return;
}
}
}
/**
* Parses the event body of the subtitle.
*
* @param data A {@link ParsableByteArray} from which the body should be read.
* @param cues A list to which parsed cues will be added.
* @param cueTimesUs An array to which parsed cue timestamps will be added.
*/
private void parseEventBody(ParsableByteArray data, List<Cue> cues, LongArray cueTimesUs) {
String currentLine;
while ((currentLine = data.readLine()) != null) {
if (!haveInitializationData && currentLine.startsWith(FORMAT_LINE_PREFIX)) {
parseFormatLine(currentLine);
} else if (currentLine.startsWith(DIALOGUE_LINE_PREFIX)) {
parseDialogueLine(currentLine, cues, cueTimesUs);
}
}
}
/**
* Parses a format line.
*
* @param formatLine The line to parse.
*/
private void parseFormatLine(String formatLine) {
String[] values = TextUtils.split(formatLine.substring(FORMAT_LINE_PREFIX.length()), ",");
formatKeyCount = values.length;
formatStartIndex = C.INDEX_UNSET;
formatEndIndex = C.INDEX_UNSET;
formatTextIndex = C.INDEX_UNSET;
for (int i = 0; i < formatKeyCount; i++) {
String key = values[i].trim().toLowerCase();
switch (key) {
case "start":
formatStartIndex = i;
break;
case "end":
formatEndIndex = i;
break;
case "text":
formatTextIndex = i;
break;
default:
// Do nothing.
break;
}
}
}
/**
* Parses a dialogue line.
*
* @param dialogueLine The line to parse.
* @param cues A list to which parsed cues will be added.
* @param cueTimesUs An array to which parsed cue timestamps will be added.
*/
private void parseDialogueLine(String dialogueLine, List<Cue> cues, LongArray cueTimesUs) {
if (formatKeyCount == 0) {
Log.w(TAG, "Skipping dialogue line before format: " + dialogueLine);
return;
}
String[] lineValues = dialogueLine.substring(DIALOGUE_LINE_PREFIX.length())
.split(",", formatKeyCount);
long startTimeUs = SsaDecoder.parseTimecodeUs(lineValues[formatStartIndex]);
if (startTimeUs == C.TIME_UNSET) {
Log.w(TAG, "Skipping invalid timing: " + dialogueLine);
return;
}
long endTimeUs = C.TIME_UNSET;
String endTimeString = lineValues[formatEndIndex];
if (!endTimeString.trim().isEmpty()) {
endTimeUs = SsaDecoder.parseTimecodeUs(endTimeString);
if (endTimeUs == C.TIME_UNSET) {
Log.w(TAG, "Skipping invalid timing: " + dialogueLine);
return;
}
}
String text = lineValues[formatTextIndex]
.replaceAll("\\{.*?\\}", "")
.replaceAll("\\\\N", "\n")
.replaceAll("\\\\n", "\n");
cues.add(new Cue(text));
cueTimesUs.add(startTimeUs);
if (endTimeUs != C.TIME_UNSET) {
cues.add(null);
cueTimesUs.add(endTimeUs);
}
}
/**
* Parses an SSA timecode string.
*
* @param timeString The string to parse.
* @return The parsed timestamp in microseconds.
*/
public static long parseTimecodeUs(String timeString) {
Matcher matcher = SSA_TIMECODE_PATTERN.matcher(timeString);
if (!matcher.matches()) {
return C.TIME_UNSET;
}
long timestampUs = Long.parseLong(matcher.group(1)) * 60 * 60 * C.MICROS_PER_SECOND;
timestampUs += Long.parseLong(matcher.group(2)) * 60 * C.MICROS_PER_SECOND;
timestampUs += Long.parseLong(matcher.group(3)) * C.MICROS_PER_SECOND;
timestampUs += Long.parseLong(matcher.group(4)) * 10000; // 100ths of a second.
return timestampUs;
}
}

View file

@ -0,0 +1,72 @@
/*
* Copyright (C) 2017 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.exoplayer2.text.ssa;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.Subtitle;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.util.Collections;
import java.util.List;
/**
* A representation of an SSA/ASS subtitle.
*/
/* package */ final class SsaSubtitle implements Subtitle {
private final Cue[] cues;
private final long[] cueTimesUs;
/**
* @param cues The cues in the subtitle. Null entries may be used to represent empty cues.
* @param cueTimesUs The cue times, in microseconds.
*/
public SsaSubtitle(Cue[] cues, long[] cueTimesUs) {
this.cues = cues;
this.cueTimesUs = cueTimesUs;
}
@Override
public int getNextEventTimeIndex(long timeUs) {
int index = Util.binarySearchCeil(cueTimesUs, timeUs, false, false);
return index < cueTimesUs.length ? index : C.INDEX_UNSET;
}
@Override
public int getEventTimeCount() {
return cueTimesUs.length;
}
@Override
public long getEventTime(int index) {
Assertions.checkArgument(index >= 0);
Assertions.checkArgument(index < cueTimesUs.length);
return cueTimesUs[index];
}
@Override
public List<Cue> getCues(long timeUs) {
int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false);
if (index == -1 || cues[index] == null) {
// timeUs is earlier than the start of the first cue, or we have an empty cue.
return Collections.emptyList();
} else {
return Collections.singletonList(cues[index]);
}
}
}

View file

@ -65,6 +65,7 @@ public final class MimeTypes {
public static final String AUDIO_UNKNOWN = BASE_TYPE_AUDIO + "/x-unknown";
public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt";
public static final String TEXT_SSA = BASE_TYPE_TEXT + "/x-ssa";
public static final String APPLICATION_MP4 = BASE_TYPE_APPLICATION + "/mp4";
public static final String APPLICATION_WEBM = BASE_TYPE_APPLICATION + "/webm";