mirror of
https://github.com/androidx/media.git
synced 2025-05-11 09:39:52 +08:00
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
This commit is contained in:
parent
f36500c200
commit
1dc8bb5bb1
BIN
library/core/src/androidTest/assets/tx3g/no_subtitle
Normal file
BIN
library/core/src/androidTest/assets/tx3g/no_subtitle
Normal file
Binary file not shown.
BIN
library/core/src/androidTest/assets/tx3g/sample_utf16_be_no_styl
Normal file
BIN
library/core/src/androidTest/assets/tx3g/sample_utf16_be_no_styl
Normal file
Binary file not shown.
BIN
library/core/src/androidTest/assets/tx3g/sample_utf16_le_no_styl
Normal file
BIN
library/core/src/androidTest/assets/tx3g/sample_utf16_le_no_styl
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
library/core/src/androidTest/assets/tx3g/sample_with_styl
Normal file
BIN
library/core/src/androidTest/assets/tx3g/sample_with_styl
Normal file
Binary file not shown.
@ -0,0 +1,123 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer2.text.tx3g;
|
||||||
|
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.test.InstrumentationTestCase;
|
||||||
|
import android.text.SpannedString;
|
||||||
|
import android.text.style.ForegroundColorSpan;
|
||||||
|
import android.text.style.StyleSpan;
|
||||||
|
import android.text.style.UnderlineSpan;
|
||||||
|
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||||
|
import com.google.android.exoplayer2.text.Subtitle;
|
||||||
|
import com.google.android.exoplayer2.text.SubtitleDecoderException;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit test for {@link Tx3gDecoder}.
|
||||||
|
*/
|
||||||
|
public final class Tx3gDecoderTest extends InstrumentationTestCase {
|
||||||
|
|
||||||
|
private static final String NO_SUBTITLE = "tx3g/no_subtitle";
|
||||||
|
private static final String SAMPLE_WITH_STYL = "tx3g/sample_with_styl";
|
||||||
|
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";
|
||||||
|
|
||||||
|
public void testDecodeNoSubtitle() throws IOException, SubtitleDecoderException {
|
||||||
|
Tx3gDecoder decoder = new Tx3gDecoder();
|
||||||
|
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), NO_SUBTITLE);
|
||||||
|
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||||
|
assertTrue(subtitle.getCues(0).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testDecodeWithStyl() throws IOException, SubtitleDecoderException {
|
||||||
|
Tx3gDecoder decoder = new Tx3gDecoder();
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testDecodeUtf16BeNoStyl() throws IOException, SubtitleDecoderException {
|
||||||
|
Tx3gDecoder decoder = new Tx3gDecoder();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testDecodeUtf16LeNoStyl() throws IOException, SubtitleDecoderException {
|
||||||
|
Tx3gDecoder decoder = new Tx3gDecoder();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testDecodeWithMultipleStyl() throws IOException, SubtitleDecoderException {
|
||||||
|
Tx3gDecoder decoder = new Tx3gDecoder();
|
||||||
|
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);
|
||||||
|
assertEquals("Line 2\nLine 3", text.toString());
|
||||||
|
assertEquals(4, text.getSpans(0, text.length(), Object.class).length);
|
||||||
|
StyleSpan styleSpan = findSpan(text, 0, 5, StyleSpan.class);
|
||||||
|
assertEquals(Typeface.ITALIC, styleSpan.getStyle());
|
||||||
|
findSpan(text, 7, 12, UnderlineSpan.class);
|
||||||
|
ForegroundColorSpan colorSpan = findSpan(text, 0, 5, ForegroundColorSpan.class);
|
||||||
|
assertEquals(Color.GREEN, colorSpan.getForegroundColor());
|
||||||
|
colorSpan = findSpan(text, 7, 12, ForegroundColorSpan.class);
|
||||||
|
assertEquals(Color.GREEN, colorSpan.getForegroundColor());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testDecodeWithOtherExtension() throws IOException, SubtitleDecoderException {
|
||||||
|
Tx3gDecoder decoder = new Tx3gDecoder();
|
||||||
|
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);
|
||||||
|
assertEquals("CC Test", text.toString());
|
||||||
|
assertEquals(2, text.getSpans(0, text.length(), Object.class).length);
|
||||||
|
StyleSpan styleSpan = findSpan(text, 0, 6, StyleSpan.class);
|
||||||
|
assertEquals(Typeface.BOLD, styleSpan.getStyle());
|
||||||
|
ForegroundColorSpan colorSpan = findSpan(text, 0, 6, ForegroundColorSpan.class);
|
||||||
|
assertEquals(Color.GREEN, colorSpan.getForegroundColor());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> T findSpan(SpannedString testObject, int expectedStart, int expectedEnd,
|
||||||
|
Class<T> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -76,6 +76,11 @@ public final class C {
|
|||||||
*/
|
*/
|
||||||
public static final String UTF8_NAME = "UTF-8";
|
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.
|
* Crypto modes for a codec.
|
||||||
*/
|
*/
|
||||||
|
@ -15,10 +15,21 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.text.tx3g;
|
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.Cue;
|
||||||
import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
|
import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
|
||||||
import com.google.android.exoplayer2.text.Subtitle;
|
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.ParsableByteArray;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link SimpleSubtitleDecoder} for tx3g.
|
* A {@link SimpleSubtitleDecoder} for tx3g.
|
||||||
@ -27,6 +38,20 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
|||||||
*/
|
*/
|
||||||
public final class Tx3gDecoder extends SimpleSubtitleDecoder {
|
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;
|
private final ParsableByteArray parsableByteArray;
|
||||||
|
|
||||||
public Tx3gDecoder() {
|
public Tx3gDecoder() {
|
||||||
@ -35,14 +60,90 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Subtitle decode(byte[] bytes, int length, boolean reset) {
|
protected Subtitle decode(byte[] bytes, int length, boolean reset)
|
||||||
parsableByteArray.reset(bytes, length);
|
throws SubtitleDecoderException {
|
||||||
int textLength = parsableByteArray.readUnsignedShort();
|
try {
|
||||||
if (textLength == 0) {
|
parsableByteArray.reset(bytes, length);
|
||||||
return Tx3gSubtitle.EMPTY;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -201,6 +201,14 @@ public final class ParsableByteArray {
|
|||||||
return (data[position] & 0xFF);
|
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.
|
* Reads the next byte as an unsigned value.
|
||||||
*/
|
*/
|
||||||
|
Loading…
x
Reference in New Issue
Block a user