diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java index 2a12679a0b..10f59bde6e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java @@ -15,7 +15,18 @@ */ 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.text.Cue; 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.util.MimeTypes; 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"). */ 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_TYPE_FLAG = 0x02; 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_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. 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_CARRIAGE_RETURN = 0x2D; 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_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). private static final int[] BASIC_CHARACTER_SET = new int[] { - 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, // ! " # $ % & ' - 0x28, 0x29, // ( ) - 0xE1, // 2A: 225 'á' "Latin small letter A with acute" - 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, // + , - . / - 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 : ; < = > ? - 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 - 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, // P Q R S T U V W - 0x58, 0x59, 0x5A, 0x5B, // X Y Z [ - 0xE9, // 5C: 233 'é' "Latin small letter E with acute" - 0x5D, // ] - 0xED, // 5E: 237 'í' "Latin small letter I with acute" - 0xF3, // 5F: 243 'ó' "Latin small letter O 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 - 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 - 0x78, 0x79, 0x7A, // x y z - 0xE7, // 7B: 231 'ç' "Latin small letter C with cedilla" - 0xF7, // 7C: 247 '÷' "Division sign" - 0xD1, // 7D: 209 'Ñ' "Latin capital letter N with tilde" - 0xF1, // 7E: 241 'ñ' "Latin small letter N with tilde" - 0x25A0 // 7F: "Black Square" (NB: 2588 = Full Block) + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, // ! " # $ % & ' + 0x28, 0x29, // ( ) + 0xE1, // 2A: 225 'á' "Latin small letter A with acute" + 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, // + , - . / + 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 : ; < = > ? + 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 + 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, // P Q R S T U V W + 0x58, 0x59, 0x5A, 0x5B, // X Y Z [ + 0xE9, // 5C: 233 'é' "Latin small letter E with acute" + 0x5D, // ] + 0xED, // 5E: 237 'í' "Latin small letter I with acute" + 0xF3, // 5F: 243 'ó' "Latin small letter O 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 + 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 + 0x78, 0x79, 0x7A, // x y z + 0xE7, // 7B: 231 'ç' "Latin small letter C with cedilla" + 0xF7, // 7C: 247 '÷' "Division sign" + 0xD1, // 7D: 209 'Ñ' "Latin capital letter N with tilde" + 0xF1, // 7E: 241 'ñ' "Latin small letter N with tilde" + 0x25A0 // 7F: "Black Square" (NB: 2588 = Full Block) }; // Special North American 608 CC char set. private static final int[] SPECIAL_CHARACTER_SET = new int[] { - 0xAE, // 30: 174 '®' "Registered Sign" - registered trademark symbol - 0xB0, // 31: 176 '°' "Degree Sign" - 0xBD, // 32: 189 '½' "Vulgar Fraction One Half" (1/2 symbol) - 0xBF, // 33: 191 '¿' "Inverted Question Mark" - 0x2122, // 34: "Trade Mark Sign" (tm superscript) - 0xA2, // 35: 162 '¢' "Cent Sign" - 0xA3, // 36: 163 '£' "Pound Sign" - pounds sterling - 0x266A, // 37: "Eighth Note" - music note - 0xE0, // 38: 224 'à' "Latin small letter A with grave" - 0x20, // 39: TRANSPARENT SPACE - for now use ordinary space - 0xE8, // 3A: 232 'è' "Latin small letter E with grave" - 0xE2, // 3B: 226 'â' "Latin small letter A with circumflex" - 0xEA, // 3C: 234 'ê' "Latin small letter E with circumflex" - 0xEE, // 3D: 238 'î' "Latin small letter I with circumflex" - 0xF4, // 3E: 244 'ô' "Latin small letter O with circumflex" - 0xFB // 3F: 251 'û' "Latin small letter U with circumflex" + 0xAE, // 30: 174 '®' "Registered Sign" - registered trademark symbol + 0xB0, // 31: 176 '°' "Degree Sign" + 0xBD, // 32: 189 '½' "Vulgar Fraction One Half" (1/2 symbol) + 0xBF, // 33: 191 '¿' "Inverted Question Mark" + 0x2122, // 34: "Trade Mark Sign" (tm superscript) + 0xA2, // 35: 162 '¢' "Cent Sign" + 0xA3, // 36: 163 '£' "Pound Sign" - pounds sterling + 0x266A, // 37: "Eighth Note" - music note + 0xE0, // 38: 224 'à' "Latin small letter A with grave" + 0x20, // 39: TRANSPARENT SPACE - for now use ordinary space + 0xE8, // 3A: 232 'è' "Latin small letter E with grave" + 0xE2, // 3B: 226 'â' "Latin small letter A with circumflex" + 0xEA, // 3C: 234 'ê' "Latin small letter E with circumflex" + 0xEE, // 3D: 238 'î' "Latin small letter I with circumflex" + 0xF4, // 3E: 244 'ô' "Latin small letter O with circumflex" + 0xFB // 3F: 251 'û' "Latin small letter U with circumflex" }; // Extended Spanish/Miscellaneous and French char set. private static final int[] SPECIAL_ES_FR_CHARACTER_SET = new int[] { - // Spanish and misc. - 0xC1, 0xC9, 0xD3, 0xDA, 0xDC, 0xFC, 0x2018, 0xA1, - 0x2A, 0x27, 0x2014, 0xA9, 0x2120, 0x2022, 0x201C, 0x201D, - // French. - 0xC0, 0xC2, 0xC7, 0xC8, 0xCA, 0xCB, 0xEB, 0xCE, - 0xCF, 0xEF, 0xD4, 0xD9, 0xF9, 0xDB, 0xAB, 0xBB + // Spanish and misc. + 0xC1, 0xC9, 0xD3, 0xDA, 0xDC, 0xFC, 0x2018, 0xA1, + 0x2A, 0x27, 0x2014, 0xA9, 0x2120, 0x2022, 0x201C, 0x201D, + // French. + 0xC0, 0xC2, 0xC7, 0xC8, 0xCA, 0xCB, 0xEB, 0xCE, + 0xCF, 0xEF, 0xD4, 0xD9, 0xF9, 0xDB, 0xAB, 0xBB }; //Extended Portuguese and German/Danish char set. private static final int[] SPECIAL_PT_DE_CHARACTER_SET = new int[] { - // Portuguese. - 0xC3, 0xE3, 0xCD, 0xCC, 0xEC, 0xD2, 0xF2, 0xD5, - 0xF5, 0x7B, 0x7D, 0x5C, 0x5E, 0x5F, 0x7C, 0x7E, - // German/Danish. - 0xC4, 0xE4, 0xD6, 0xF6, 0xDF, 0xA5, 0xA4, 0x2502, - 0xC5, 0xE5, 0xD8, 0xF8, 0x250C, 0x2510, 0x2514, 0x2518 + // Portuguese. + 0xC3, 0xE3, 0xCD, 0xCC, 0xEC, 0xD2, 0xF2, 0xD5, + 0xF5, 0x7B, 0x7D, 0x5C, 0x5E, 0x5F, 0x7C, 0x7E, + // German/Danish. + 0xC4, 0xE4, 0xD6, 0xF6, 0xDF, 0xA5, 0xA4, 0x2502, + 0xC5, 0xE5, 0xD8, 0xF8, 0x250C, 0x2510, 0x2514, 0x2518 }; private final ParsableByteArray ccData; - private final StringBuilder captionStringBuilder; private final int packetLength; private final int selectedField; + private final LinkedList cueBuilders; + + private CueBuilder currentCueBuilder; + private List cues; + private List lastCues; private int captionMode; private int captionRowCount; - private String captionString; - - private String lastCaptionString; private boolean repeatableControlSet; private byte repeatableControlCc1; @@ -185,8 +208,8 @@ public final class Cea608Decoder extends CeaDecoder { public Cea608Decoder(String mimeType, int accessibilityChannel) { 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; switch (accessibilityChannel) { case 3: @@ -201,7 +224,7 @@ public final class Cea608Decoder extends CeaDecoder { } setCaptionMode(CC_MODE_UNKNOWN); - captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT; + resetCueBuilders(); } @Override @@ -212,11 +235,11 @@ public final class Cea608Decoder extends CeaDecoder { @Override public void flush() { super.flush(); + cues = null; + lastCues = null; setCaptionMode(CC_MODE_UNKNOWN); + resetCueBuilders(); captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT; - captionStringBuilder.setLength(0); - captionString = null; - lastCaptionString = null; repeatableControlSet = false; repeatableControlCc1 = 0; repeatableControlCc2 = 0; @@ -229,13 +252,13 @@ public final class Cea608Decoder extends CeaDecoder { @Override protected boolean isNewSubtitleDataAvailable() { - return !TextUtils.equals(captionString, lastCaptionString); + return cues != lastCues; } @Override protected Subtitle createSubtitle() { - lastCaptionString = captionString; - return new CeaSubtitle(new Cue(captionString)); + lastCues = cues; + return new CeaSubtitle(cues); } @Override @@ -246,10 +269,13 @@ public final class Cea608Decoder extends CeaDecoder { while (ccData.bytesLeft() >= packetLength) { byte ccDataHeader = packetLength == 2 ? CC_IMPLICIT_DATA_HEADER : (byte) ccData.readUnsignedByte(); - byte ccData1 = (byte) (ccData.readUnsignedByte() & 0x7F); - byte ccData2 = (byte) (ccData.readUnsignedByte() & 0x7F); + byte ccData1 = (byte) (ccData.readUnsignedByte() & 0x7F); // strip the parity bit + byte ccData2 = (byte) (ccData.readUnsignedByte() & 0x7F); // strip the parity bit // 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) { continue; } @@ -264,49 +290,47 @@ public final class Cea608Decoder extends CeaDecoder { if (ccData1 == 0 && ccData2 == 0) { continue; } + // If we've reached this point then there is data to process; flag that work has been done. captionDataProcessed = true; // Special North American character set. - // ccData1 - P|0|0|1|C|0|0|1 - // ccData2 - P|0|1|1|X|X|X|X - if ((ccData1 == 0x11 || ccData1 == 0x19) && ((ccData2 & 0x70) == 0x30)) { - // TODO: Make use of the channel bit - captionStringBuilder.append(getSpecialChar(ccData2)); + // ccData1 - 0|0|0|1|C|0|0|1 + // ccData2 - 0|0|1|1|X|X|X|X + if (((ccData1 & 0xF7) == 0x11) && ((ccData2 & 0xF0) == 0x30)) { + // TODO: Make use of the channel toggle + currentCueBuilder.append(getSpecialChar(ccData2)); continue; } // Extended Western European character set. - // ccData1 - P|0|0|1|C|0|1|S - // ccData2 - P|0|1|X|X|X|X|X - if ((ccData2 & 0x60) == 0x20) { - // Extended Spanish/Miscellaneous and French character set (S = 0). - if (ccData1 == 0x12 || ccData1 == 0x1A) { - // TODO: Make use of the channel bit - backspace(); // Remove standard equivalent of the special extended char. - captionStringBuilder.append(getExtendedEsFrChar(ccData2)); - continue; - } - - // Extended Portuguese and German/Danish character set (S = 1). - 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; + // ccData1 - 0|0|0|1|C|0|1|S + // ccData2 - 0|0|1|X|X|X|X|X + if (((ccData1 & 0xF6) == 0x12) && (ccData2 & 0xE0) == 0x20) { + // TODO: Make use of the channel toggle + // Remove standard equivalent of the special extended char before appending new one + currentCueBuilder.backspace(); + if ((ccData1 & 0x01) == 0x00) { + // Extended Spanish/Miscellaneous and French character set (S = 0). + currentCueBuilder.append(getExtendedEsFrChar(ccData2)); + } else { + // Extended Portuguese and German/Danish character set (S = 1). + currentCueBuilder.append(getExtendedPtDeChar(ccData2)); } + continue; } // Control character. - if (ccData1 < 0x20) { + // ccData1 - 0|0|0|X|X|X|X|X + if ((ccData1 & 0xE0) == 0x00) { isRepeatableControl = handleCtrl(ccData1, ccData2); continue; } // Basic North American character set. - captionStringBuilder.append(getChar(ccData1)); - if (ccData2 >= 0x20) { - captionStringBuilder.append(getChar(ccData2)); + currentCueBuilder.append(getChar(ccData1)); + if ((ccData2 & 0xE0) != 0x00) { + currentCueBuilder.append(getChar(ccData2)); } } @@ -315,34 +339,102 @@ public final class Cea608Decoder extends CeaDecoder { repeatableControlSet = false; } if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) { - captionString = getDisplayCaption(); + cues = getDisplayCues(); } } } private boolean handleCtrl(byte cc1, byte cc2) { 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 (repeatableControlSet && repeatableControlCc1 == cc1 && repeatableControlCc2 == cc2) { + // This is a duplicate. Clear the repeatable control flag and return. repeatableControlSet = false; return true; } 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; repeatableControlCc1 = cc1; repeatableControlCc2 = cc2; } } - if (isMiscCode(cc1, cc2)) { - handleMiscCode(cc2); + + if (isMidrowCtrlCode(cc1, cc2)) { + handleMidrowCtrl(cc2); } else if (isPreambleAddressCode(cc1, cc2)) { - // TODO: Add better handling of this with specific positioning. - maybeAppendNewline(); + handlePreambleAddressCode(cc1, cc2); + } else if (isTabCtrlCode(cc1, cc2)) { + currentCueBuilder.tab(cc2 - 0x20); + } else if (isMiscCode(cc1, cc2)) { + handleMiscCode(cc2); } + 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) { switch (cc2) { case CTRL_ROLL_UP_CAPTIONS_2_ROWS: @@ -371,68 +463,40 @@ public final class Cea608Decoder extends CeaDecoder { switch (cc2) { case CTRL_ERASE_DISPLAYED_MEMORY: - captionString = null; + cues = null; if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) { - captionStringBuilder.setLength(0); + resetCueBuilders(); } break; case CTRL_ERASE_NON_DISPLAYED_MEMORY: - captionStringBuilder.setLength(0); + resetCueBuilders(); break; case CTRL_END_OF_CAPTION: - captionString = getDisplayCaption(); - captionStringBuilder.setLength(0); + cues = getDisplayCues(); + resetCueBuilders(); break; case CTRL_CARRIAGE_RETURN: - maybeAppendNewline(); - break; - case CTRL_BACKSPACE: - if (captionStringBuilder.length() > 0) { - captionStringBuilder.setLength(captionStringBuilder.length() - 1); + // carriage returns only apply to rollup captions; don't bother if we don't have anything + // to add a carriage return to + if (captionMode == CC_MODE_ROLL_UP && !currentCueBuilder.isEmpty()) { + currentCueBuilder.rollUp(); } break; + case CTRL_BACKSPACE: + currentCueBuilder.backspace(); + break; + case CTRL_DELETE_TO_END_OF_ROW: + // TODO: implement + break; } } - private void backspace() { - if (captionStringBuilder.length() > 0) { - captionStringBuilder.setLength(captionStringBuilder.length() - 1); + private List getDisplayCues() { + List displayCues = new ArrayList<>(); + for (int i = 0; i < cueBuilders.size(); i++) { + displayCues.add(cueBuilders.get(i).build()); } - } - - 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); + return displayCues; } private void setCaptionMode(int captionMode) { @@ -442,20 +506,26 @@ public final class Cea608Decoder extends CeaDecoder { this.captionMode = captionMode; // Clear the working memory. - captionStringBuilder.setLength(0); + resetCueBuilders(); if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_UNKNOWN) { // 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) { int index = (ccData & 0x7F) - 0x20; return (char) BASIC_CHARACTER_SET[index]; } private static char getSpecialChar(byte ccData) { - int index = ccData & 0xF; + int index = ccData & 0x0F; return (char) SPECIAL_CHARACTER_SET[index]; } @@ -469,17 +539,33 @@ public final class Cea608Decoder extends CeaDecoder { return (char) SPECIAL_PT_DE_CHARACTER_SET[index]; } - private static boolean isMiscCode(byte cc1, byte cc2) { - return (cc1 == CTRL_MISC_CHAN_1 || cc1 == CTRL_MISC_CHAN_2) - && (cc2 >= 0x20 && cc2 <= 0x2F); + private static boolean isMidrowCtrlCode(byte cc1, byte cc2) { + // cc1 - 0|0|0|1|C|0|0|1 + // cc2 - 0|0|1|0|X|X|X|X + return ((cc1 & 0xF7) == 0x11) && ((cc2 & 0xF0) == 0x20); } 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) { - 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; } + 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 preambleStyles; + private final List midrowStyles; + private final List 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; + } + + } + + } + } diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaSubtitle.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaSubtitle.java index 5becefe106..620b2c7d80 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaSubtitle.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaSubtitle.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.text.cea; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Subtitle; -import java.util.Collections; import java.util.List; /** @@ -28,14 +27,10 @@ import java.util.List; private final List cues; /** - * @param cue The subtitle cue. + * @param cues The subtitle cues. */ - public CeaSubtitle(Cue cue) { - if (cue == null) { - cues = Collections.emptyList(); - } else { - cues = Collections.singletonList(cue); - } + public CeaSubtitle(List cues) { + this.cues = cues; } @Override @@ -56,7 +51,6 @@ import java.util.List; @Override public List getCues(long timeUs) { return cues; - } } diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java index 8c3ac77cb2..6b26dc12c1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java @@ -233,7 +233,7 @@ import com.google.android.exoplayer2.util.Util; int anchorPosition = Math.round(parentWidth * cuePosition) + parentLeft; textLeft = cuePositionAnchor == Cue.ANCHOR_TYPE_END ? anchorPosition - textWidth : cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorPosition * 2 - textWidth) / 2 - : anchorPosition; + : anchorPosition; textLeft = Math.max(textLeft, parentLeft); textRight = Math.min(textLeft + textWidth, parentRight); } else { @@ -257,7 +257,7 @@ import com.google.android.exoplayer2.util.Util; } textTop = cueLineAnchor == Cue.ANCHOR_TYPE_END ? anchorPosition - textHeight : cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorPosition * 2 - textHeight) / 2 - : anchorPosition; + : anchorPosition; if (textTop + textHeight > parentBottom) { textTop = parentBottom - textHeight; } else if (textTop < parentTop) {