mirror of
https://github.com/androidx/media.git
synced 2025-05-16 20:19:57 +08:00
Support default style in Tx3g decoder.
The track initialization data of Tx3g includes default style values for font styles, colour, and font family. Additionally the decoder now supports vertical subtitle placements other than the Tx3g default of 85% video height. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=152930057
This commit is contained in:
parent
1a22a4be5f
commit
0a7011b390
BIN
library/core/src/androidTest/assets/tx3g/initialization
Normal file
BIN
library/core/src/androidTest/assets/tx3g/initialization
Normal file
Binary file not shown.
Binary file not shown.
BIN
library/core/src/androidTest/assets/tx3g/sample_just_text
Normal file
BIN
library/core/src/androidTest/assets/tx3g/sample_just_text
Normal file
Binary file not shown.
Binary file not shown.
BIN
library/core/src/androidTest/assets/tx3g/sample_with_tbox
Normal file
BIN
library/core/src/androidTest/assets/tx3g/sample_with_tbox
Normal file
Binary file not shown.
@ -21,11 +21,15 @@ import android.test.InstrumentationTestCase;
|
||||
import android.text.SpannedString;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.text.style.TypefaceSpan;
|
||||
import android.text.style.UnderlineSpan;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.text.Cue;
|
||||
import com.google.android.exoplayer2.text.Subtitle;
|
||||
import com.google.android.exoplayer2.text.SubtitleDecoderException;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* Unit test for {@link Tx3gDecoder}.
|
||||
@ -33,21 +37,36 @@ import java.io.IOException;
|
||||
public final class Tx3gDecoderTest extends InstrumentationTestCase {
|
||||
|
||||
private static final String NO_SUBTITLE = "tx3g/no_subtitle";
|
||||
private static final String SAMPLE_JUST_TEXT = "tx3g/sample_just_text";
|
||||
private static final String SAMPLE_WITH_STYL = "tx3g/sample_with_styl";
|
||||
private static final String SAMPLE_WITH_STYL_ALL_DEFAULTS = "tx3g/sample_with_styl_all_defaults";
|
||||
private static final String SAMPLE_UTF16_BE_NO_STYL = "tx3g/sample_utf16_be_no_styl";
|
||||
private static final String SAMPLE_UTF16_LE_NO_STYL = "tx3g/sample_utf16_le_no_styl";
|
||||
private static final String SAMPLE_WITH_MULTIPLE_STYL = "tx3g/sample_with_multiple_styl";
|
||||
private static final String SAMPLE_WITH_OTHER_EXTENSION = "tx3g/sample_with_other_extension";
|
||||
private static final String SAMPLE_WITH_TBOX = "tx3g/sample_with_tbox";
|
||||
private static final String INITIALIZATION = "tx3g/initialization";
|
||||
private static final String INITIALIZATION_ALL_DEFAULTS = "tx3g/initialization_all_defaults";
|
||||
|
||||
public void testDecodeNoSubtitle() throws IOException, SubtitleDecoderException {
|
||||
Tx3gDecoder decoder = new Tx3gDecoder();
|
||||
Tx3gDecoder decoder = new Tx3gDecoder(Collections.<byte[]>emptyList());
|
||||
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), NO_SUBTITLE);
|
||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||
assertTrue(subtitle.getCues(0).isEmpty());
|
||||
}
|
||||
|
||||
public void testDecodeJustText() throws IOException, SubtitleDecoderException {
|
||||
Tx3gDecoder decoder = new Tx3gDecoder(Collections.<byte[]>emptyList());
|
||||
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), SAMPLE_JUST_TEXT);
|
||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
||||
assertEquals("CC Test", text.toString());
|
||||
assertEquals(0, text.getSpans(0, text.length(), Object.class).length);
|
||||
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);
|
||||
}
|
||||
|
||||
public void testDecodeWithStyl() throws IOException, SubtitleDecoderException {
|
||||
Tx3gDecoder decoder = new Tx3gDecoder();
|
||||
Tx3gDecoder decoder = new Tx3gDecoder(Collections.<byte[]>emptyList());
|
||||
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), SAMPLE_WITH_STYL);
|
||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
||||
@ -58,28 +77,41 @@ public final class Tx3gDecoderTest extends InstrumentationTestCase {
|
||||
findSpan(text, 0, 6, UnderlineSpan.class);
|
||||
ForegroundColorSpan colorSpan = findSpan(text, 0, 6, ForegroundColorSpan.class);
|
||||
assertEquals(Color.GREEN, colorSpan.getForegroundColor());
|
||||
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);
|
||||
}
|
||||
|
||||
public void testDecodeWithStylAllDefaults() throws IOException, SubtitleDecoderException {
|
||||
Tx3gDecoder decoder = new Tx3gDecoder(Collections.<byte[]>emptyList());
|
||||
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), SAMPLE_WITH_STYL_ALL_DEFAULTS);
|
||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
||||
assertEquals("CC Test", text.toString());
|
||||
assertEquals(0, text.getSpans(0, text.length(), Object.class).length);
|
||||
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);
|
||||
}
|
||||
|
||||
public void testDecodeUtf16BeNoStyl() throws IOException, SubtitleDecoderException {
|
||||
Tx3gDecoder decoder = new Tx3gDecoder();
|
||||
Tx3gDecoder decoder = new Tx3gDecoder(Collections.<byte[]>emptyList());
|
||||
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), SAMPLE_UTF16_BE_NO_STYL);
|
||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
||||
assertEquals("你好", text.toString());
|
||||
assertEquals(0, text.getSpans(0, text.length(), Object.class).length);
|
||||
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);
|
||||
}
|
||||
|
||||
public void testDecodeUtf16LeNoStyl() throws IOException, SubtitleDecoderException {
|
||||
Tx3gDecoder decoder = new Tx3gDecoder();
|
||||
Tx3gDecoder decoder = new Tx3gDecoder(Collections.<byte[]>emptyList());
|
||||
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), SAMPLE_UTF16_LE_NO_STYL);
|
||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
||||
assertEquals("你好", text.toString());
|
||||
assertEquals(0, text.getSpans(0, text.length(), Object.class).length);
|
||||
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);
|
||||
}
|
||||
|
||||
public void testDecodeWithMultipleStyl() throws IOException, SubtitleDecoderException {
|
||||
Tx3gDecoder decoder = new Tx3gDecoder();
|
||||
Tx3gDecoder decoder = new Tx3gDecoder(Collections.<byte[]>emptyList());
|
||||
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), SAMPLE_WITH_MULTIPLE_STYL);
|
||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
||||
@ -92,10 +124,11 @@ public final class Tx3gDecoderTest extends InstrumentationTestCase {
|
||||
assertEquals(Color.GREEN, colorSpan.getForegroundColor());
|
||||
colorSpan = findSpan(text, 7, 12, ForegroundColorSpan.class);
|
||||
assertEquals(Color.GREEN, colorSpan.getForegroundColor());
|
||||
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);
|
||||
}
|
||||
|
||||
public void testDecodeWithOtherExtension() throws IOException, SubtitleDecoderException {
|
||||
Tx3gDecoder decoder = new Tx3gDecoder();
|
||||
Tx3gDecoder decoder = new Tx3gDecoder(Collections.<byte[]>emptyList());
|
||||
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), SAMPLE_WITH_OTHER_EXTENSION);
|
||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
||||
@ -105,6 +138,62 @@ public final class Tx3gDecoderTest extends InstrumentationTestCase {
|
||||
assertEquals(Typeface.BOLD, styleSpan.getStyle());
|
||||
ForegroundColorSpan colorSpan = findSpan(text, 0, 6, ForegroundColorSpan.class);
|
||||
assertEquals(Color.GREEN, colorSpan.getForegroundColor());
|
||||
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);
|
||||
}
|
||||
|
||||
public void testInitializationDecodeWithStyl() throws IOException, SubtitleDecoderException {
|
||||
byte[] initBytes = TestUtil.getByteArray(getInstrumentation(), INITIALIZATION);
|
||||
Tx3gDecoder decoder = new Tx3gDecoder(Collections.singletonList(initBytes));
|
||||
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), SAMPLE_WITH_STYL);
|
||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
||||
assertEquals("CC Test", text.toString());
|
||||
assertEquals(5, text.getSpans(0, text.length(), Object.class).length);
|
||||
StyleSpan styleSpan = findSpan(text, 0, text.length(), StyleSpan.class);
|
||||
assertEquals(Typeface.BOLD_ITALIC, styleSpan.getStyle());
|
||||
findSpan(text, 0, text.length(), UnderlineSpan.class);
|
||||
TypefaceSpan typefaceSpan = findSpan(text, 0, text.length(), TypefaceSpan.class);
|
||||
assertEquals(C.SERIF_NAME, typefaceSpan.getFamily());
|
||||
ForegroundColorSpan colorSpan = findSpan(text, 0, text.length(), ForegroundColorSpan.class);
|
||||
assertEquals(Color.RED, colorSpan.getForegroundColor());
|
||||
colorSpan = findSpan(text, 0, 6, ForegroundColorSpan.class);
|
||||
assertEquals(Color.GREEN, colorSpan.getForegroundColor());
|
||||
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.1f);
|
||||
}
|
||||
|
||||
public void testInitializationDecodeWithTbox() throws IOException, SubtitleDecoderException {
|
||||
byte[] initBytes = TestUtil.getByteArray(getInstrumentation(), INITIALIZATION);
|
||||
Tx3gDecoder decoder = new Tx3gDecoder(Collections.singletonList(initBytes));
|
||||
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), SAMPLE_WITH_TBOX);
|
||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
||||
assertEquals("CC Test", text.toString());
|
||||
assertEquals(4, text.getSpans(0, text.length(), Object.class).length);
|
||||
StyleSpan styleSpan = findSpan(text, 0, text.length(), StyleSpan.class);
|
||||
assertEquals(Typeface.BOLD_ITALIC, styleSpan.getStyle());
|
||||
findSpan(text, 0, text.length(), UnderlineSpan.class);
|
||||
TypefaceSpan typefaceSpan = findSpan(text, 0, text.length(), TypefaceSpan.class);
|
||||
assertEquals(C.SERIF_NAME, typefaceSpan.getFamily());
|
||||
ForegroundColorSpan colorSpan = findSpan(text, 0, text.length(), ForegroundColorSpan.class);
|
||||
assertEquals(Color.RED, colorSpan.getForegroundColor());
|
||||
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.1875f);
|
||||
}
|
||||
|
||||
public void testInitializationAllDefaultsDecodeWithStyl() throws IOException,
|
||||
SubtitleDecoderException {
|
||||
byte[] initBytes = TestUtil.getByteArray(getInstrumentation(), INITIALIZATION_ALL_DEFAULTS);
|
||||
Tx3gDecoder decoder = new Tx3gDecoder(Collections.singletonList(initBytes));
|
||||
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), SAMPLE_WITH_STYL);
|
||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
||||
assertEquals("CC Test", text.toString());
|
||||
assertEquals(3, text.getSpans(0, text.length(), Object.class).length);
|
||||
StyleSpan styleSpan = findSpan(text, 0, 6, StyleSpan.class);
|
||||
assertEquals(Typeface.BOLD_ITALIC, styleSpan.getStyle());
|
||||
findSpan(text, 0, 6, UnderlineSpan.class);
|
||||
ForegroundColorSpan colorSpan = findSpan(text, 0, 6, ForegroundColorSpan.class);
|
||||
assertEquals(Color.GREEN, colorSpan.getForegroundColor());
|
||||
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);
|
||||
}
|
||||
|
||||
private static <T> T findSpan(SpannedString testObject, int expectedStart, int expectedEnd,
|
||||
@ -120,4 +209,10 @@ public final class Tx3gDecoderTest extends InstrumentationTestCase {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void assertFractionalLinePosition(Cue cue, float expectedFraction) {
|
||||
assertEquals(Cue.LINE_TYPE_FRACTION, cue.lineType);
|
||||
assertEquals(Cue.ANCHOR_TYPE_START, cue.lineAnchor);
|
||||
assertTrue(Math.abs(expectedFraction - cue.line) < 1e-6);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -81,6 +81,16 @@ public final class C {
|
||||
*/
|
||||
public static final String UTF16_NAME = "UTF-16";
|
||||
|
||||
/**
|
||||
* * The name of the serif font family.
|
||||
*/
|
||||
public static final String SERIF_NAME = "serif";
|
||||
|
||||
/**
|
||||
* * The name of the sans-serif font family.
|
||||
*/
|
||||
public static final String SANS_SERIF_NAME = "sans-serif";
|
||||
|
||||
/**
|
||||
* Crypto modes for a codec.
|
||||
*/
|
||||
|
@ -283,30 +283,31 @@ public final class Format implements Parcelable {
|
||||
public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
|
||||
int bitrate, @C.SelectionFlags int selectionFlags, String language, DrmInitData drmInitData) {
|
||||
return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language,
|
||||
NO_VALUE, drmInitData, OFFSET_SAMPLE_RELATIVE);
|
||||
NO_VALUE, drmInitData, OFFSET_SAMPLE_RELATIVE, Collections.<byte[]>emptyList());
|
||||
}
|
||||
|
||||
public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
|
||||
int bitrate, @C.SelectionFlags int selectionFlags, String language, int accessibilityChannel,
|
||||
DrmInitData drmInitData) {
|
||||
return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language,
|
||||
accessibilityChannel, drmInitData, OFFSET_SAMPLE_RELATIVE);
|
||||
accessibilityChannel, drmInitData, OFFSET_SAMPLE_RELATIVE, Collections.<byte[]>emptyList());
|
||||
}
|
||||
|
||||
public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
|
||||
int bitrate, @C.SelectionFlags int selectionFlags, String language, DrmInitData drmInitData,
|
||||
long subsampleOffsetUs) {
|
||||
return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language,
|
||||
NO_VALUE, drmInitData, subsampleOffsetUs);
|
||||
NO_VALUE, drmInitData, subsampleOffsetUs, Collections.<byte[]>emptyList());
|
||||
}
|
||||
|
||||
public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
|
||||
int bitrate, @C.SelectionFlags int selectionFlags, String language,
|
||||
int accessibilityChannel, DrmInitData drmInitData, long subsampleOffsetUs) {
|
||||
int accessibilityChannel, DrmInitData drmInitData, long subsampleOffsetUs,
|
||||
List<byte[]> initializationData) {
|
||||
return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE,
|
||||
NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
|
||||
NO_VALUE, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, null,
|
||||
drmInitData, null);
|
||||
NO_VALUE, selectionFlags, language, accessibilityChannel, subsampleOffsetUs,
|
||||
initializationData, drmInitData, null);
|
||||
}
|
||||
|
||||
// Image.
|
||||
@ -438,6 +439,7 @@ public final class Format implements Parcelable {
|
||||
drmInitData, metadata);
|
||||
}
|
||||
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
public Format copyWithManifestFormatInfo(Format manifestFormat) {
|
||||
if (this == manifestFormat) {
|
||||
// No need to copy from ourselves.
|
||||
|
@ -611,24 +611,11 @@ import java.util.List;
|
||||
|| childAtomType == Atom.TYPE__mp3 || childAtomType == Atom.TYPE_alac) {
|
||||
parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId,
|
||||
language, isQuickTime, drmInitData, out, i);
|
||||
} else if (childAtomType == Atom.TYPE_TTML) {
|
||||
out.format = Format.createTextSampleFormat(Integer.toString(trackId),
|
||||
MimeTypes.APPLICATION_TTML, null, Format.NO_VALUE, 0, language, drmInitData);
|
||||
} else if (childAtomType == Atom.TYPE_tx3g) {
|
||||
out.format = Format.createTextSampleFormat(Integer.toString(trackId),
|
||||
MimeTypes.APPLICATION_TX3G, null, Format.NO_VALUE, 0, language, drmInitData);
|
||||
} else if (childAtomType == Atom.TYPE_wvtt) {
|
||||
out.format = Format.createTextSampleFormat(Integer.toString(trackId),
|
||||
MimeTypes.APPLICATION_MP4VTT, null, Format.NO_VALUE, 0, language, drmInitData);
|
||||
} else if (childAtomType == Atom.TYPE_stpp) {
|
||||
out.format = Format.createTextSampleFormat(Integer.toString(trackId),
|
||||
MimeTypes.APPLICATION_TTML, null, Format.NO_VALUE, 0, language, drmInitData,
|
||||
0 /* subsample timing is absolute */);
|
||||
} else if (childAtomType == Atom.TYPE_c608) {
|
||||
// Defined by the QuickTime File Format specification.
|
||||
out.format = Format.createTextSampleFormat(Integer.toString(trackId),
|
||||
MimeTypes.APPLICATION_MP4CEA608, null, Format.NO_VALUE, 0, language, drmInitData);
|
||||
out.requiredSampleTransformation = Track.TRANSFORMATION_CEA608_CDAT;
|
||||
} else if (childAtomType == Atom.TYPE_TTML || childAtomType == Atom.TYPE_tx3g
|
||||
|| childAtomType == Atom.TYPE_wvtt || childAtomType == Atom.TYPE_stpp
|
||||
|| childAtomType == Atom.TYPE_c608) {
|
||||
parseTextSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId,
|
||||
language, drmInitData, out);
|
||||
} else if (childAtomType == Atom.TYPE_camm) {
|
||||
out.format = Format.createSampleFormat(Integer.toString(trackId),
|
||||
MimeTypes.APPLICATION_CAMERA_MOTION, null, Format.NO_VALUE, drmInitData);
|
||||
@ -638,12 +625,49 @@ import java.util.List;
|
||||
return out;
|
||||
}
|
||||
|
||||
private static void parseTextSampleEntry(ParsableByteArray parent, int atomType, int position,
|
||||
int atomSize, int trackId, String language, DrmInitData drmInitData, StsdData out)
|
||||
throws ParserException {
|
||||
parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE);
|
||||
|
||||
// Default values.
|
||||
List<byte[]> initializationData = null;
|
||||
long subsampleOffsetUs = Format.OFFSET_SAMPLE_RELATIVE;
|
||||
|
||||
String mimeType;
|
||||
if (atomType == Atom.TYPE_TTML) {
|
||||
mimeType = MimeTypes.APPLICATION_TTML;
|
||||
} else if (atomType == Atom.TYPE_tx3g) {
|
||||
mimeType = MimeTypes.APPLICATION_TX3G;
|
||||
int sampleDescriptionLength = atomSize - Atom.HEADER_SIZE - 8;
|
||||
byte[] sampleDescriptionData = new byte[sampleDescriptionLength];
|
||||
parent.readBytes(sampleDescriptionData, 0, sampleDescriptionLength);
|
||||
initializationData = Collections.singletonList(sampleDescriptionData);
|
||||
} else if (atomType == Atom.TYPE_wvtt) {
|
||||
mimeType = MimeTypes.APPLICATION_MP4VTT;
|
||||
} else if (atomType == Atom.TYPE_stpp) {
|
||||
mimeType = MimeTypes.APPLICATION_TTML;
|
||||
subsampleOffsetUs = 0; // Subsample timing is absolute.
|
||||
} else if (atomType == Atom.TYPE_c608) {
|
||||
// Defined by the QuickTime File Format specification.
|
||||
mimeType = MimeTypes.APPLICATION_MP4CEA608;
|
||||
out.requiredSampleTransformation = Track.TRANSFORMATION_CEA608_CDAT;
|
||||
} else {
|
||||
// Never happens.
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
out.format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null,
|
||||
Format.NO_VALUE, 0, language, Format.NO_VALUE, drmInitData, subsampleOffsetUs,
|
||||
initializationData);
|
||||
}
|
||||
|
||||
private static void parseVideoSampleEntry(ParsableByteArray parent, int atomType, int position,
|
||||
int size, int trackId, int rotationDegrees, DrmInitData drmInitData, StsdData out,
|
||||
int entryIndex) throws ParserException {
|
||||
parent.setPosition(position + Atom.HEADER_SIZE);
|
||||
parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE);
|
||||
|
||||
parent.skipBytes(24);
|
||||
parent.skipBytes(16);
|
||||
int width = parent.readUnsignedShort();
|
||||
int height = parent.readUnsignedShort();
|
||||
boolean pixelWidthHeightRatioFromPasp = false;
|
||||
@ -784,15 +808,14 @@ import java.util.List;
|
||||
private static void parseAudioSampleEntry(ParsableByteArray parent, int atomType, int position,
|
||||
int size, int trackId, String language, boolean isQuickTime, DrmInitData drmInitData,
|
||||
StsdData out, int entryIndex) {
|
||||
parent.setPosition(position + Atom.HEADER_SIZE);
|
||||
parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE);
|
||||
|
||||
int quickTimeSoundDescriptionVersion = 0;
|
||||
if (isQuickTime) {
|
||||
parent.skipBytes(8);
|
||||
quickTimeSoundDescriptionVersion = parent.readUnsignedShort();
|
||||
parent.skipBytes(6);
|
||||
} else {
|
||||
parent.skipBytes(16);
|
||||
parent.skipBytes(8);
|
||||
}
|
||||
|
||||
int channelCount;
|
||||
@ -1177,6 +1200,8 @@ import java.util.List;
|
||||
*/
|
||||
private static final class StsdData {
|
||||
|
||||
public static final int STSD_HEADER_SIZE = 8;
|
||||
|
||||
public final TrackEncryptionBox[] trackEncryptionBoxes;
|
||||
|
||||
public Format format;
|
||||
|
@ -92,7 +92,7 @@ public interface SubtitleDecoderFactory {
|
||||
case MimeTypes.APPLICATION_SUBRIP:
|
||||
return new SubripDecoder();
|
||||
case MimeTypes.APPLICATION_TX3G:
|
||||
return new Tx3gDecoder();
|
||||
return new Tx3gDecoder(format.initializationData);
|
||||
case MimeTypes.APPLICATION_CEA608:
|
||||
case MimeTypes.APPLICATION_MP4CEA608:
|
||||
return new Cea608Decoder(format.sampleMimeType, format.accessibilityChannel);
|
||||
|
@ -15,26 +15,28 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.text.tx3g;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.text.style.TypefaceSpan;
|
||||
import android.text.style.UnderlineSpan;
|
||||
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.text.Subtitle;
|
||||
import com.google.android.exoplayer2.text.SubtitleDecoderException;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A {@link SimpleSubtitleDecoder} for tx3g.
|
||||
* <p>
|
||||
* Currently only supports parsing of a single text track.
|
||||
* Currently supports parsing of a single text track with embedded styles.
|
||||
*/
|
||||
public final class Tx3gDecoder extends SimpleSubtitleDecoder {
|
||||
|
||||
@ -42,6 +44,8 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder {
|
||||
private static final char BOM_UTF16_LE = '\uFFFE';
|
||||
|
||||
private static final int TYPE_STYL = Util.getIntegerCodeForString("styl");
|
||||
private static final int TYPE_TBOX = Util.getIntegerCodeForString("tbox");
|
||||
private static final String TX3G_SERIF = "Serif";
|
||||
|
||||
private static final int SIZE_ATOM_HEADER = 8;
|
||||
private static final int SIZE_SHORT = 2;
|
||||
@ -52,44 +56,107 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder {
|
||||
private static final int FONT_FACE_ITALIC = 0x0002;
|
||||
private static final int FONT_FACE_UNDERLINE = 0x0004;
|
||||
|
||||
private final ParsableByteArray parsableByteArray;
|
||||
private static final int SPAN_PRIORITY_LOW = (0xFF << Spanned.SPAN_PRIORITY_SHIFT);
|
||||
private static final int SPAN_PRIORITY_HIGH = (0x00 << Spanned.SPAN_PRIORITY_SHIFT);
|
||||
|
||||
public Tx3gDecoder() {
|
||||
private static final int DEFAULT_FONT_FACE = 0;
|
||||
private static final int DEFAULT_COLOR = Color.WHITE;
|
||||
private static final String DEFAULT_FONT_FAMILY = C.SANS_SERIF_NAME;
|
||||
private static final float DEFAULT_VERTICAL_PLACEMENT = 0.85f;
|
||||
|
||||
private final ParsableByteArray parsableByteArray;
|
||||
private boolean customVerticalPlacement;
|
||||
private int defaultFontFace;
|
||||
private int defaultColorRgba;
|
||||
private String defaultFontFamily;
|
||||
private float defaultVerticalPlacement;
|
||||
private int calculatedVideoTrackHeight;
|
||||
|
||||
/**
|
||||
* Sets up a new {@link Tx3gDecoder} with default values.
|
||||
*
|
||||
* @param initializationData Sample description atom ('stsd') data with default subtitle styles.
|
||||
*/
|
||||
public Tx3gDecoder(List<byte[]> initializationData) {
|
||||
super("Tx3gDecoder");
|
||||
parsableByteArray = new ParsableByteArray();
|
||||
decodeInitializationData(initializationData);
|
||||
}
|
||||
|
||||
private void decodeInitializationData(List<byte[]> initializationData) {
|
||||
if (initializationData != null && initializationData.size() == 1
|
||||
&& (initializationData.get(0).length == 48 || initializationData.get(0).length == 53)) {
|
||||
byte[] initializationBytes = initializationData.get(0);
|
||||
defaultFontFace = initializationBytes[24];
|
||||
defaultColorRgba = ((initializationBytes[26] & 0xFF) << 24)
|
||||
| ((initializationBytes[27] & 0xFF) << 16)
|
||||
| ((initializationBytes[28] & 0xFF) << 8)
|
||||
| (initializationBytes[29] & 0xFF);
|
||||
String fontFamily = new String(initializationBytes, 43, initializationBytes.length - 43);
|
||||
defaultFontFamily = TX3G_SERIF.equals(fontFamily) ? C.SERIF_NAME : C.SANS_SERIF_NAME;
|
||||
//font size (initializationBytes[25]) is 5% of video height
|
||||
calculatedVideoTrackHeight = 20 * initializationBytes[25];
|
||||
customVerticalPlacement = (initializationBytes[0] & 0x20) != 0;
|
||||
if (customVerticalPlacement) {
|
||||
int requestedVerticalPlacement = ((initializationBytes[10] & 0xFF) << 8)
|
||||
| (initializationBytes[11] & 0xFF);
|
||||
defaultVerticalPlacement = (float) requestedVerticalPlacement / calculatedVideoTrackHeight;
|
||||
defaultVerticalPlacement = Util.constrainValue(defaultVerticalPlacement, 0.0f, 0.95f);
|
||||
} else {
|
||||
defaultVerticalPlacement = DEFAULT_VERTICAL_PLACEMENT;
|
||||
}
|
||||
} else {
|
||||
defaultFontFace = DEFAULT_FONT_FACE;
|
||||
defaultColorRgba = DEFAULT_COLOR;
|
||||
defaultFontFamily = DEFAULT_FONT_FAMILY;
|
||||
customVerticalPlacement = false;
|
||||
defaultVerticalPlacement = DEFAULT_VERTICAL_PLACEMENT;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Subtitle decode(byte[] bytes, int length, boolean reset)
|
||||
throws SubtitleDecoderException {
|
||||
try {
|
||||
parsableByteArray.reset(bytes, length);
|
||||
String cueTextString = readSubtitleText(parsableByteArray);
|
||||
if (cueTextString.isEmpty()) {
|
||||
return Tx3gSubtitle.EMPTY;
|
||||
}
|
||||
SpannableStringBuilder cueText = new SpannableStringBuilder(cueTextString);
|
||||
while (parsableByteArray.bytesLeft() >= SIZE_ATOM_HEADER) {
|
||||
int atomSize = parsableByteArray.readInt();
|
||||
int atomType = parsableByteArray.readInt();
|
||||
if (atomType == TYPE_STYL) {
|
||||
Assertions.checkArgument(parsableByteArray.bytesLeft() >= SIZE_SHORT);
|
||||
int styleRecordCount = parsableByteArray.readUnsignedShort();
|
||||
for (int i = 0; i < styleRecordCount; i++) {
|
||||
applyStyleRecord(parsableByteArray, cueText);
|
||||
}
|
||||
} else {
|
||||
parsableByteArray.skipBytes(atomSize - SIZE_ATOM_HEADER);
|
||||
}
|
||||
}
|
||||
return new Tx3gSubtitle(new Cue(cueText));
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new SubtitleDecoderException("Unexpected subtitle format.", e);
|
||||
parsableByteArray.reset(bytes, length);
|
||||
String cueTextString = readSubtitleText(parsableByteArray);
|
||||
if (cueTextString.isEmpty()) {
|
||||
return Tx3gSubtitle.EMPTY;
|
||||
}
|
||||
// Attach default styles.
|
||||
SpannableStringBuilder cueText = new SpannableStringBuilder(cueTextString);
|
||||
attachFontFace(cueText, defaultFontFace, DEFAULT_FONT_FACE, 0, cueText.length(),
|
||||
SPAN_PRIORITY_LOW);
|
||||
attachColor(cueText, defaultColorRgba, DEFAULT_COLOR, 0, cueText.length(),
|
||||
SPAN_PRIORITY_LOW);
|
||||
attachFontFamily(cueText, defaultFontFamily, DEFAULT_FONT_FAMILY, 0, cueText.length(),
|
||||
SPAN_PRIORITY_LOW);
|
||||
float verticalPlacement = defaultVerticalPlacement;
|
||||
// Find and attach additional styles.
|
||||
while (parsableByteArray.bytesLeft() >= SIZE_ATOM_HEADER) {
|
||||
int position = parsableByteArray.getPosition();
|
||||
int atomSize = parsableByteArray.readInt();
|
||||
int atomType = parsableByteArray.readInt();
|
||||
if (atomType == TYPE_STYL) {
|
||||
assertTrue(parsableByteArray.bytesLeft() >= SIZE_SHORT);
|
||||
int styleRecordCount = parsableByteArray.readUnsignedShort();
|
||||
for (int i = 0; i < styleRecordCount; i++) {
|
||||
applyStyleRecord(parsableByteArray, cueText);
|
||||
}
|
||||
} else if (atomType == TYPE_TBOX && customVerticalPlacement) {
|
||||
assertTrue(parsableByteArray.bytesLeft() >= SIZE_SHORT);
|
||||
int requestedVerticalPlacement = parsableByteArray.readUnsignedShort();
|
||||
verticalPlacement = (float) requestedVerticalPlacement / calculatedVideoTrackHeight;
|
||||
verticalPlacement = Util.constrainValue(verticalPlacement, 0.0f, 0.95f);
|
||||
}
|
||||
parsableByteArray.setPosition(position + atomSize);
|
||||
}
|
||||
return new Tx3gSubtitle(new Cue(cueText, null, verticalPlacement, Cue.LINE_TYPE_FRACTION,
|
||||
Cue.ANCHOR_TYPE_START, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET));
|
||||
}
|
||||
|
||||
private static String readSubtitleText(ParsableByteArray parsableByteArray) {
|
||||
Assertions.checkArgument(parsableByteArray.bytesLeft() >= SIZE_SHORT);
|
||||
private static String readSubtitleText(ParsableByteArray parsableByteArray)
|
||||
throws SubtitleDecoderException {
|
||||
assertTrue(parsableByteArray.bytesLeft() >= SIZE_SHORT);
|
||||
int textLength = parsableByteArray.readUnsignedShort();
|
||||
if (textLength == 0) {
|
||||
return "";
|
||||
@ -103,47 +170,65 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder {
|
||||
return parsableByteArray.readString(textLength, Charset.forName(C.UTF8_NAME));
|
||||
}
|
||||
|
||||
private static void applyStyleRecord(ParsableByteArray parsableByteArray,
|
||||
SpannableStringBuilder cueText) {
|
||||
Assertions.checkArgument(parsableByteArray.bytesLeft() >= SIZE_STYLE_RECORD);
|
||||
private void applyStyleRecord(ParsableByteArray parsableByteArray,
|
||||
SpannableStringBuilder cueText) throws SubtitleDecoderException {
|
||||
assertTrue(parsableByteArray.bytesLeft() >= SIZE_STYLE_RECORD);
|
||||
int start = parsableByteArray.readUnsignedShort();
|
||||
int end = parsableByteArray.readUnsignedShort();
|
||||
parsableByteArray.skipBytes(2); // font identifier
|
||||
int fontFace = parsableByteArray.readUnsignedByte();
|
||||
parsableByteArray.skipBytes(1); // font size
|
||||
int colorRgba = parsableByteArray.readInt();
|
||||
|
||||
if (fontFace != 0) {
|
||||
attachFontFace(cueText, fontFace, start, end);
|
||||
}
|
||||
attachColor(cueText, colorRgba, start, end);
|
||||
attachFontFace(cueText, fontFace, defaultFontFace, start, end, SPAN_PRIORITY_HIGH);
|
||||
attachColor(cueText, colorRgba, defaultColorRgba, start, end, SPAN_PRIORITY_HIGH);
|
||||
}
|
||||
|
||||
private static void attachFontFace(SpannableStringBuilder cueText, int fontFace, int start,
|
||||
int end) {
|
||||
boolean isBold = (fontFace & FONT_FACE_BOLD) != 0;
|
||||
boolean isItalic = (fontFace & FONT_FACE_ITALIC) != 0;
|
||||
if (isBold) {
|
||||
if (isItalic) {
|
||||
cueText.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), start, end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
} else {
|
||||
cueText.setSpan(new StyleSpan(Typeface.BOLD), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
private static void attachFontFace(SpannableStringBuilder cueText, int fontFace,
|
||||
int defaultFontFace, int start, int end, int spanPriority) {
|
||||
if (fontFace != defaultFontFace) {
|
||||
final int flags = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | spanPriority;
|
||||
boolean isBold = (fontFace & FONT_FACE_BOLD) != 0;
|
||||
boolean isItalic = (fontFace & FONT_FACE_ITALIC) != 0;
|
||||
if (isBold) {
|
||||
if (isItalic) {
|
||||
cueText.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), start, end, flags);
|
||||
} else {
|
||||
cueText.setSpan(new StyleSpan(Typeface.BOLD), start, end, flags);
|
||||
}
|
||||
} else if (isItalic) {
|
||||
cueText.setSpan(new StyleSpan(Typeface.ITALIC), start, end, flags);
|
||||
}
|
||||
boolean isUnderlined = (fontFace & FONT_FACE_UNDERLINE) != 0;
|
||||
if (isUnderlined) {
|
||||
cueText.setSpan(new UnderlineSpan(), start, end, flags);
|
||||
}
|
||||
if (!isUnderlined && !isBold && !isItalic) {
|
||||
cueText.setSpan(new StyleSpan(Typeface.NORMAL), start, end, flags);
|
||||
}
|
||||
} else if (isItalic) {
|
||||
cueText.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
boolean isUnderlined = (fontFace & FONT_FACE_UNDERLINE) != 0;
|
||||
if (isUnderlined) {
|
||||
cueText.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
|
||||
private static void attachColor(SpannableStringBuilder cueText, int colorRgba, int start,
|
||||
int end) {
|
||||
int colorArgb = ((colorRgba & 0xFF) << 24) | (colorRgba >>> 8);
|
||||
cueText.setSpan(new ForegroundColorSpan(colorArgb), start, end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
private static void attachColor(SpannableStringBuilder cueText, int colorRgba,
|
||||
int defaultColorRgba, int start, int end, int spanPriority) {
|
||||
if (colorRgba != defaultColorRgba) {
|
||||
int colorArgb = ((colorRgba & 0xFF) << 24) | (colorRgba >>> 8);
|
||||
cueText.setSpan(new ForegroundColorSpan(colorArgb), start, end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | spanPriority);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
private static void attachFontFamily(SpannableStringBuilder cueText, String fontFamily,
|
||||
String defaultFontFamily, int start, int end, int spanPriority) {
|
||||
if (fontFamily != defaultFontFamily) {
|
||||
cueText.setSpan(new TypefaceSpan(fontFamily), start, end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | spanPriority);
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertTrue(boolean checkValue) throws SubtitleDecoderException {
|
||||
if (!checkValue) {
|
||||
throw new SubtitleDecoderException("Unexpected subtitle format.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user