Merge branch 'RikHeijdens-eia-608-improvements' into dev-v2

This commit is contained in:
Oliver Woodman 2016-12-01 18:31:49 +00:00
commit 54b4df703a
3 changed files with 429 additions and 171 deletions

View File

@ -15,7 +15,18 @@
*/ */
package com.google.android.exoplayer2.text.cea; package com.google.android.exoplayer2.text.cea;
import android.text.TextUtils; import static com.google.android.exoplayer2.text.Cue.TYPE_UNSET;
import android.graphics.Color;
import android.graphics.Typeface;
import android.text.Layout.Alignment;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.CharacterStyle;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.text.style.UnderlineSpan;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.Subtitle;
@ -23,14 +34,15 @@ import com.google.android.exoplayer2.text.SubtitleDecoder;
import com.google.android.exoplayer2.text.SubtitleInputBuffer; import com.google.android.exoplayer2.text.SubtitleInputBuffer;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
/** /**
* A {@link SubtitleDecoder} for CEA-608 (also known as "line 21 captions" and "EIA-608"). * A {@link SubtitleDecoder} for CEA-608 (also known as "line 21 captions" and "EIA-608").
*/ */
public final class Cea608Decoder extends CeaDecoder { public final class Cea608Decoder extends CeaDecoder {
private static final String TAG = "Cea608Decoder";
private static final int CC_VALID_FLAG = 0x04; private static final int CC_VALID_FLAG = 0x04;
private static final int CC_TYPE_FLAG = 0x02; private static final int CC_TYPE_FLAG = 0x02;
private static final int CC_FIELD_FLAG = 0x01; private static final int CC_FIELD_FLAG = 0x01;
@ -50,6 +62,18 @@ public final class Cea608Decoder extends CeaDecoder {
private static final int CC_MODE_POP_ON = 2; private static final int CC_MODE_POP_ON = 2;
private static final int CC_MODE_PAINT_ON = 3; private static final int CC_MODE_PAINT_ON = 3;
private static final int[] ROW_INDICES = new int[] {11, 1, 3, 12, 14, 5, 7, 9};
private static final int[] COLUMN_INDICES = new int[] {0, 4, 8, 12, 16, 20, 24, 28};
private static final int[] COLORS = new int[] {
Color.WHITE,
Color.GREEN,
Color.BLUE,
Color.CYAN,
Color.RED,
Color.YELLOW,
Color.MAGENTA,
};
// The default number of rows to display in roll-up captions mode. // The default number of rows to display in roll-up captions mode.
private static final int DEFAULT_CAPTIONS_ROW_COUNT = 4; private static final int DEFAULT_CAPTIONS_ROW_COUNT = 4;
@ -94,90 +118,89 @@ public final class Cea608Decoder extends CeaDecoder {
private static final byte CTRL_ERASE_DISPLAYED_MEMORY = 0x2C; private static final byte CTRL_ERASE_DISPLAYED_MEMORY = 0x2C;
private static final byte CTRL_CARRIAGE_RETURN = 0x2D; private static final byte CTRL_CARRIAGE_RETURN = 0x2D;
private static final byte CTRL_ERASE_NON_DISPLAYED_MEMORY = 0x2E; private static final byte CTRL_ERASE_NON_DISPLAYED_MEMORY = 0x2E;
private static final byte CTRL_DELETE_TO_END_OF_ROW = 0x24;
private static final byte CTRL_BACKSPACE = 0x21; private static final byte CTRL_BACKSPACE = 0x21;
private static final byte CTRL_MISC_CHAN_1 = 0x14;
private static final byte CTRL_MISC_CHAN_2 = 0x1C;
// Basic North American 608 CC char set, mostly ASCII. Indexed by (char-0x20). // Basic North American 608 CC char set, mostly ASCII. Indexed by (char-0x20).
private static final int[] BASIC_CHARACTER_SET = new int[] { private static final int[] BASIC_CHARACTER_SET = new int[] {
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, // ! " # $ % & ' 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, // ! " # $ % & '
0x28, 0x29, // ( ) 0x28, 0x29, // ( )
0xE1, // 2A: 225 'á' "Latin small letter A with acute" 0xE1, // 2A: 225 'á' "Latin small letter A with acute"
0x2B, 0x2C, 0x2D, 0x2E, 0x2F, // + , - . / 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, // + , - . /
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, // 0 1 2 3 4 5 6 7 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, // 0 1 2 3 4 5 6 7
0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, // 8 9 : ; < = > ? 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, // 8 9 : ; < = > ?
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, // @ A B C D E F G 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, // @ A B C D E F G
0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, // H I J K L M N O 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, // H I J K L M N O
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, // P Q R S T U V W 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, // P Q R S T U V W
0x58, 0x59, 0x5A, 0x5B, // X Y Z [ 0x58, 0x59, 0x5A, 0x5B, // X Y Z [
0xE9, // 5C: 233 'é' "Latin small letter E with acute" 0xE9, // 5C: 233 'é' "Latin small letter E with acute"
0x5D, // ] 0x5D, // ]
0xED, // 5E: 237 'í' "Latin small letter I with acute" 0xED, // 5E: 237 'í' "Latin small letter I with acute"
0xF3, // 5F: 243 'ó' "Latin small letter O with acute" 0xF3, // 5F: 243 'ó' "Latin small letter O with acute"
0xFA, // 60: 250 'ú' "Latin small letter U with acute" 0xFA, // 60: 250 'ú' "Latin small letter U with acute"
0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, // a b c d e f g 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, // a b c d e f g
0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, // h i j k l m n o 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, // h i j k l m n o
0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, // p q r s t u v w 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, // p q r s t u v w
0x78, 0x79, 0x7A, // x y z 0x78, 0x79, 0x7A, // x y z
0xE7, // 7B: 231 'ç' "Latin small letter C with cedilla" 0xE7, // 7B: 231 'ç' "Latin small letter C with cedilla"
0xF7, // 7C: 247 '÷' "Division sign" 0xF7, // 7C: 247 '÷' "Division sign"
0xD1, // 7D: 209 'Ñ' "Latin capital letter N with tilde" 0xD1, // 7D: 209 'Ñ' "Latin capital letter N with tilde"
0xF1, // 7E: 241 'ñ' "Latin small letter N with tilde" 0xF1, // 7E: 241 'ñ' "Latin small letter N with tilde"
0x25A0 // 7F: "Black Square" (NB: 2588 = Full Block) 0x25A0 // 7F: "Black Square" (NB: 2588 = Full Block)
}; };
// Special North American 608 CC char set. // Special North American 608 CC char set.
private static final int[] SPECIAL_CHARACTER_SET = new int[] { private static final int[] SPECIAL_CHARACTER_SET = new int[] {
0xAE, // 30: 174 '®' "Registered Sign" - registered trademark symbol 0xAE, // 30: 174 '®' "Registered Sign" - registered trademark symbol
0xB0, // 31: 176 '°' "Degree Sign" 0xB0, // 31: 176 '°' "Degree Sign"
0xBD, // 32: 189 '½' "Vulgar Fraction One Half" (1/2 symbol) 0xBD, // 32: 189 '½' "Vulgar Fraction One Half" (1/2 symbol)
0xBF, // 33: 191 '¿' "Inverted Question Mark" 0xBF, // 33: 191 '¿' "Inverted Question Mark"
0x2122, // 34: "Trade Mark Sign" (tm superscript) 0x2122, // 34: "Trade Mark Sign" (tm superscript)
0xA2, // 35: 162 '¢' "Cent Sign" 0xA2, // 35: 162 '¢' "Cent Sign"
0xA3, // 36: 163 '£' "Pound Sign" - pounds sterling 0xA3, // 36: 163 '£' "Pound Sign" - pounds sterling
0x266A, // 37: "Eighth Note" - music note 0x266A, // 37: "Eighth Note" - music note
0xE0, // 38: 224 'à' "Latin small letter A with grave" 0xE0, // 38: 224 'à' "Latin small letter A with grave"
0x20, // 39: TRANSPARENT SPACE - for now use ordinary space 0x20, // 39: TRANSPARENT SPACE - for now use ordinary space
0xE8, // 3A: 232 'è' "Latin small letter E with grave" 0xE8, // 3A: 232 'è' "Latin small letter E with grave"
0xE2, // 3B: 226 'â' "Latin small letter A with circumflex" 0xE2, // 3B: 226 'â' "Latin small letter A with circumflex"
0xEA, // 3C: 234 'ê' "Latin small letter E with circumflex" 0xEA, // 3C: 234 'ê' "Latin small letter E with circumflex"
0xEE, // 3D: 238 'î' "Latin small letter I with circumflex" 0xEE, // 3D: 238 'î' "Latin small letter I with circumflex"
0xF4, // 3E: 244 'ô' "Latin small letter O with circumflex" 0xF4, // 3E: 244 'ô' "Latin small letter O with circumflex"
0xFB // 3F: 251 'û' "Latin small letter U with circumflex" 0xFB // 3F: 251 'û' "Latin small letter U with circumflex"
}; };
// Extended Spanish/Miscellaneous and French char set. // Extended Spanish/Miscellaneous and French char set.
private static final int[] SPECIAL_ES_FR_CHARACTER_SET = new int[] { private static final int[] SPECIAL_ES_FR_CHARACTER_SET = new int[] {
// Spanish and misc. // Spanish and misc.
0xC1, 0xC9, 0xD3, 0xDA, 0xDC, 0xFC, 0x2018, 0xA1, 0xC1, 0xC9, 0xD3, 0xDA, 0xDC, 0xFC, 0x2018, 0xA1,
0x2A, 0x27, 0x2014, 0xA9, 0x2120, 0x2022, 0x201C, 0x201D, 0x2A, 0x27, 0x2014, 0xA9, 0x2120, 0x2022, 0x201C, 0x201D,
// French. // French.
0xC0, 0xC2, 0xC7, 0xC8, 0xCA, 0xCB, 0xEB, 0xCE, 0xC0, 0xC2, 0xC7, 0xC8, 0xCA, 0xCB, 0xEB, 0xCE,
0xCF, 0xEF, 0xD4, 0xD9, 0xF9, 0xDB, 0xAB, 0xBB 0xCF, 0xEF, 0xD4, 0xD9, 0xF9, 0xDB, 0xAB, 0xBB
}; };
//Extended Portuguese and German/Danish char set. //Extended Portuguese and German/Danish char set.
private static final int[] SPECIAL_PT_DE_CHARACTER_SET = new int[] { private static final int[] SPECIAL_PT_DE_CHARACTER_SET = new int[] {
// Portuguese. // Portuguese.
0xC3, 0xE3, 0xCD, 0xCC, 0xEC, 0xD2, 0xF2, 0xD5, 0xC3, 0xE3, 0xCD, 0xCC, 0xEC, 0xD2, 0xF2, 0xD5,
0xF5, 0x7B, 0x7D, 0x5C, 0x5E, 0x5F, 0x7C, 0x7E, 0xF5, 0x7B, 0x7D, 0x5C, 0x5E, 0x5F, 0x7C, 0x7E,
// German/Danish. // German/Danish.
0xC4, 0xE4, 0xD6, 0xF6, 0xDF, 0xA5, 0xA4, 0x2502, 0xC4, 0xE4, 0xD6, 0xF6, 0xDF, 0xA5, 0xA4, 0x2502,
0xC5, 0xE5, 0xD8, 0xF8, 0x250C, 0x2510, 0x2514, 0x2518 0xC5, 0xE5, 0xD8, 0xF8, 0x250C, 0x2510, 0x2514, 0x2518
}; };
private final ParsableByteArray ccData; private final ParsableByteArray ccData;
private final StringBuilder captionStringBuilder;
private final int packetLength; private final int packetLength;
private final int selectedField; private final int selectedField;
private final LinkedList<CueBuilder> cueBuilders;
private CueBuilder currentCueBuilder;
private List<Cue> cues;
private List<Cue> lastCues;
private int captionMode; private int captionMode;
private int captionRowCount; private int captionRowCount;
private String captionString;
private String lastCaptionString;
private boolean repeatableControlSet; private boolean repeatableControlSet;
private byte repeatableControlCc1; private byte repeatableControlCc1;
@ -185,8 +208,8 @@ public final class Cea608Decoder extends CeaDecoder {
public Cea608Decoder(String mimeType, int accessibilityChannel) { public Cea608Decoder(String mimeType, int accessibilityChannel) {
ccData = new ParsableByteArray(); ccData = new ParsableByteArray();
captionStringBuilder = new StringBuilder(); cueBuilders = new LinkedList<>();
currentCueBuilder = new CueBuilder(CC_MODE_UNKNOWN, DEFAULT_CAPTIONS_ROW_COUNT);
packetLength = MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) ? 2 : 3; packetLength = MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) ? 2 : 3;
switch (accessibilityChannel) { switch (accessibilityChannel) {
case 3: case 3:
@ -201,7 +224,7 @@ public final class Cea608Decoder extends CeaDecoder {
} }
setCaptionMode(CC_MODE_UNKNOWN); setCaptionMode(CC_MODE_UNKNOWN);
captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT; resetCueBuilders();
} }
@Override @Override
@ -212,11 +235,11 @@ public final class Cea608Decoder extends CeaDecoder {
@Override @Override
public void flush() { public void flush() {
super.flush(); super.flush();
cues = null;
lastCues = null;
setCaptionMode(CC_MODE_UNKNOWN); setCaptionMode(CC_MODE_UNKNOWN);
resetCueBuilders();
captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT; captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT;
captionStringBuilder.setLength(0);
captionString = null;
lastCaptionString = null;
repeatableControlSet = false; repeatableControlSet = false;
repeatableControlCc1 = 0; repeatableControlCc1 = 0;
repeatableControlCc2 = 0; repeatableControlCc2 = 0;
@ -229,13 +252,13 @@ public final class Cea608Decoder extends CeaDecoder {
@Override @Override
protected boolean isNewSubtitleDataAvailable() { protected boolean isNewSubtitleDataAvailable() {
return !TextUtils.equals(captionString, lastCaptionString); return cues != lastCues;
} }
@Override @Override
protected Subtitle createSubtitle() { protected Subtitle createSubtitle() {
lastCaptionString = captionString; lastCues = cues;
return new CeaSubtitle(new Cue(captionString)); return new CeaSubtitle(cues);
} }
@Override @Override
@ -246,10 +269,13 @@ public final class Cea608Decoder extends CeaDecoder {
while (ccData.bytesLeft() >= packetLength) { while (ccData.bytesLeft() >= packetLength) {
byte ccDataHeader = packetLength == 2 ? CC_IMPLICIT_DATA_HEADER byte ccDataHeader = packetLength == 2 ? CC_IMPLICIT_DATA_HEADER
: (byte) ccData.readUnsignedByte(); : (byte) ccData.readUnsignedByte();
byte ccData1 = (byte) (ccData.readUnsignedByte() & 0x7F); byte ccData1 = (byte) (ccData.readUnsignedByte() & 0x7F); // strip the parity bit
byte ccData2 = (byte) (ccData.readUnsignedByte() & 0x7F); byte ccData2 = (byte) (ccData.readUnsignedByte() & 0x7F); // strip the parity bit
// Only examine valid CEA-608 packets // Only examine valid CEA-608 packets
// TODO: We're currently ignoring the top 5 marker bits, which should all be 1s according
// to the CEA-608 specification. We need to determine if the data should be handled
// differently when that is not the case.
if ((ccDataHeader & (CC_VALID_FLAG | CC_TYPE_FLAG)) != CC_VALID_608_ID) { if ((ccDataHeader & (CC_VALID_FLAG | CC_TYPE_FLAG)) != CC_VALID_608_ID) {
continue; continue;
} }
@ -264,49 +290,47 @@ public final class Cea608Decoder extends CeaDecoder {
if (ccData1 == 0 && ccData2 == 0) { if (ccData1 == 0 && ccData2 == 0) {
continue; continue;
} }
// If we've reached this point then there is data to process; flag that work has been done. // If we've reached this point then there is data to process; flag that work has been done.
captionDataProcessed = true; captionDataProcessed = true;
// Special North American character set. // Special North American character set.
// ccData1 - P|0|0|1|C|0|0|1 // ccData1 - 0|0|0|1|C|0|0|1
// ccData2 - P|0|1|1|X|X|X|X // ccData2 - 0|0|1|1|X|X|X|X
if ((ccData1 == 0x11 || ccData1 == 0x19) && ((ccData2 & 0x70) == 0x30)) { if (((ccData1 & 0xF7) == 0x11) && ((ccData2 & 0xF0) == 0x30)) {
// TODO: Make use of the channel bit // TODO: Make use of the channel toggle
captionStringBuilder.append(getSpecialChar(ccData2)); currentCueBuilder.append(getSpecialChar(ccData2));
continue; continue;
} }
// Extended Western European character set. // Extended Western European character set.
// ccData1 - P|0|0|1|C|0|1|S // ccData1 - 0|0|0|1|C|0|1|S
// ccData2 - P|0|1|X|X|X|X|X // ccData2 - 0|0|1|X|X|X|X|X
if ((ccData2 & 0x60) == 0x20) { if (((ccData1 & 0xF6) == 0x12) && (ccData2 & 0xE0) == 0x20) {
// Extended Spanish/Miscellaneous and French character set (S = 0). // TODO: Make use of the channel toggle
if (ccData1 == 0x12 || ccData1 == 0x1A) { // Remove standard equivalent of the special extended char before appending new one
// TODO: Make use of the channel bit currentCueBuilder.backspace();
backspace(); // Remove standard equivalent of the special extended char. if ((ccData1 & 0x01) == 0x00) {
captionStringBuilder.append(getExtendedEsFrChar(ccData2)); // Extended Spanish/Miscellaneous and French character set (S = 0).
continue; currentCueBuilder.append(getExtendedEsFrChar(ccData2));
} } else {
// Extended Portuguese and German/Danish character set (S = 1).
// Extended Portuguese and German/Danish character set (S = 1). currentCueBuilder.append(getExtendedPtDeChar(ccData2));
if (ccData1 == 0x13 || ccData1 == 0x1B) {
// TODO: Make use of the channel bit
backspace(); // Remove standard equivalent of the special extended char.
captionStringBuilder.append(getExtendedPtDeChar(ccData2));
continue;
} }
continue;
} }
// Control character. // Control character.
if (ccData1 < 0x20) { // ccData1 - 0|0|0|X|X|X|X|X
if ((ccData1 & 0xE0) == 0x00) {
isRepeatableControl = handleCtrl(ccData1, ccData2); isRepeatableControl = handleCtrl(ccData1, ccData2);
continue; continue;
} }
// Basic North American character set. // Basic North American character set.
captionStringBuilder.append(getChar(ccData1)); currentCueBuilder.append(getChar(ccData1));
if (ccData2 >= 0x20) { if ((ccData2 & 0xE0) != 0x00) {
captionStringBuilder.append(getChar(ccData2)); currentCueBuilder.append(getChar(ccData2));
} }
} }
@ -315,34 +339,102 @@ public final class Cea608Decoder extends CeaDecoder {
repeatableControlSet = false; repeatableControlSet = false;
} }
if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) { if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) {
captionString = getDisplayCaption(); cues = getDisplayCues();
} }
} }
} }
private boolean handleCtrl(byte cc1, byte cc2) { private boolean handleCtrl(byte cc1, byte cc2) {
boolean isRepeatableControl = isRepeatable(cc1); boolean isRepeatableControl = isRepeatable(cc1);
// Most control commands are sent twice in succession to ensure they are received properly.
// We don't want to process duplicate commands, so if we see the same repeatable command twice
// in a row, ignore the second one.
if (isRepeatableControl) { if (isRepeatableControl) {
if (repeatableControlSet if (repeatableControlSet
&& repeatableControlCc1 == cc1 && repeatableControlCc1 == cc1
&& repeatableControlCc2 == cc2) { && repeatableControlCc2 == cc2) {
// This is a duplicate. Clear the repeatable control flag and return.
repeatableControlSet = false; repeatableControlSet = false;
return true; return true;
} else { } else {
// This is a repeatable command, but we haven't see it yet, so set the repeabable control
// flag (to ensure we ignore the next one should it be a duplicate) and continue processing
// the command.
repeatableControlSet = true; repeatableControlSet = true;
repeatableControlCc1 = cc1; repeatableControlCc1 = cc1;
repeatableControlCc2 = cc2; repeatableControlCc2 = cc2;
} }
} }
if (isMiscCode(cc1, cc2)) {
handleMiscCode(cc2); if (isMidrowCtrlCode(cc1, cc2)) {
handleMidrowCtrl(cc2);
} else if (isPreambleAddressCode(cc1, cc2)) { } else if (isPreambleAddressCode(cc1, cc2)) {
// TODO: Add better handling of this with specific positioning. handlePreambleAddressCode(cc1, cc2);
maybeAppendNewline(); } else if (isTabCtrlCode(cc1, cc2)) {
currentCueBuilder.tab(cc2 - 0x20);
} else if (isMiscCode(cc1, cc2)) {
handleMiscCode(cc2);
} }
return isRepeatableControl; return isRepeatableControl;
} }
private void handleMidrowCtrl(byte cc2) {
// TODO: support the extended styles (i.e. backgrounds and transparencies)
// cc2 - 0|0|1|0|ATRBT|U
// ATRBT is the 3-byte encoded attribute, and U is the underline toggle
boolean isUnderlined = (cc2 & 0x01) == 0x01;
currentCueBuilder.setUnderline(isUnderlined);
int attribute = (cc2 >> 1) & 0x0F;
if (attribute == 0x07) {
currentCueBuilder.setMidrowStyle(new StyleSpan(Typeface.ITALIC), 2);
currentCueBuilder.setMidrowStyle(new ForegroundColorSpan(Color.WHITE), 1);
} else {
currentCueBuilder.setMidrowStyle(new ForegroundColorSpan(COLORS[attribute]), 1);
}
}
private void handlePreambleAddressCode(byte cc1, byte cc2) {
// cc1 - 0|0|0|1|C|E|ROW
// C is the channel toggle, E is the extended flag, and ROW is the encoded row
int row = ROW_INDICES[cc1 & 0x07];
// TODO: Make use of the channel toggle
// TODO: support the extended address and style
// cc2 - 0|1|N|ATTRBTE|U
// N is the next row down toggle, ATTRBTE is the 4-byte encoded attribute, and U is the
// underline toggle
boolean nextRowDown = (cc2 & 0x20) != 0;
if (row != currentCueBuilder.getRow() || nextRowDown) {
if (!currentCueBuilder.isEmpty()) {
currentCueBuilder = new CueBuilder(captionMode, captionRowCount);
cueBuilders.add(currentCueBuilder);
}
currentCueBuilder.setRow(nextRowDown ? ++row : row);
}
if ((cc2 & 0x01) == 0x01) {
currentCueBuilder.setPreambleStyle(new UnderlineSpan());
}
// cc2 - 0|1|N|0|STYLE|U
// cc2 - 0|1|N|1|CURSR|U
int attribute = cc2 >> 1 & 0x0F;
if (attribute <= 0x07) {
if (attribute == 0x07) {
currentCueBuilder.setPreambleStyle(new StyleSpan(Typeface.ITALIC));
currentCueBuilder.setPreambleStyle(new ForegroundColorSpan(Color.WHITE));
} else {
currentCueBuilder.setPreambleStyle(new ForegroundColorSpan(COLORS[attribute]));
}
} else {
currentCueBuilder.setIndent(COLUMN_INDICES[attribute & 0x07]);
}
}
private void handleMiscCode(byte cc2) { private void handleMiscCode(byte cc2) {
switch (cc2) { switch (cc2) {
case CTRL_ROLL_UP_CAPTIONS_2_ROWS: case CTRL_ROLL_UP_CAPTIONS_2_ROWS:
@ -371,68 +463,40 @@ public final class Cea608Decoder extends CeaDecoder {
switch (cc2) { switch (cc2) {
case CTRL_ERASE_DISPLAYED_MEMORY: case CTRL_ERASE_DISPLAYED_MEMORY:
captionString = null; cues = null;
if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) { if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) {
captionStringBuilder.setLength(0); resetCueBuilders();
} }
break; break;
case CTRL_ERASE_NON_DISPLAYED_MEMORY: case CTRL_ERASE_NON_DISPLAYED_MEMORY:
captionStringBuilder.setLength(0); resetCueBuilders();
break; break;
case CTRL_END_OF_CAPTION: case CTRL_END_OF_CAPTION:
captionString = getDisplayCaption(); cues = getDisplayCues();
captionStringBuilder.setLength(0); resetCueBuilders();
break; break;
case CTRL_CARRIAGE_RETURN: case CTRL_CARRIAGE_RETURN:
maybeAppendNewline(); // carriage returns only apply to rollup captions; don't bother if we don't have anything
break; // to add a carriage return to
case CTRL_BACKSPACE: if (captionMode == CC_MODE_ROLL_UP && !currentCueBuilder.isEmpty()) {
if (captionStringBuilder.length() > 0) { currentCueBuilder.rollUp();
captionStringBuilder.setLength(captionStringBuilder.length() - 1);
} }
break; break;
case CTRL_BACKSPACE:
currentCueBuilder.backspace();
break;
case CTRL_DELETE_TO_END_OF_ROW:
// TODO: implement
break;
} }
} }
private void backspace() { private List<Cue> getDisplayCues() {
if (captionStringBuilder.length() > 0) { List<Cue> displayCues = new ArrayList<>();
captionStringBuilder.setLength(captionStringBuilder.length() - 1); for (int i = 0; i < cueBuilders.size(); i++) {
displayCues.add(cueBuilders.get(i).build());
} }
} return displayCues;
private void maybeAppendNewline() {
int buildLength = captionStringBuilder.length();
if (buildLength > 0 && captionStringBuilder.charAt(buildLength - 1) != '\n') {
captionStringBuilder.append('\n');
}
}
private String getDisplayCaption() {
int buildLength = captionStringBuilder.length();
if (buildLength == 0) {
return null;
}
boolean endsWithNewline = captionStringBuilder.charAt(buildLength - 1) == '\n';
if (buildLength == 1 && endsWithNewline) {
return null;
}
int endIndex = endsWithNewline ? buildLength - 1 : buildLength;
if (captionMode != CC_MODE_ROLL_UP) {
return captionStringBuilder.substring(0, endIndex);
}
int startIndex = 0;
int searchBackwardFromIndex = endIndex;
for (int i = 0; i < captionRowCount && searchBackwardFromIndex != -1; i++) {
searchBackwardFromIndex = captionStringBuilder.lastIndexOf("\n", searchBackwardFromIndex - 1);
}
if (searchBackwardFromIndex != -1) {
startIndex = searchBackwardFromIndex + 1;
}
captionStringBuilder.delete(0, startIndex);
return captionStringBuilder.substring(0, endIndex - startIndex);
} }
private void setCaptionMode(int captionMode) { private void setCaptionMode(int captionMode) {
@ -442,20 +506,26 @@ public final class Cea608Decoder extends CeaDecoder {
this.captionMode = captionMode; this.captionMode = captionMode;
// Clear the working memory. // Clear the working memory.
captionStringBuilder.setLength(0); resetCueBuilders();
if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_UNKNOWN) { if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_UNKNOWN) {
// When switching to roll-up or unknown, we also need to clear the caption. // When switching to roll-up or unknown, we also need to clear the caption.
captionString = null; cues = null;
} }
} }
private void resetCueBuilders() {
currentCueBuilder.reset(captionMode, captionRowCount);
cueBuilders.clear();
cueBuilders.add(currentCueBuilder);
}
private static char getChar(byte ccData) { private static char getChar(byte ccData) {
int index = (ccData & 0x7F) - 0x20; int index = (ccData & 0x7F) - 0x20;
return (char) BASIC_CHARACTER_SET[index]; return (char) BASIC_CHARACTER_SET[index];
} }
private static char getSpecialChar(byte ccData) { private static char getSpecialChar(byte ccData) {
int index = ccData & 0xF; int index = ccData & 0x0F;
return (char) SPECIAL_CHARACTER_SET[index]; return (char) SPECIAL_CHARACTER_SET[index];
} }
@ -469,17 +539,33 @@ public final class Cea608Decoder extends CeaDecoder {
return (char) SPECIAL_PT_DE_CHARACTER_SET[index]; return (char) SPECIAL_PT_DE_CHARACTER_SET[index];
} }
private static boolean isMiscCode(byte cc1, byte cc2) { private static boolean isMidrowCtrlCode(byte cc1, byte cc2) {
return (cc1 == CTRL_MISC_CHAN_1 || cc1 == CTRL_MISC_CHAN_2) // cc1 - 0|0|0|1|C|0|0|1
&& (cc2 >= 0x20 && cc2 <= 0x2F); // cc2 - 0|0|1|0|X|X|X|X
return ((cc1 & 0xF7) == 0x11) && ((cc2 & 0xF0) == 0x20);
} }
private static boolean isPreambleAddressCode(byte cc1, byte cc2) { private static boolean isPreambleAddressCode(byte cc1, byte cc2) {
return (cc1 >= 0x10 && cc1 <= 0x1F) && (cc2 >= 0x40 && cc2 <= 0x7F); // cc1 - 0|0|0|1|C|X|X|X
// cc2 - 0|1|X|X|X|X|X|X
return ((cc1 & 0xF0) == 0x10) && ((cc2 & 0xC0) == 0x80);
}
private static boolean isTabCtrlCode(byte cc1, byte cc2) {
// cc1 - 0|0|0|1|C|1|1|1
// cc2 - 0|0|1|0|0|0|0|1 to 0|0|1|0|0|0|1|1
return ((cc1 & 0xF7) == 0x17) && (cc2 >= 0x21 && cc2 <= 0x23);
}
private static boolean isMiscCode(byte cc1, byte cc2) {
// cc1 - 0|0|0|1|C|1|0|0
// cc2 - 0|0|1|0|X|X|X|X
return ((cc1 & 0xF7) == 0x14) && ((cc2 & 0xF0) == 0x20);
} }
private static boolean isRepeatable(byte cc1) { private static boolean isRepeatable(byte cc1) {
return cc1 >= 0x10 && cc1 <= 0x1F; // cc1 - 0|0|0|1|X|X|X|X
return (cc1 & 0xF0) == 0x10;
} }
/** /**
@ -507,4 +593,182 @@ public final class Cea608Decoder extends CeaDecoder {
&& userIdentifier == USER_ID && userDataTypeCode == USER_DATA_TYPE_CODE; && userIdentifier == USER_ID && userDataTypeCode == USER_DATA_TYPE_CODE;
} }
private static class CueBuilder {
private static final int POSITION_UNSET = -1;
// 608 captions define a 15 row by 32 column screen grid. These constants convert from 608
// positions to normalized screen position.
private static final int SCREEN_CHARWIDTH = 32;
private static final int BASE_ROW = 15;
private final List<CharacterStyle> preambleStyles;
private final List<CueStyle> midrowStyles;
private final List<SpannableString> rolledUpCaptions;
private final SpannableStringBuilder captionStringBuilder;
private int row;
private int indent;
private int tabOffset;
private int captionMode;
private int captionRowCount;
private int underlineStartPosition;
public CueBuilder(int captionMode, int captionRowCount) {
preambleStyles = new ArrayList<>();
midrowStyles = new ArrayList<>();
rolledUpCaptions = new LinkedList<>();
captionStringBuilder = new SpannableStringBuilder();
reset(captionMode, captionRowCount);
}
public void reset(int captionMode, int captionRowCount) {
preambleStyles.clear();
midrowStyles.clear();
rolledUpCaptions.clear();
captionStringBuilder.clear();
row = BASE_ROW;
indent = 0;
tabOffset = 0;
this.captionMode = captionMode;
this.captionRowCount = captionRowCount;
underlineStartPosition = POSITION_UNSET;
}
public boolean isEmpty() {
return preambleStyles.isEmpty() && midrowStyles.isEmpty() && rolledUpCaptions.isEmpty()
&& captionStringBuilder.length() == 0;
}
public void backspace() {
int length = captionStringBuilder.length();
if (length > 0) {
captionStringBuilder.delete(length - 1, length);
}
}
public int getRow() {
return row;
}
public void setRow(int row) {
this.row = row;
}
public void rollUp() {
rolledUpCaptions.add(buildSpannableString());
captionStringBuilder.clear();
preambleStyles.clear();
midrowStyles.clear();
underlineStartPosition = POSITION_UNSET;
int numRows = Math.min(captionRowCount, row);
while (rolledUpCaptions.size() >= numRows) {
rolledUpCaptions.remove(0);
}
}
public void setIndent(int indent) {
this.indent = indent;
}
public void tab(int tabs) {
tabOffset += tabs;
}
public void setPreambleStyle(CharacterStyle style) {
preambleStyles.add(style);
}
public void setMidrowStyle(CharacterStyle style, int nextStyleIncrement) {
midrowStyles.add(new CueStyle(style, captionStringBuilder.length(), nextStyleIncrement));
}
public void setUnderline(boolean enabled) {
if (enabled) {
underlineStartPosition = captionStringBuilder.length();
} else if (underlineStartPosition != POSITION_UNSET) {
// underline spans won't overlap, so it's safe to modify the builder directly with them
captionStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition,
captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
underlineStartPosition = POSITION_UNSET;
}
}
public void append(char text) {
captionStringBuilder.append(text);
}
public SpannableString buildSpannableString() {
int length = captionStringBuilder.length();
// preamble styles apply to the entire cue
for (int i = 0; i < preambleStyles.size(); i++) {
captionStringBuilder.setSpan(preambleStyles.get(i), 0, length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
// midrow styles only apply to part of the cue, and after preamble styles
for (int i = 0; i < midrowStyles.size(); i++) {
CueStyle cueStyle = midrowStyles.get(i);
int end = (i < midrowStyles.size() - cueStyle.nextStyleIncrement)
? midrowStyles.get(i + cueStyle.nextStyleIncrement).start
: length;
captionStringBuilder.setSpan(cueStyle.style, cueStyle.start, end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
// special case for midrow underlines that went to the end of the cue
if (underlineStartPosition != POSITION_UNSET) {
captionStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition, length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
return new SpannableString(captionStringBuilder);
}
public Cue build() {
SpannableStringBuilder cueString = new SpannableStringBuilder();
// add any rolled up captions, separated by new lines
for (int i = 0; i < rolledUpCaptions.size(); i++) {
cueString.append(rolledUpCaptions.get(i));
cueString.append('\n');
}
// add the current line
cueString.append(buildSpannableString());
float position = (float) (indent + tabOffset) / SCREEN_CHARWIDTH;
float line;
int lineType;
if (captionMode == CC_MODE_ROLL_UP) {
line = (row - 1) - BASE_ROW;
lineType = Cue.LINE_TYPE_NUMBER;
} else {
line = (float) (row - 1) / BASE_ROW;
lineType = Cue.LINE_TYPE_FRACTION;
}
return new Cue(cueString, Alignment.ALIGN_NORMAL, line, lineType, TYPE_UNSET, position,
TYPE_UNSET, 0.8f);
}
private static class CueStyle {
public final CharacterStyle style;
public final int start;
public final int nextStyleIncrement;
public CueStyle(CharacterStyle style, int start, int nextStyleIncrement) {
this.style = style;
this.start = start;
this.nextStyleIncrement = nextStyleIncrement;
}
}
}
} }

View File

@ -17,7 +17,6 @@ package com.google.android.exoplayer2.text.cea;
import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.Subtitle;
import java.util.Collections;
import java.util.List; import java.util.List;
/** /**
@ -28,14 +27,10 @@ import java.util.List;
private final List<Cue> cues; private final List<Cue> cues;
/** /**
* @param cue The subtitle cue. * @param cues The subtitle cues.
*/ */
public CeaSubtitle(Cue cue) { public CeaSubtitle(List<Cue> cues) {
if (cue == null) { this.cues = cues;
cues = Collections.emptyList();
} else {
cues = Collections.singletonList(cue);
}
} }
@Override @Override
@ -56,7 +51,6 @@ import java.util.List;
@Override @Override
public List<Cue> getCues(long timeUs) { public List<Cue> getCues(long timeUs) {
return cues; return cues;
} }
} }

