Implemented PAC and Midrow code handling for EIA-608 captions

This commit is contained in:
Rik Heijdens 2016-08-24 14:08:37 +02:00
parent f6fdcee9b3
commit 43e6e16168
2 changed files with 292 additions and 42 deletions

View file

@ -15,14 +15,27 @@
*/
package com.google.android.exoplayer2.text.eia608;
import android.text.TextUtils;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.SubtitleDecoder;
import com.google.android.exoplayer2.text.SubtitleDecoderException;
import com.google.android.exoplayer2.text.SubtitleInputBuffer;
import com.google.android.exoplayer2.text.SubtitleOutputBuffer;
import com.google.android.exoplayer2.util.Assertions;
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.LinkedList;
import java.util.TreeSet;
@ -86,6 +99,12 @@ public final class Eia608Decoder implements SubtitleDecoder {
private static final byte CTRL_CARRIAGE_RETURN = 0x2D;
private static final byte CTRL_ERASE_NON_DISPLAYED_MEMORY = 0x2E;
private static final byte CTRL_TAB_OFFSET_CHAN_1 = 0x17;
private static final byte CTRL_TAB_OFFSET_CHAN_2 = 0x1F;
private static final byte CTRL_TAB_OFFSET_1 = 0x21;
private static final byte CTRL_TAB_OFFSET_2 = 0x22;
private static final byte CTRL_TAB_OFFSET_3 = 0x23;
private static final byte CTRL_BACKSPACE = 0x21;
private static final byte CTRL_MISC_CHAN_1 = 0x14;
@ -159,13 +178,66 @@ 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[] {
10.00f, // Row 1
15.33f,
20.66f,
26.00f,
31.33f,
36.66f,
42.00f,
47.33f,
52.66f,
58.00f,
63.33f,
68.66f,
74.00f,
79.33f,
84.66f // Row 15
};
// 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
// 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[] COLOR_MAP = new int[] {
Color.WHITE,
Color.GREEN,
Color.BLUE,
Color.CYAN,
Color.RED,
Color.YELLOW,
Color.MAGENTA,
Color.BLACK // Only used by Mid Row style changes, for PAC an value of 0x7 means italics.
};
// Transparency is defined in the two left most bytes 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[10]; // 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 StringBuilder captionStringBuilder;
private final SpannableStringBuilder captionStringBuilder;
private long playbackPositionUs;
@ -173,9 +245,12 @@ public final class Eia608Decoder implements SubtitleDecoder {
private int captionMode;
private int captionRowCount;
private String captionString;
private String lastCaptionString;
private LinkedList<Cue> cues;
private HashMap<Integer, CharacterStyle> captionStyles;
float cueIndent;
float cueLine;
int tabOffset;
private boolean repeatableControlSet;
private byte repeatableControlCc1;
@ -194,10 +269,14 @@ public final class Eia608Decoder implements SubtitleDecoder {
ccData = new ParsableByteArray();
captionStringBuilder = new StringBuilder();
captionStringBuilder = new SpannableStringBuilder();
captionStyles = new HashMap<>();
setCaptionMode(CC_MODE_UNKNOWN);
captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT;
cueIndent = DEFAULT_INDENT;
cueLine = DEFAULT_CUE_LINE;
tabOffset = 0;
}
@Override
@ -253,11 +332,11 @@ public final class Eia608Decoder implements SubtitleDecoder {
decode(inputBuffer);
// check if we have any caption updates to report
if (!TextUtils.equals(captionString, lastCaptionString)) {
lastCaptionString = captionString;
if (!cues.isEmpty()) {
if (!inputBuffer.isDecodeOnly()) {
SubtitleOutputBuffer outputBuffer = availableOutputBuffers.pollFirst();
outputBuffer.setContent(inputBuffer.timeUs, new Eia608Subtitle(captionString), 0);
outputBuffer.setContent(inputBuffer.timeUs, new Eia608Subtitle(cues), 0);
cues = new LinkedList<>();
releaseInputBuffer(inputBuffer);
return outputBuffer;
}
@ -284,9 +363,11 @@ public final class Eia608Decoder implements SubtitleDecoder {
setCaptionMode(CC_MODE_UNKNOWN);
captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT;
playbackPositionUs = 0;
captionStringBuilder.setLength(0);
captionString = null;
lastCaptionString = null;
flushCaptionBuilder();
cues = new LinkedList<>();
cueIndent = DEFAULT_INDENT;
cueLine = DEFAULT_CUE_LINE;
tabOffset = 0;
repeatableControlSet = false;
repeatableControlCc1 = 0;
repeatableControlCc2 = 0;
@ -342,6 +423,11 @@ public final class Eia608Decoder implements SubtitleDecoder {
continue;
}
// Mid row changes.
if ((ccData1 == 0x11 || ccData1 == 0x19) && ccData2 >= 0x20 && ccData2 <= 0x2F) {
handleMidrowCode(ccData1, ccData2);
}
// Control character.
if (ccData1 < 0x20) {
isRepeatableControl = handleCtrl(ccData1, ccData2);
@ -360,7 +446,7 @@ public final class Eia608Decoder implements SubtitleDecoder {
repeatableControlSet = false;
}
if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) {
captionString = getDisplayCaption();
buildCue();
}
}
}
@ -380,8 +466,9 @@ public final class Eia608Decoder implements SubtitleDecoder {
if (isMiscCode(cc1, cc2)) {
handleMiscCode(cc2);
} else if (isPreambleAddressCode(cc1, cc2)) {
// TODO: Add better handling of this with specific positioning.
maybeAppendNewline();
handlePreambleCode(cc1, cc2);
} else if (isTabOffset(cc1, cc2)) {
handleTabOffset(cc2);
}
return isRepeatableControl;
}
@ -414,32 +501,197 @@ public final class Eia608Decoder implements SubtitleDecoder {
switch (cc2) {
case CTRL_ERASE_DISPLAYED_MEMORY:
captionString = null;
if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) {
captionStringBuilder.setLength(0);
flushCaptionBuilder();
}
return;
case CTRL_ERASE_NON_DISPLAYED_MEMORY:
captionStringBuilder.setLength(0);
flushCaptionBuilder();
return;
case CTRL_END_OF_CAPTION:
captionString = getDisplayCaption();
captionStringBuilder.setLength(0);
buildCue();
flushCaptionBuilder();
return;
case CTRL_CARRIAGE_RETURN:
maybeAppendNewline();
return;
case CTRL_BACKSPACE:
if (captionStringBuilder.length() > 0) {
captionStringBuilder.setLength(captionStringBuilder.length() - 1);
}
backspace();
return;
}
}
private void handlePreambleCode(byte cc1, byte cc2) {
// For PAC layout see: https://en.wikipedia.org/wiki/EIA-608#Control_commands
applySpan(); // Apply any 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();
}
// Go through the bits, starting with the last bit - the underline flag:
boolean underline = (cc2 & 0x1) != 0;
if (underline) {
captionStyles.put(getSpanStartIndex(), new UnderlineSpan());
}
// Next, parse the attribute bits:
int attribute = cc2 >> 1 & 0xF;
if (attribute >= 0x0 && attribute < 0x7) {
// Attribute is a foreground color
captionStyles.put(getSpanStartIndex(), new ForegroundColorSpan(COLOR_MAP[attribute]));
} else if (attribute == 0x7) {
// Attribute is "italics"
captionStyles.put(getSpanStartIndex(), 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];
}
}
// Parse the row bits
int row = cc1 & 0x7;
if (row >= 0x4) {
// Extended Preamble Code
row = row & 0x3;
switch (row) {
case 0x0:
// Row 14 or 15
cueLine = CUE_LINE_MAP[13];
break;
case 0x1:
// Row 5 or 6
cueLine = CUE_LINE_MAP[4];
break;
case 0x2:
// Row 7 or 8
cueLine = CUE_LINE_MAP[7];
break;
case 0x3:
// Row 9 or 10
cueLine = CUE_LINE_MAP[8];
break;
}
} else {
// Regular Preamble Code
switch (row) {
case 0x0:
// Row 11 (Default)
cueLine = CUE_LINE_MAP[10];
break;
case 0x1:
// Row 1 (Top)
cueLine = CUE_LINE_MAP[0];
break;
case 0x2:
// Row 4 (Top)
cueLine = CUE_LINE_MAP[3];
break;
case 0x3:
// Row 12 or 13 (Bottom)
cueLine = CUE_LINE_MAP[11];
break;
}
}
}
private void handleMidrowCode(byte cc1, byte cc2) {
boolean transparentOrUnderline = (cc2 & 0x1) != 0;
int attribute = cc2 >> 1 & 0xF;
if ((cc1 & 0x1) != 0) {
// Background Color
captionStyles.put(getSpanStartIndex(), new BackgroundColorSpan(transparentOrUnderline ?
COLOR_MAP[attribute] & TRANSPARENCY_MASK : COLOR_MAP[attribute]));
} else {
// Foreground color
captionStyles.put(getSpanStartIndex(), new ForegroundColorSpan(COLOR_MAP[attribute]));
if (transparentOrUnderline) {
// Text should be underlined
captionStyles.put(getSpanStartIndex(), 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) {
switch (cc2) {
case CTRL_TAB_OFFSET_1:
tabOffset++;
break;
case CTRL_TAB_OFFSET_2:
tabOffset += 2;
break;
case CTRL_TAB_OFFSET_3:
tabOffset += 3;
break;
}
}
}
private int getSpanStartIndex() {
return captionStringBuilder.length() > 0 ? captionStringBuilder.length() - 1 : 0;
}
/**
* Applies a Span to the SpannableStringBuilder.
*/
private void applySpan() {
// Check if we have to do anything.
if (captionStyles.size() == 0) {
return;
}
for (Integer startIndex : captionStyles.keySet()) {
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() {
applySpan(); // 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.setLength(captionStringBuilder.length() - 1);
captionStringBuilder.replace(captionStringBuilder.length() - 1, captionStringBuilder.length(), "");
}
}
@ -450,7 +702,7 @@ public final class Eia608Decoder implements SubtitleDecoder {
}
}
private String getDisplayCaption() {
private CharSequence getDisplayCaption() {
int buildLength = captionStringBuilder.length();
if (buildLength == 0) {
return null;
@ -463,19 +715,19 @@ public final class Eia608Decoder implements SubtitleDecoder {
int endIndex = endsWithNewline ? buildLength - 1 : buildLength;
if (captionMode != CC_MODE_ROLL_UP) {
return captionStringBuilder.substring(0, endIndex);
return captionStringBuilder.subSequence(0, endIndex);
}
int startIndex = 0;
int searchBackwardFromIndex = endIndex;
for (int i = 0; i < captionRowCount && searchBackwardFromIndex != -1; i++) {
searchBackwardFromIndex = captionStringBuilder.lastIndexOf("\n", searchBackwardFromIndex - 1);
searchBackwardFromIndex = captionStringBuilder.toString().lastIndexOf("\n", searchBackwardFromIndex - 1);
}
if (searchBackwardFromIndex != -1) {
startIndex = searchBackwardFromIndex + 1;
}
captionStringBuilder.delete(0, startIndex);
return captionStringBuilder.substring(0, endIndex - startIndex);
return captionStringBuilder.subSequence(0, endIndex - startIndex);
}
private void setCaptionMode(int captionMode) {
@ -485,10 +737,11 @@ public final class Eia608Decoder implements SubtitleDecoder {
this.captionMode = captionMode;
// Clear the working memory.
captionStringBuilder.setLength(0);
captionStringBuilder.clear();
captionStringBuilder.clearSpans();
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 = new LinkedList<>();
}
}
@ -525,6 +778,11 @@ public final class Eia608Decoder implements SubtitleDecoder {
return cc1 >= 0x10 && cc1 <= 0x1F;
}
private static boolean isTabOffset(byte cc1, byte cc2) {
return (cc1 == CTRL_TAB_OFFSET_CHAN_1 || cc1 == CTRL_TAB_OFFSET_CHAN_2)
&& (cc2 >= 0x21 && cc2 <= 0x23);
}
/**
* Inspects an sei message to determine whether it contains EIA-608.
* <p>

View file

@ -15,10 +15,9 @@
*/
package com.google.android.exoplayer2.text.eia608;
import android.text.TextUtils;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.Subtitle;
import java.util.Collections;
import java.util.List;
/**
@ -26,13 +25,10 @@ import java.util.List;
*/
/* package */ final class Eia608Subtitle implements Subtitle {
private final String text;
private final List<Cue> cues;
/**
* @param text The subtitle text.
*/
public Eia608Subtitle(String text) {
this.text = text;
public Eia608Subtitle(List<Cue> cues) {
this.cues = cues;
}
@Override
@ -52,11 +48,7 @@ import java.util.List;
@Override
public List<Cue> getCues(long timeUs) {
if (TextUtils.isEmpty(text)) {
return Collections.emptyList();
} else {
return Collections.singletonList(new Cue(text));
}
return cues;
}
}