diff --git a/library/core/src/androidTest/assets/tx3g/initialization b/library/core/src/androidTest/assets/tx3g/initialization new file mode 100644 index 0000000000..def42b9ade Binary files /dev/null and b/library/core/src/androidTest/assets/tx3g/initialization differ diff --git a/library/core/src/androidTest/assets/tx3g/initialization_all_defaults b/library/core/src/androidTest/assets/tx3g/initialization_all_defaults new file mode 100644 index 0000000000..be2f92b5f2 Binary files /dev/null and b/library/core/src/androidTest/assets/tx3g/initialization_all_defaults differ diff --git a/library/core/src/androidTest/assets/tx3g/sample_just_text b/library/core/src/androidTest/assets/tx3g/sample_just_text new file mode 100644 index 0000000000..68561eca7e Binary files /dev/null and b/library/core/src/androidTest/assets/tx3g/sample_just_text differ diff --git a/library/core/src/androidTest/assets/tx3g/sample_with_styl_all_defaults b/library/core/src/androidTest/assets/tx3g/sample_with_styl_all_defaults new file mode 100644 index 0000000000..c1dec5d3c9 Binary files /dev/null and b/library/core/src/androidTest/assets/tx3g/sample_with_styl_all_defaults differ diff --git a/library/core/src/androidTest/assets/tx3g/sample_with_tbox b/library/core/src/androidTest/assets/tx3g/sample_with_tbox new file mode 100644 index 0000000000..72a8fb97cb Binary files /dev/null and b/library/core/src/androidTest/assets/tx3g/sample_with_tbox differ diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoderTest.java index ce5281584a..0b24d0f275 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoderTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoderTest.java @@ -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.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.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.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.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.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.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.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.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 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); + } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index 56092f17a9..6fb3c4500e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -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. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Format.java b/library/core/src/main/java/com/google/android/exoplayer2/Format.java index 866e512288..c29c8fbd83 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Format.java @@ -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.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.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.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 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. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 54141f2545..4b30b383ec 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -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 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; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java index f65d5a6e55..795189e1a6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java @@ -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); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java index 79ec89838a..2270ccc632 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java @@ -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. *

- * 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 initializationData) { super("Tx3gDecoder"); parsableByteArray = new ParsableByteArray(); + decodeInitializationData(initializationData); + } + + private void decodeInitializationData(List 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."); + } } }