View File

@ -233,7 +233,7 @@ import com.google.android.exoplayer2.util.Util;
int anchorPosition = Math.round(parentWidth * cuePosition) + parentLeft; int anchorPosition = Math.round(parentWidth * cuePosition) + parentLeft;
textLeft = cuePositionAnchor == Cue.ANCHOR_TYPE_END ? anchorPosition - textWidth textLeft = cuePositionAnchor == Cue.ANCHOR_TYPE_END ? anchorPosition - textWidth
: cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorPosition * 2 - textWidth) / 2 : cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorPosition * 2 - textWidth) / 2
: anchorPosition; : anchorPosition;
textLeft = Math.max(textLeft, parentLeft); textLeft = Math.max(textLeft, parentLeft);
textRight = Math.min(textLeft + textWidth, parentRight); textRight = Math.min(textLeft + textWidth, parentRight);
} else { } else {
@ -257,7 +257,7 @@ import com.google.android.exoplayer2.util.Util;
} }
textTop = cueLineAnchor == Cue.ANCHOR_TYPE_END ? anchorPosition - textHeight textTop = cueLineAnchor == Cue.ANCHOR_TYPE_END ? anchorPosition - textHeight
: cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorPosition * 2 - textHeight) / 2 : cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorPosition * 2 - textHeight) / 2
: anchorPosition; : anchorPosition;
if (textTop + textHeight > parentBottom) { if (textTop + textHeight > parentBottom) {
textTop = parentBottom - textHeight; textTop = parentBottom - textHeight;
} else if (textTop < parentTop) { } else if (textTop < parentTop) {