mirror of
https://github.com/samsonjs/media.git
synced 2026-03-31 10:25:48 +00:00
Caption lines as separate cues
This commit is contained in:
parent
7cd94819c7
commit
71d83f3e84
4 changed files with 298 additions and 243 deletions
|
|
@ -0,0 +1,203 @@
|
|||
package com.google.android.exoplayer2.text.eia608;
|
||||
|
||||
import com.google.android.exoplayer2.text.Cue;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
|
||||
import android.text.Layout;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.CharacterStyle;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.StyleSpan;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A Builder for EIA-608 cues.
|
||||
*/
|
||||
/* package */ final class Eia608CueBuilder {
|
||||
|
||||
private static final int BASE_ROW = 15;
|
||||
|
||||
/**
|
||||
* The caption string.
|
||||
*/
|
||||
private SpannableStringBuilder captionStringBuilder;
|
||||
|
||||
/**
|
||||
* The caption styles to apply to the caption string.
|
||||
*/
|
||||
private HashMap<Integer, CharacterStyle> captionStyles;
|
||||
|
||||
/**
|
||||
* The row on which the Cue should be displayed.
|
||||
*/
|
||||
private int row;
|
||||
|
||||
/**
|
||||
* The indent of the cue - horizontal positioning.
|
||||
*/
|
||||
private int indent;
|
||||
|
||||
/**
|
||||
* The setTabOffset offset for the cue.
|
||||
*/
|
||||
private int tabOffset;
|
||||
|
||||
public Eia608CueBuilder() {
|
||||
row = BASE_ROW;
|
||||
indent = 0;
|
||||
tabOffset = 0;
|
||||
captionStringBuilder = new SpannableStringBuilder();
|
||||
captionStyles = new HashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the row for this cue.
|
||||
* @param row the row to set.
|
||||
*/
|
||||
public void setRow(int row) {
|
||||
Assertions.checkArgument(row >= 1 && row <= 15);
|
||||
this.row = row;
|
||||
}
|
||||
|
||||
public int getRow() {
|
||||
return row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rolls up the Cue one row.
|
||||
* @return true if rolling was possible.
|
||||
*/
|
||||
public boolean rollUp() {
|
||||
if (row < 1) {
|
||||
return false;
|
||||
}
|
||||
setRow(row - 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the indent for this cue.
|
||||
* @param indent an indent value, must be a multiple of 4 within the range [0,28]
|
||||
*/
|
||||
public void setIndent(int indent) {
|
||||
Assertions.checkArgument(indent % 4 == 0 && indent <= 28);
|
||||
this.indent = indent;
|
||||
}
|
||||
|
||||
public void tab(int tabs) {
|
||||
tabOffset += tabs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indents the cue position with amountOfTabs.
|
||||
* @param tabOffset the amount of tabs the cue position should be indented.
|
||||
*/
|
||||
public void setTabOffset(int tabOffset) {
|
||||
this.tabOffset = tabOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a character to the current Cue.
|
||||
* @param character the character to append.
|
||||
*/
|
||||
public void append(char character) {
|
||||
captionStringBuilder.append(character);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the last character of the caption string.
|
||||
*/
|
||||
public void backspace() {
|
||||
if (captionStringBuilder.length() > 0) {
|
||||
captionStringBuilder.delete(captionStringBuilder.length() - 1, captionStringBuilder.length());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a character style at the current cueIndex.
|
||||
* Takes care of style priorities.
|
||||
*
|
||||
* @param style the style to set.
|
||||
*/
|
||||
public void setCharacterStyle(CharacterStyle style) {
|
||||
int startIndex = getSpanStartIndex();
|
||||
// Close all open spans of the same type, and add a new one.
|
||||
if (style instanceof ForegroundColorSpan) {
|
||||
// Setting a foreground color clears the italics style.
|
||||
closeSpan(StyleSpan.class); // Italics is a style span.
|
||||
}
|
||||
closeSpan(style.getClass());
|
||||
captionStyles.put(startIndex, style);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes all open spans of the spansToApply class.
|
||||
* @param spansToClose the class of which the spans should be closed.
|
||||
*/
|
||||
private void closeSpan(Class<? extends CharacterStyle> spansToClose) {
|
||||
for (Integer index : captionStyles.keySet()) {
|
||||
CharacterStyle style = captionStyles.get(index);
|
||||
if (spansToClose.isInstance(style)) {
|
||||
if (index < captionStringBuilder.length()) {
|
||||
captionStringBuilder.setSpan(style, index,
|
||||
captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
captionStyles.remove(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies all currently opened spans to the SpannableStringBuilder.
|
||||
*/
|
||||
public void closeSpans() {
|
||||
// Check if we have to do anything.
|
||||
if (captionStyles.size() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Integer startIndex : captionStyles.keySet()) {
|
||||
// There may be cases, e.g. when seeking where the startIndex becomes greater
|
||||
// than what is actually in the string builder, in that case, just discard the span.
|
||||
if (startIndex < captionStringBuilder.length()) {
|
||||
CharacterStyle captionStyle = captionStyles.get(startIndex);
|
||||
captionStringBuilder.setSpan(captionStyle, startIndex,
|
||||
captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
captionStyles.remove(startIndex);
|
||||
}
|
||||
}
|
||||
|
||||
public Cue build() {
|
||||
closeSpans();
|
||||
float cueLine = 10 + (5.33f * row);
|
||||
float cuePosition = 10 + (2.5f * indent);
|
||||
cuePosition = (tabOffset * 2.5f) + cuePosition;
|
||||
return new Cue(captionStringBuilder, Layout.Alignment.ALIGN_NORMAL, cueLine / 100, Cue.LINE_TYPE_FRACTION,
|
||||
Cue.ANCHOR_TYPE_START, cuePosition / 100, Cue.TYPE_UNSET, Cue.DIMEN_UNSET);
|
||||
}
|
||||
|
||||
private int getSpanStartIndex() {
|
||||
return captionStringBuilder.length() > 0 ? captionStringBuilder.length() - 1 : 0;
|
||||
}
|
||||
|
||||
public static List<Cue> buildCues(List<Eia608CueBuilder> builders) {
|
||||
if (builders.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
LinkedList<Cue> cues = new LinkedList<>();
|
||||
for (Eia608CueBuilder builder : builders) {
|
||||
if (builder.captionStringBuilder.length() == 0) {
|
||||
continue;
|
||||
}
|
||||
cues.add(builder.build());
|
||||
}
|
||||
return cues;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -26,16 +26,12 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
|||
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
import android.text.Layout;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.BackgroundColorSpan;
|
||||
import android.text.style.CharacterStyle;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.text.style.UnderlineSpan;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.TreeSet;
|
||||
|
||||
|
|
@ -98,6 +94,7 @@ public final class Eia608Decoder implements SubtitleDecoder {
|
|||
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_TAB_OFFSET_CHAN_1 = 0x17;
|
||||
private static final byte CTRL_TAB_OFFSET_CHAN_2 = 0x1F;
|
||||
|
|
@ -175,33 +172,13 @@ public final class Eia608Decoder implements SubtitleDecoder {
|
|||
0xC5, 0xE5, 0xD8, 0xF8, 0x250C, 0x2510, 0x2514, 0x2518
|
||||
};
|
||||
|
||||
// Maps EIA-608 PAC row numbers to WebVTT cue line settings.
|
||||
// Adapted from: https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/608toVTT.html#x1-preamble-address-code-pac
|
||||
private static final float[] CUE_LINE_MAP = new float[] {
|
||||
63.33f, // Row 11
|
||||
10.00f, // Row 1
|
||||
26.00f, // Row 4
|
||||
68.66f, // Row 12
|
||||
79.33f, // Row 14
|
||||
31.33f, // Row 5
|
||||
42.00f, // Row 7
|
||||
52.66f, // Row 9
|
||||
};
|
||||
// Maps EIA-608 PAC row bits to rows.
|
||||
private static final int[] ROW_INDICES = new int[] { 11, 1, 3, 12, 14, 5, 7, 9 };
|
||||
|
||||
// Maps EIA-608 PAC indents to WebVTT cue position values.
|
||||
// Adapted from: https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/608toVTT.html#x1-preamble-address-code-pac
|
||||
// Note that these cue position values may not give the intended result, unless the font size is set
|
||||
// Maps EIA-608 PAC cursor bits to indents.
|
||||
// Note that these indents may not give the intended result, unless the font size is set
|
||||
// to allow for a maximum of 32 (or 41) characters per line.
|
||||
private static final float[] INDENT_MAP = new float[] {
|
||||
10.0f, // Indent 0/Column 1
|
||||
20.0f, // Indent 4/Column 5
|
||||
30.0f, // Indent 8/Column 9
|
||||
40.0f, // Indent 12/Column 13
|
||||
50.0f, // Indent 16/Column 17
|
||||
60.0f, // Indent 20/Column 21
|
||||
70.0f, // Indent 24/Column 25
|
||||
80.0f, // Indent 28/Column 29
|
||||
};
|
||||
private static final int[] CURSOR_INDICES = new int[] { 0, 4, 8, 12, 16, 20, 24, 28 };
|
||||
|
||||
private static final int[] COLOR_MAP = new int[] {
|
||||
Color.WHITE,
|
||||
|
|
@ -216,18 +193,14 @@ public final class Eia608Decoder implements SubtitleDecoder {
|
|||
|
||||
// Transparency is defined in the first byte of an integer.
|
||||
private static final int TRANSPARENCY_MASK = 0x80FFFFFF;
|
||||
|
||||
private static final int STYLE_ITALIC = Typeface.ITALIC;
|
||||
private static final float DEFAULT_CUE_LINE = CUE_LINE_MAP[4]; // Row 11
|
||||
private static final float DEFAULT_INDENT = INDENT_MAP[0]; // Indent 0
|
||||
|
||||
private final LinkedList<SubtitleInputBuffer> availableInputBuffers;
|
||||
private final LinkedList<SubtitleOutputBuffer> availableOutputBuffers;
|
||||
private final TreeSet<SubtitleInputBuffer> queuedInputBuffers;
|
||||
|
||||
private final ParsableByteArray ccData;
|
||||
|
||||
private final SpannableStringBuilder captionStringBuilder;
|
||||
private final LinkedList<Eia608CueBuilder> cues;
|
||||
|
||||
private long playbackPositionUs;
|
||||
|
||||
|
|
@ -235,17 +208,17 @@ public final class Eia608Decoder implements SubtitleDecoder {
|
|||
|
||||
private int captionMode;
|
||||
private int captionRowCount;
|
||||
boolean nextRowDown;
|
||||
|
||||
private LinkedList<Cue> cues;
|
||||
private HashMap<Integer, CharacterStyle> captionStyles;
|
||||
float cueIndent;
|
||||
float cueLine;
|
||||
int tabOffset;
|
||||
// The Cue that's currently being built and decoded.
|
||||
private Eia608CueBuilder currentCue;
|
||||
|
||||
private boolean repeatableControlSet;
|
||||
private byte repeatableControlCc1;
|
||||
private byte repeatableControlCc2;
|
||||
|
||||
private Eia608Subtitle subtitle;
|
||||
|
||||
public Eia608Decoder() {
|
||||
availableInputBuffers = new LinkedList<>();
|
||||
for (int i = 0; i < NUM_INPUT_BUFFERS; i++) {
|
||||
|
|
@ -258,15 +231,12 @@ public final class Eia608Decoder implements SubtitleDecoder {
|
|||
queuedInputBuffers = new TreeSet<>();
|
||||
|
||||
ccData = new ParsableByteArray();
|
||||
|
||||
captionStringBuilder = new SpannableStringBuilder();
|
||||
captionStyles = new HashMap<>();
|
||||
subtitle = new Eia608Subtitle();
|
||||
cues = new LinkedList<>();
|
||||
nextRowDown = false;
|
||||
|
||||
setCaptionMode(CC_MODE_UNKNOWN);
|
||||
captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT;
|
||||
cueIndent = DEFAULT_INDENT;
|
||||
cueLine = DEFAULT_CUE_LINE;
|
||||
tabOffset = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -322,14 +292,11 @@ public final class Eia608Decoder implements SubtitleDecoder {
|
|||
decode(inputBuffer);
|
||||
|
||||
// check if we have any caption updates to report
|
||||
if (!cues.isEmpty()) {
|
||||
if (!inputBuffer.isDecodeOnly()) {
|
||||
SubtitleOutputBuffer outputBuffer = availableOutputBuffers.pollFirst();
|
||||
outputBuffer.setContent(inputBuffer.timeUs, new Eia608Subtitle(cues), 0);
|
||||
cues = new LinkedList<>();
|
||||
releaseInputBuffer(inputBuffer);
|
||||
return outputBuffer;
|
||||
}
|
||||
if (!inputBuffer.isDecodeOnly()) {
|
||||
SubtitleOutputBuffer outputBuffer = availableOutputBuffers.pollFirst();
|
||||
outputBuffer.setContent(inputBuffer.timeUs, subtitle, 0);
|
||||
releaseInputBuffer(inputBuffer);
|
||||
return outputBuffer;
|
||||
}
|
||||
|
||||
releaseInputBuffer(inputBuffer);
|
||||
|
|
@ -353,11 +320,9 @@ public final class Eia608Decoder implements SubtitleDecoder {
|
|||
setCaptionMode(CC_MODE_UNKNOWN);
|
||||
captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT;
|
||||
playbackPositionUs = 0;
|
||||
flushCaptionBuilder();
|
||||
cues = new LinkedList<>();
|
||||
cueIndent = DEFAULT_INDENT;
|
||||
cueLine = DEFAULT_CUE_LINE;
|
||||
tabOffset = 0;
|
||||
currentCue = new Eia608CueBuilder();
|
||||
cues.clear();
|
||||
nextRowDown = false;
|
||||
repeatableControlSet = false;
|
||||
repeatableControlCc1 = 0;
|
||||
repeatableControlCc2 = 0;
|
||||
|
|
@ -393,23 +358,23 @@ public final class Eia608Decoder implements SubtitleDecoder {
|
|||
// Special North American character set.
|
||||
// ccData2 - P|0|1|1|X|X|X|X
|
||||
if ((ccData1 == 0x11 || ccData1 == 0x19) && ((ccData2 & 0x70) == 0x30)) {
|
||||
captionStringBuilder.append(getSpecialChar(ccData2));
|
||||
currentCue.append(getSpecialChar(ccData2));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extended Spanish/Miscellaneous and French character set.
|
||||
// ccData2 - P|0|1|X|X|X|X|X
|
||||
if ((ccData1 == 0x12 || ccData1 == 0x1A) && ((ccData2 & 0x60) == 0x20)) {
|
||||
backspace(); // Remove standard equivalent of the special extended char.
|
||||
captionStringBuilder.append(getExtendedEsFrChar(ccData2));
|
||||
currentCue.backspace(); // Remove standard equivalent of the special extended char.
|
||||
currentCue.append(getExtendedEsFrChar(ccData2));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extended Portuguese and German/Danish character set.
|
||||
// ccData2 - P|0|1|X|X|X|X|X
|
||||
if ((ccData1 == 0x13 || ccData1 == 0x1B) && ((ccData2 & 0x60) == 0x20)) {
|
||||
backspace(); // Remove standard equivalent of the special extended char.
|
||||
captionStringBuilder.append(getExtendedPtDeChar(ccData2));
|
||||
currentCue.backspace(); // Remove standard equivalent of the special extended char.
|
||||
currentCue.append(getExtendedPtDeChar(ccData2));
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -425,9 +390,9 @@ public final class Eia608Decoder implements SubtitleDecoder {
|
|||
}
|
||||
|
||||
// Basic North American character set.
|
||||
captionStringBuilder.append(getChar(ccData1));
|
||||
currentCue.append(getChar(ccData1));
|
||||
if (ccData2 >= 0x20) {
|
||||
captionStringBuilder.append(getChar(ccData2));
|
||||
currentCue.append(getChar(ccData2));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -435,8 +400,8 @@ public final class Eia608Decoder implements SubtitleDecoder {
|
|||
if (!isRepeatableControl) {
|
||||
repeatableControlSet = false;
|
||||
}
|
||||
if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) {
|
||||
buildCue();
|
||||
if (captionMode == CC_MODE_PAINT_ON || captionMode == CC_MODE_ROLL_UP) {
|
||||
renderCues();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -456,7 +421,7 @@ public final class Eia608Decoder implements SubtitleDecoder {
|
|||
if (isMiscCode(cc1, cc2)) {
|
||||
handleMiscCode(cc2);
|
||||
} else if (isPreambleAddressCode(cc1, cc2)) {
|
||||
handlePreambleCode(cc1, cc2);
|
||||
handlePreambleAddressCode(cc1, cc2);
|
||||
} else if (isTabOffset(cc1, cc2)) {
|
||||
handleTabOffset(cc2);
|
||||
}
|
||||
|
|
@ -492,65 +457,75 @@ public final class Eia608Decoder implements SubtitleDecoder {
|
|||
switch (cc2) {
|
||||
case CTRL_ERASE_DISPLAYED_MEMORY:
|
||||
if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) {
|
||||
flushCaptionBuilder();
|
||||
currentCue = new Eia608CueBuilder();
|
||||
cues.clear();
|
||||
}
|
||||
subtitle.setCues(Collections.<Cue>emptyList());
|
||||
return;
|
||||
case CTRL_ERASE_NON_DISPLAYED_MEMORY:
|
||||
flushCaptionBuilder();
|
||||
currentCue = new Eia608CueBuilder();
|
||||
cues.clear();
|
||||
return;
|
||||
case CTRL_END_OF_CAPTION:
|
||||
buildCue();
|
||||
flushCaptionBuilder();
|
||||
renderCues();
|
||||
cues.clear();
|
||||
return;
|
||||
case CTRL_CARRIAGE_RETURN:
|
||||
maybeAppendNewline();
|
||||
// Each time a Carriage Return is received, the text in the top row of the window is erased
|
||||
// from memory and from the display. The remaining rows of text are each rolled up into the
|
||||
// next highest row in the window, leaving the base row blank and ready to accept new text.
|
||||
if (captionMode == CC_MODE_ROLL_UP) {
|
||||
for (Eia608CueBuilder cue : cues) {
|
||||
// Roll up all the other rows.
|
||||
if (!cue.rollUp()) {
|
||||
cues.remove(cue);
|
||||
}
|
||||
}
|
||||
currentCue = new Eia608CueBuilder();
|
||||
cues.add(currentCue);
|
||||
while (cues.size() > captionRowCount) {
|
||||
cues.pollFirst();
|
||||
}
|
||||
}
|
||||
return;
|
||||
case CTRL_BACKSPACE:
|
||||
backspace();
|
||||
currentCue.backspace();
|
||||
return;
|
||||
case CTRL_DELETE_TO_END_OF_ROW:
|
||||
// TODO: Clear currentCue's captionText.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePreambleCode(byte cc1, byte cc2) {
|
||||
private void handlePreambleAddressCode(byte cc1, byte cc2) {
|
||||
// For PAC layout see: https://en.wikipedia.org/wiki/EIA-608#Control_commands
|
||||
applySpans(); // Apply any open spans.
|
||||
|
||||
// Parse the "next row down" flag.
|
||||
boolean nextRowDown = (cc2 & 0x20) != 0;
|
||||
if (nextRowDown) {
|
||||
// TODO: We should create a new cue instead, this may cause issues when
|
||||
// the new line receives it's own PAC which we ignore currently.
|
||||
// As a result of that the new line will be positioned directly below the
|
||||
// previous line.
|
||||
maybeAppendNewline();
|
||||
// Parse the "next row down" toggle.
|
||||
nextRowDown = (cc2 & 0x20) != 0;
|
||||
|
||||
int row = ROW_INDICES[cc1 & 0x7];
|
||||
if (row != currentCue.getRow() || nextRowDown) {
|
||||
currentCue = new Eia608CueBuilder();
|
||||
cues.add(currentCue);
|
||||
}
|
||||
currentCue.setRow(nextRowDown ? ++row : row);
|
||||
|
||||
// Go through the bits, starting with the last bit - the underline flag:
|
||||
boolean underline = (cc2 & 0x1) != 0;
|
||||
if (underline) {
|
||||
setCharacterStyle(new UnderlineSpan());
|
||||
currentCue.setCharacterStyle(new UnderlineSpan());
|
||||
}
|
||||
|
||||
// Next, parse the attribute bits:
|
||||
int attribute = cc2 >> 1 & 0xF;
|
||||
if (attribute >= 0x0 && attribute < 0x7) {
|
||||
// Attribute is a foreground color
|
||||
setCharacterStyle(new ForegroundColorSpan(COLOR_MAP[attribute]));
|
||||
currentCue.setCharacterStyle(new ForegroundColorSpan(COLOR_MAP[attribute]));
|
||||
} else if (attribute == 0x7) {
|
||||
// Attribute is "italics"
|
||||
setCharacterStyle(new StyleSpan(STYLE_ITALIC));
|
||||
currentCue.setCharacterStyle(new StyleSpan(STYLE_ITALIC));
|
||||
} else if (attribute >= 0x8 && attribute <= 0xF) {
|
||||
// Attribute is an indent
|
||||
if (cueIndent == DEFAULT_INDENT) {
|
||||
// Only update the indent, if it's the default indent.
|
||||
// This is not conform the spec, but otherwise indentations may be off
|
||||
// because we don't create a new cue when we see the nextRowDown flag.
|
||||
cueIndent = INDENT_MAP[attribute & 0x7];
|
||||
}
|
||||
currentCue.setIndent(CURSOR_INDICES[attribute & 0x7]);
|
||||
}
|
||||
|
||||
// Parse the row bits
|
||||
cueLine = CUE_LINE_MAP[cc1 & 0x7];
|
||||
}
|
||||
|
||||
private void handleMidrowCode(byte cc1, byte cc2) {
|
||||
|
|
@ -558,152 +533,20 @@ public final class Eia608Decoder implements SubtitleDecoder {
|
|||
int attribute = cc2 >> 1 & 0xF;
|
||||
if ((cc1 & 0x1) != 0) {
|
||||
// Background Color
|
||||
setCharacterStyle(new BackgroundColorSpan(transparentOrUnderline ?
|
||||
currentCue.setCharacterStyle(new BackgroundColorSpan(transparentOrUnderline ?
|
||||
COLOR_MAP[attribute] & TRANSPARENCY_MASK : COLOR_MAP[attribute]));
|
||||
} else {
|
||||
// Foreground color
|
||||
setCharacterStyle(new ForegroundColorSpan(COLOR_MAP[attribute]));
|
||||
currentCue.setCharacterStyle(new ForegroundColorSpan(COLOR_MAP[attribute]));
|
||||
if (transparentOrUnderline) {
|
||||
// Text should be underlined
|
||||
setCharacterStyle(new UnderlineSpan());
|
||||
currentCue.setCharacterStyle(new UnderlineSpan());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleTabOffset(byte cc2) {
|
||||
// Formula for tab offset handling adapted from:
|
||||
// https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/608toVTT.html#x1-preamble-address-code-pac
|
||||
// We're ignoring any tab offsets that do not occur at the beginning of a new cue.
|
||||
// This is not conform the spec, but works in most cases.
|
||||
if (captionStringBuilder.length() == 0) {
|
||||
tabOffset = cc2 - 0x20;
|
||||
}
|
||||
}
|
||||
|
||||
private int getSpanStartIndex() {
|
||||
return captionStringBuilder.length() > 0 ? captionStringBuilder.length() - 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a character style at the current cueIndex.
|
||||
* Takes care of style priorities.
|
||||
*
|
||||
* @param style the style to set.
|
||||
*/
|
||||
private void setCharacterStyle(CharacterStyle style) {
|
||||
int startIndex = getSpanStartIndex();
|
||||
// Close all open spans of the same type, and add a new one.
|
||||
if (style instanceof ForegroundColorSpan) {
|
||||
// Setting a foreground color clears the italics style.
|
||||
applySpan(StyleSpan.class); //
|
||||
}
|
||||
applySpan(style.getClass());
|
||||
captionStyles.put(startIndex, style);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes all open spans of the spansToApply class.
|
||||
* @param spansToClose the class of which the spans should be closed.
|
||||
*/
|
||||
private void applySpan(Class<? extends CharacterStyle> spansToClose) {
|
||||
for (Integer index : captionStyles.keySet()) {
|
||||
CharacterStyle style = captionStyles.get(index);
|
||||
if (spansToClose.isInstance(style)) {
|
||||
if (index < captionStringBuilder.length()) {
|
||||
captionStringBuilder.setSpan(style, index,
|
||||
captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
captionStyles.remove(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies all currently opened spans to the SpannableStringBuilder.
|
||||
*/
|
||||
private void applySpans() {
|
||||
// Check if we have to do anything.
|
||||
if (captionStyles.size() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Integer startIndex : captionStyles.keySet()) {
|
||||
// There may be cases, e.g. when seeking where the startIndex becomes greater
|
||||
// than what is actually in the string builder, in that case, just discard the span.
|
||||
if (startIndex < captionStringBuilder.length()) {
|
||||
CharacterStyle captionStyle = captionStyles.get(startIndex);
|
||||
captionStringBuilder.setSpan(captionStyle, startIndex,
|
||||
captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
captionStyles.remove(startIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a cue from whatever is in the SpannableStringBuilder now.
|
||||
*/
|
||||
private void buildCue() {
|
||||
applySpans(); // Apply Spans
|
||||
CharSequence captionString = getDisplayCaption();
|
||||
if (captionString != null) {
|
||||
cueIndent = tabOffset * 2.5f + cueIndent;
|
||||
tabOffset = 0;
|
||||
Cue cue = new Cue(captionString, Layout.Alignment.ALIGN_NORMAL, cueLine / 100, Cue.LINE_TYPE_FRACTION,
|
||||
Cue.ANCHOR_TYPE_START, cueIndent / 100, Cue.TYPE_UNSET, Cue.DIMEN_UNSET);
|
||||
cues.add(cue);
|
||||
if (captionMode == CC_MODE_POP_ON) {
|
||||
captionStringBuilder.clear();
|
||||
captionStringBuilder.clearSpans();
|
||||
cueLine = DEFAULT_CUE_LINE;
|
||||
}
|
||||
cueIndent = DEFAULT_INDENT;
|
||||
}
|
||||
}
|
||||
|
||||
private void flushCaptionBuilder() {
|
||||
captionStringBuilder.clear();
|
||||
captionStringBuilder.clearSpans();
|
||||
}
|
||||
|
||||
private void backspace() {
|
||||
if (captionStringBuilder.length() > 0) {
|
||||
captionStringBuilder.delete(captionStringBuilder.length() - 1, captionStringBuilder.length());
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeAppendNewline() {
|
||||
int buildLength = captionStringBuilder.length();
|
||||
if (buildLength > 0 && captionStringBuilder.charAt(buildLength - 1) != '\n') {
|
||||
captionStringBuilder.append('\n');
|
||||
}
|
||||
}
|
||||
|
||||
private CharSequence 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.subSequence(0, endIndex);
|
||||
}
|
||||
|
||||
int startIndex = 0;
|
||||
int searchBackwardFromIndex = endIndex;
|
||||
for (int i = 0; i < captionRowCount && searchBackwardFromIndex != -1; i++) {
|
||||
searchBackwardFromIndex = captionStringBuilder.toString().lastIndexOf("\n", searchBackwardFromIndex - 1);
|
||||
}
|
||||
if (searchBackwardFromIndex != -1) {
|
||||
startIndex = searchBackwardFromIndex + 1;
|
||||
}
|
||||
captionStringBuilder.delete(0, startIndex);
|
||||
return captionStringBuilder.subSequence(0, endIndex - startIndex);
|
||||
currentCue.tab(cc2 - 0x20);
|
||||
}
|
||||
|
||||
private void setCaptionMode(int captionMode) {
|
||||
|
|
@ -713,14 +556,17 @@ public final class Eia608Decoder implements SubtitleDecoder {
|
|||
|
||||
this.captionMode = captionMode;
|
||||
// Clear the working memory.
|
||||
captionStringBuilder.clear();
|
||||
captionStringBuilder.clearSpans();
|
||||
currentCue = new Eia608CueBuilder();
|
||||
if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_UNKNOWN) {
|
||||
// When switching to roll-up or unknown, we also need to clear the caption.
|
||||
cues = new LinkedList<>();
|
||||
cues.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void renderCues() {
|
||||
subtitle.setCues(Eia608CueBuilder.buildCues(cues));
|
||||
}
|
||||
|
||||
private static char getChar(byte ccData) {
|
||||
int index = (ccData & 0x7F) - 0x20;
|
||||
return (char) BASIC_CHARACTER_SET[index];
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.text.eia608;
|
|||
import com.google.android.exoplayer2.text.Cue;
|
||||
import com.google.android.exoplayer2.text.Subtitle;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
|
|
@ -25,9 +26,13 @@ import java.util.List;
|
|||
*/
|
||||
/* package */ final class Eia608Subtitle implements Subtitle {
|
||||
|
||||
private final List<Cue> cues;
|
||||
private List<Cue> cues;
|
||||
|
||||
public Eia608Subtitle(List<Cue> cues) {
|
||||
public Eia608Subtitle() {
|
||||
cues = new LinkedList<>();
|
||||
}
|
||||
|
||||
/* package */ void setCues(List<Cue> cues) {
|
||||
this.cues = cues;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.ui;
|
||||
|
||||
import com.google.android.exoplayer2.text.CaptionStyleCompat;
|
||||
import com.google.android.exoplayer2.text.Cue;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
|
|
@ -30,9 +34,6 @@ import android.text.TextPaint;
|
|||
import android.text.TextUtils;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import com.google.android.exoplayer2.text.CaptionStyleCompat;
|
||||
import com.google.android.exoplayer2.text.Cue;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
/**
|
||||
* Paints subtitle {@link Cue}s.
|
||||
|
|
|
|||
Loading…
Reference in a new issue