From 0a7011b3904abb2cb8d4d8834d1086da45b8d687 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 12 Apr 2017 06:11:27 -0700 Subject: [PATCH] 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 --- .../androidTest/assets/tx3g/initialization | Bin 0 -> 48 bytes .../assets/tx3g/initialization_all_defaults | Bin 0 -> 53 bytes .../androidTest/assets/tx3g/sample_just_text | Bin 0 -> 9 bytes .../assets/tx3g/sample_with_styl_all_defaults | Bin 0 -> 31 bytes .../androidTest/assets/tx3g/sample_with_tbox | Bin 0 -> 25 bytes .../exoplayer2/text/tx3g/Tx3gDecoderTest.java | 107 ++++++++- .../java/com/google/android/exoplayer2/C.java | 10 + .../com/google/android/exoplayer2/Format.java | 14 +- .../exoplayer2/extractor/mp4/AtomParsers.java | 71 ++++-- .../text/SubtitleDecoderFactory.java | 2 +- .../exoplayer2/text/tx3g/Tx3gDecoder.java | 203 +++++++++++++----- 11 files changed, 312 insertions(+), 95 deletions(-) create mode 100644 library/core/src/androidTest/assets/tx3g/initialization create mode 100644 library/core/src/androidTest/assets/tx3g/initialization_all_defaults create mode 100644 library/core/src/androidTest/assets/tx3g/sample_just_text create mode 100644 library/core/src/androidTest/assets/tx3g/sample_with_styl_all_defaults create mode 100644 library/core/src/androidTest/assets/tx3g/sample_with_tbox diff --git a/library/core/src/androidTest/assets/tx3g/initialization b/library/core/src/androidTest/assets/tx3g/initialization new file mode 100644 index 0000000000000000000000000000000000000000..def42b9aded8d1dde23d219c7364edec639b4ffb GIT binary patch literal 48 tcmY#jU|?YU4yN)ST{JeC^HQJ)Y%Hs literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..68561eca7e22ab4f59ed0d7916e6315318720a15 GIT binary patch literal 9 QcmZQzcXn0?Ni8k`00emptyList()); 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."); + } } }