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,12 +118,10 @@ 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, // ! " # $ % & '
@ -169,15 +191,16 @@ public final class Cea608Decoder extends CeaDecoder {
}; };
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) {
// 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). // Extended Spanish/Miscellaneous and French character set (S = 0).
if (ccData1 == 0x12 || ccData1 == 0x1A) { currentCueBuilder.append(getExtendedEsFrChar(ccData2));
// TODO: Make use of the channel bit } else {
backspace(); // Remove standard equivalent of the special extended char.
captionStringBuilder.append(getExtendedEsFrChar(ccData2));
continue;
}
// Extended Portuguese and German/Danish character set (S = 1). // Extended Portuguese and German/Danish character set (S = 1).
if (ccData1 == 0x13 || ccData1 == 0x1B) { currentCueBuilder.append(getExtendedPtDeChar(ccData2));
// 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
// to add a carriage return to
if (captionMode == CC_MODE_ROLL_UP && !currentCueBuilder.isEmpty()) {
currentCueBuilder.rollUp();
}
break; break;
case CTRL_BACKSPACE: case CTRL_BACKSPACE:
if (captionStringBuilder.length() > 0) { currentCueBuilder.backspace();
captionStringBuilder.setLength(captionStringBuilder.length() - 1); break;
} case CTRL_DELETE_TO_END_OF_ROW:
// TODO: implement
break; 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;
} }
} }