From 1dc8bb5bb17f1a627546eb7e8b4af8fb0cf31697 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 11 Apr 2017 03:35:33 -0700 Subject: [PATCH] Support 'styl' in Tx3g decoder. Extended Tx3gDecoder to read additional information after subtitle text. Currently parses font face, font size, and foreground colour. Font identifier and other information provided in subtitle sample description not yet supported. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=152793774 --- .../src/androidTest/assets/tx3g/no_subtitle | Bin 0 -> 2 bytes .../assets/tx3g/sample_utf16_be_no_styl | Bin 0 -> 8 bytes .../assets/tx3g/sample_utf16_le_no_styl | Bin 0 -> 8 bytes .../assets/tx3g/sample_with_multiple_styl | Bin 0 -> 61 bytes .../assets/tx3g/sample_with_other_extension | Bin 0 -> 39 bytes .../androidTest/assets/tx3g/sample_with_styl | Bin 0 -> 31 bytes .../exoplayer2/text/tx3g/Tx3gDecoderTest.java | 123 ++++++++++++++++++ .../java/com/google/android/exoplayer2/C.java | 5 + .../exoplayer2/text/tx3g/Tx3gDecoder.java | 115 +++++++++++++++- .../exoplayer2/util/ParsableByteArray.java | 8 ++ 10 files changed, 244 insertions(+), 7 deletions(-) create mode 100644 library/core/src/androidTest/assets/tx3g/no_subtitle create mode 100644 library/core/src/androidTest/assets/tx3g/sample_utf16_be_no_styl create mode 100644 library/core/src/androidTest/assets/tx3g/sample_utf16_le_no_styl create mode 100644 library/core/src/androidTest/assets/tx3g/sample_with_multiple_styl create mode 100644 library/core/src/androidTest/assets/tx3g/sample_with_other_extension create mode 100644 library/core/src/androidTest/assets/tx3g/sample_with_styl create mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoderTest.java diff --git a/library/core/src/androidTest/assets/tx3g/no_subtitle b/library/core/src/androidTest/assets/tx3g/no_subtitle new file mode 100644 index 0000000000000000000000000000000000000000..09f370e38f498a462e1ca0faa724559b6630c04f GIT binary patch literal 2 JcmZQz0000200961 literal 0 HcmV?d00001 diff --git a/library/core/src/androidTest/assets/tx3g/sample_utf16_be_no_styl b/library/core/src/androidTest/assets/tx3g/sample_utf16_be_no_styl new file mode 100644 index 0000000000000000000000000000000000000000..9c3fed2b7d4ac24ba78ba32f0084f63eb68ee28c GIT binary patch literal 8 PcmZQz`}f~JA+i<#4y*%- literal 0 HcmV?d00001 diff --git a/library/core/src/androidTest/assets/tx3g/sample_utf16_le_no_styl b/library/core/src/androidTest/assets/tx3g/sample_utf16_le_no_styl new file mode 100644 index 0000000000000000000000000000000000000000..03a2cb23505459a02654e345b8dbb21807e88ca0 GIT binary patch literal 8 PcmZQz`~NS&zcvy84&noe literal 0 HcmV?d00001 diff --git a/library/core/src/androidTest/assets/tx3g/sample_with_multiple_styl b/library/core/src/androidTest/assets/tx3g/sample_with_multiple_styl new file mode 100644 index 0000000000000000000000000000000000000000..48b01c3438e861713348e131e02206831379d6b4 GIT binary patch literal 61 zcmZSJ^~uajRWRZLQ^pJo40^>Sl{pN|Ko% T findSpan(SpannedString testObject, int expectedStart, int expectedEnd, + Class expectedType) { + T[] spans = testObject.getSpans(0, testObject.length(), expectedType); + for (T span : spans) { + if (testObject.getSpanStart(span) == expectedStart + && testObject.getSpanEnd(span) == expectedEnd) { + return span; + } + } + fail("Span not found."); + return null; + } + +} 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 02e5939b86..56092f17a9 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 @@ -76,6 +76,11 @@ public final class C { */ public static final String UTF8_NAME = "UTF-8"; + /** + * The name of the UTF-16 charset. + */ + public static final String UTF16_NAME = "UTF-16"; + /** * Crypto modes for a codec. */ 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 dccb64caec..79ec89838a 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,10 +15,21 @@ */ package com.google.android.exoplayer2.text.tx3g; +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.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; /** * A {@link SimpleSubtitleDecoder} for tx3g. @@ -27,6 +38,20 @@ import com.google.android.exoplayer2.util.ParsableByteArray; */ public final class Tx3gDecoder extends SimpleSubtitleDecoder { + private static final char BOM_UTF16_BE = '\uFEFF'; + private static final char BOM_UTF16_LE = '\uFFFE'; + + private static final int TYPE_STYL = Util.getIntegerCodeForString("styl"); + + private static final int SIZE_ATOM_HEADER = 8; + private static final int SIZE_SHORT = 2; + private static final int SIZE_BOM_UTF16 = 2; + private static final int SIZE_STYLE_RECORD = 12; + + private static final int FONT_FACE_BOLD = 0x0001; + private static final int FONT_FACE_ITALIC = 0x0002; + private static final int FONT_FACE_UNDERLINE = 0x0004; + private final ParsableByteArray parsableByteArray; public Tx3gDecoder() { @@ -35,14 +60,90 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder { } @Override - protected Subtitle decode(byte[] bytes, int length, boolean reset) { - parsableByteArray.reset(bytes, length); - int textLength = parsableByteArray.readUnsignedShort(); - if (textLength == 0) { - return Tx3gSubtitle.EMPTY; + 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); } - String cueText = parsableByteArray.readString(textLength); - return new Tx3gSubtitle(new Cue(cueText)); } + private static String readSubtitleText(ParsableByteArray parsableByteArray) { + Assertions.checkArgument(parsableByteArray.bytesLeft() >= SIZE_SHORT); + int textLength = parsableByteArray.readUnsignedShort(); + if (textLength == 0) { + return ""; + } + if (parsableByteArray.bytesLeft() >= SIZE_BOM_UTF16) { + char firstChar = parsableByteArray.peekChar(); + if (firstChar == BOM_UTF16_BE || firstChar == BOM_UTF16_LE) { + return parsableByteArray.readString(textLength, Charset.forName(C.UTF16_NAME)); + } + } + return parsableByteArray.readString(textLength, Charset.forName(C.UTF8_NAME)); + } + + private static void applyStyleRecord(ParsableByteArray parsableByteArray, + SpannableStringBuilder cueText) { + Assertions.checkArgument(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); + } + + 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); + } + } 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); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java index ef4aa05cfe..2a907e5955 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java @@ -201,6 +201,14 @@ public final class ParsableByteArray { return (data[position] & 0xFF); } + /** + * Peeks at the next char. + */ + public char peekChar() { + return (char) ((data[position] & 0xFF) << 8 + | (data[position + 1] & 0xFF)); + } + /** * Reads the next byte as an unsigned value. */