From 43e6e16168a7b7bc6e07e46396da1c26e524dc47 Mon Sep 17 00:00:00 2001 From: Rik Heijdens Date: Wed, 24 Aug 2016 14:08:37 +0200 Subject: [PATCH 01/12] Implemented PAC and Midrow code handling for EIA-608 captions --- .../exoplayer2/text/eia608/Eia608Decoder.java | 316 ++++++++++++++++-- .../text/eia608/Eia608Subtitle.java | 18 +- 2 files changed, 292 insertions(+), 42 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java index b5249cde78..c54cc2ef5e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java @@ -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 availableInputBuffers; private final LinkedList availableOutputBuffers; private final TreeSet 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 cues; + private HashMap 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. *

diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Subtitle.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Subtitle.java index 6b27004174..ae0ecd4867 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Subtitle.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Subtitle.java @@ -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 cues; - /** - * @param text The subtitle text. - */ - public Eia608Subtitle(String text) { - this.text = text; + public Eia608Subtitle(List cues) { + this.cues = cues; } @Override @@ -52,11 +48,7 @@ import java.util.List; @Override public List getCues(long timeUs) { - if (TextUtils.isEmpty(text)) { - return Collections.emptyList(); - } else { - return Collections.singletonList(new Cue(text)); - } + return cues; } } From 8b2af1244725b8d600c3f5ea2cceb4504728c7d9 Mon Sep 17 00:00:00 2001 From: Rik Heijdens Date: Wed, 24 Aug 2016 14:58:08 +0200 Subject: [PATCH 02/12] Discard spans after seeking Discard any spans that should not be applied because they don't belong to what's in the current CC buffer. --- .../android/exoplayer2/text/eia608/Eia608Decoder.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java index c54cc2ef5e..ecd2c766c4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java @@ -656,9 +656,13 @@ public final class Eia608Decoder implements SubtitleDecoder { } for (Integer startIndex : captionStyles.keySet()) { - CharacterStyle captionStyle = captionStyles.get(startIndex); - captionStringBuilder.setSpan(captionStyle, startIndex, - captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + // 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); } } From ce55587db48ae0f7be24a4817af26a444f69cc8d Mon Sep 17 00:00:00 2001 From: Rik Heijdens Date: Wed, 24 Aug 2016 16:27:23 +0200 Subject: [PATCH 03/12] Fixed comment Alpha is stored in the first byte of an integer --- .../google/android/exoplayer2/text/eia608/Eia608Decoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java index ecd2c766c4..f64cccaa4f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java @@ -224,7 +224,7 @@ public final class Eia608Decoder implements SubtitleDecoder { 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. + // 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; From 176ce485696cfda8418e7426dc6a7e25999096f5 Mon Sep 17 00:00:00 2001 From: Rik Heijdens Date: Thu, 25 Aug 2016 10:07:05 +0200 Subject: [PATCH 04/12] Improved line and tab offset mapping --- .../exoplayer2/text/eia608/Eia608Decoder.java | 82 +++---------------- 1 file changed, 10 insertions(+), 72 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java index f64cccaa4f..0d82385fc8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java @@ -101,9 +101,6 @@ public final class Eia608Decoder implements SubtitleDecoder { 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; @@ -181,21 +178,14 @@ public final class Eia608Decoder implements SubtitleDecoder { // 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 - 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 + 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 indents to WebVTT cue position values. @@ -228,7 +218,7 @@ public final class Eia608Decoder implements SubtitleDecoder { 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_CUE_LINE = CUE_LINE_MAP[4]; // Row 11 private static final float DEFAULT_INDENT = INDENT_MAP[0]; // Indent 0 private final LinkedList availableInputBuffers; @@ -560,49 +550,7 @@ public final class Eia608Decoder implements SubtitleDecoder { } // 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; - } - } + cueLine = CUE_LINE_MAP[cc1 & 0x7]; } private void handleMidrowCode(byte cc1, byte cc2) { @@ -628,17 +576,7 @@ public final class Eia608Decoder implements SubtitleDecoder { // 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; - } + tabOffset = cc2 - 0x20; } } From 7cd94819c7a4a0222022cbfd49a1d77f30aa5812 Mon Sep 17 00:00:00 2001 From: Rik Heijdens Date: Thu, 25 Aug 2016 11:23:03 +0200 Subject: [PATCH 05/12] Took care of caption styling priorities --- .../exoplayer2/text/eia608/Eia608Decoder.java | 56 +++++++++++++++---- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java index 0d82385fc8..6b76973278 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java @@ -513,7 +513,7 @@ public final class Eia608Decoder implements SubtitleDecoder { private void handlePreambleCode(byte cc1, byte cc2) { // For PAC layout see: https://en.wikipedia.org/wiki/EIA-608#Control_commands - applySpan(); // Apply any spans. + applySpans(); // Apply any open spans. // Parse the "next row down" flag. boolean nextRowDown = (cc2 & 0x20) != 0; @@ -528,17 +528,17 @@ public final class Eia608Decoder implements SubtitleDecoder { // Go through the bits, starting with the last bit - the underline flag: boolean underline = (cc2 & 0x1) != 0; if (underline) { - captionStyles.put(getSpanStartIndex(), new UnderlineSpan()); + setCharacterStyle(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])); + setCharacterStyle(new ForegroundColorSpan(COLOR_MAP[attribute])); } else if (attribute == 0x7) { // Attribute is "italics" - captionStyles.put(getSpanStartIndex(), new StyleSpan(STYLE_ITALIC)); + setCharacterStyle(new StyleSpan(STYLE_ITALIC)); } else if (attribute >= 0x8 && attribute <= 0xF) { // Attribute is an indent if (cueIndent == DEFAULT_INDENT) { @@ -558,14 +558,14 @@ public final class Eia608Decoder implements SubtitleDecoder { int attribute = cc2 >> 1 & 0xF; if ((cc1 & 0x1) != 0) { // Background Color - captionStyles.put(getSpanStartIndex(), new BackgroundColorSpan(transparentOrUnderline ? + setCharacterStyle(new BackgroundColorSpan(transparentOrUnderline ? COLOR_MAP[attribute] & TRANSPARENCY_MASK : COLOR_MAP[attribute])); } else { // Foreground color - captionStyles.put(getSpanStartIndex(), new ForegroundColorSpan(COLOR_MAP[attribute])); + setCharacterStyle(new ForegroundColorSpan(COLOR_MAP[attribute])); if (transparentOrUnderline) { // Text should be underlined - captionStyles.put(getSpanStartIndex(), new UnderlineSpan()); + setCharacterStyle(new UnderlineSpan()); } } } @@ -585,9 +585,43 @@ public final class Eia608Decoder implements SubtitleDecoder { } /** - * Applies a Span to the SpannableStringBuilder. + * Sets a character style at the current cueIndex. + * Takes care of style priorities. + * + * @param style the style to set. */ - private void applySpan() { + 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 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; @@ -609,7 +643,7 @@ public final class Eia608Decoder implements SubtitleDecoder { * Builds a cue from whatever is in the SpannableStringBuilder now. */ private void buildCue() { - applySpan(); // Apply Spans + applySpans(); // Apply Spans CharSequence captionString = getDisplayCaption(); if (captionString != null) { cueIndent = tabOffset * 2.5f + cueIndent; @@ -633,7 +667,7 @@ public final class Eia608Decoder implements SubtitleDecoder { private void backspace() { if (captionStringBuilder.length() > 0) { - captionStringBuilder.replace(captionStringBuilder.length() - 1, captionStringBuilder.length(), ""); + captionStringBuilder.delete(captionStringBuilder.length() - 1, captionStringBuilder.length()); } } From 71d83f3e8478f05271845eb36a0a3f1bfec98808 Mon Sep 17 00:00:00 2001 From: Rik Heijdens Date: Mon, 29 Aug 2016 11:33:55 +0200 Subject: [PATCH 06/12] Caption lines as separate cues --- .../text/eia608/Eia608CueBuilder.java | 203 +++++++++++ .../exoplayer2/text/eia608/Eia608Decoder.java | 322 +++++------------- .../text/eia608/Eia608Subtitle.java | 9 +- .../exoplayer2/ui/SubtitlePainter.java | 7 +- 4 files changed, 298 insertions(+), 243 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java new file mode 100644 index 0000000000..336ea6dc27 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java @@ -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 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 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 buildCues(List builders) { + if (builders.isEmpty()) { + return Collections.emptyList(); + } + LinkedList cues = new LinkedList<>(); + for (Eia608CueBuilder builder : builders) { + if (builder.captionStringBuilder.length() == 0) { + continue; + } + cues.add(builder.build()); + } + return cues; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java index 6b76973278..a22369ca76 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java @@ -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 availableInputBuffers; private final LinkedList availableOutputBuffers; private final TreeSet queuedInputBuffers; private final ParsableByteArray ccData; - - private final SpannableStringBuilder captionStringBuilder; + private final LinkedList cues; private long playbackPositionUs; @@ -235,17 +208,17 @@ public final class Eia608Decoder implements SubtitleDecoder { private int captionMode; private int captionRowCount; + boolean nextRowDown; - private LinkedList cues; - private HashMap 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.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 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]; diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Subtitle.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Subtitle.java index ae0ecd4867..674bee9488 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Subtitle.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Subtitle.java @@ -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 cues; + private List cues; - public Eia608Subtitle(List cues) { + public Eia608Subtitle() { + cues = new LinkedList<>(); + } + + /* package */ void setCues(List cues) { this.cues = 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 cb4eec40f1..4ebfc11237 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 @@ -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. From 35fa5e2ef7d230f5704f562ec5a48d096ec9385f Mon Sep 17 00:00:00 2001 From: Rik Heijdens Date: Wed, 31 Aug 2016 10:57:04 +0200 Subject: [PATCH 07/12] Fixed foreground color midrow codes being handled as background colors --- .../exoplayer2/text/eia608/Eia608Decoder.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java index a22369ca76..16062ec6eb 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java @@ -379,7 +379,7 @@ public final class Eia608Decoder implements SubtitleDecoder { } // Mid row changes. - if ((ccData1 == 0x11 || ccData1 == 0x19) && ccData2 >= 0x20 && ccData2 <= 0x2F) { + if ((ccData1 == 0x11 || ccData1 == 0x19) && (ccData2 >= 0x20 && ccData2 <= 0x2F)) { handleMidrowCode(ccData1, ccData2); } @@ -531,13 +531,18 @@ public final class Eia608Decoder implements SubtitleDecoder { private void handleMidrowCode(byte cc1, byte cc2) { boolean transparentOrUnderline = (cc2 & 0x1) != 0; int attribute = cc2 >> 1 & 0xF; - if ((cc1 & 0x1) != 0) { + if ((cc1 & 0x1) == 0) { // Background Color currentCue.setCharacterStyle(new BackgroundColorSpan(transparentOrUnderline ? COLOR_MAP[attribute] & TRANSPARENCY_MASK : COLOR_MAP[attribute])); } else { - // Foreground color - currentCue.setCharacterStyle(new ForegroundColorSpan(COLOR_MAP[attribute])); + if (attribute < 7) { + // Foreground color + currentCue.setCharacterStyle(new ForegroundColorSpan(COLOR_MAP[attribute])); + } else { + // Italics + currentCue.setCharacterStyle(new StyleSpan(STYLE_ITALIC)); + } if (transparentOrUnderline) { // Text should be underlined currentCue.setCharacterStyle(new UnderlineSpan()); From ad9f76eb62d9f2ee99466de965fc2c6a7eb4f746 Mon Sep 17 00:00:00 2001 From: Rik Heijdens Date: Wed, 31 Aug 2016 11:13:32 +0200 Subject: [PATCH 08/12] Return a copy of the SpannableStringBuilder when building Cues In order to force the SubtitlePainter to redraw cues --- .../android/exoplayer2/text/eia608/Eia608CueBuilder.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java index 336ea6dc27..eb940ec236 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java @@ -178,7 +178,8 @@ import java.util.List; 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, + return new Cue(new SpannableStringBuilder(captionStringBuilder), + Layout.Alignment.ALIGN_NORMAL, cueLine / 100, Cue.LINE_TYPE_FRACTION, Cue.ANCHOR_TYPE_START, cuePosition / 100, Cue.TYPE_UNSET, Cue.DIMEN_UNSET); } From 4d18e623d602c54e2e4d60518627f926ce5402f0 Mon Sep 17 00:00:00 2001 From: Rik Heijdens Date: Wed, 31 Aug 2016 11:55:41 +0200 Subject: [PATCH 09/12] Split Cue Rendering up into 3 phases Split Cue Rendering up into a layout, background drawing and foreground drawing phase so that issues with overlapping 608 captions are being prevented. --- .../text/eia608/Eia608CueBuilder.java | 12 ++--- .../exoplayer2/ui/SubtitlePainter.java | 45 ++++++++++++------- .../android/exoplayer2/ui/SubtitleView.java | 20 ++++++--- 3 files changed, 47 insertions(+), 30 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java index eb940ec236..3531b4dcc2 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java @@ -89,18 +89,14 @@ import java.util.List; this.indent = indent; } + /** + * Indents the Cue. + * @param tabs The amount of tabs to indent the cue with. + */ 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. 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 4ebfc11237..3930de584f 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 @@ -117,30 +117,30 @@ import android.util.Log; } /** - * Draws the provided {@link Cue} into a canvas with the specified styling. + * Creates layouts so that the provided {@link Cue} can be drawn in a {@link Canvas} by calls to + * {@link #drawBackground(Canvas)} and {@link #drawForeground(Canvas)} with the specified styling. *

* A call to this method is able to use cached results of calculations made during the previous * call, and so an instance of this class is able to optimize repeated calls to this method in * which the same parameters are passed. * - * @param cue The cue to draw. + * @param cue The cue to layout. * @param applyEmbeddedStyles Whether styling embedded within the cue should be applied. * @param style The style to use when drawing the cue text. * @param textSizePx The text size to use when drawing the cue text, in pixels. * @param bottomPaddingFraction The bottom padding fraction to apply when {@link Cue#line} is * {@link Cue#DIMEN_UNSET}, as a fraction of the viewport height - * @param canvas The canvas into which to draw. * @param cueBoxLeft The left position of the enclosing cue box. * @param cueBoxTop The top position of the enclosing cue box. * @param cueBoxRight The right position of the enclosing cue box. * @param cueBoxBottom The bottom position of the enclosing cue box. */ - public void draw(Cue cue, boolean applyEmbeddedStyles, CaptionStyleCompat style, float textSizePx, - float bottomPaddingFraction, Canvas canvas, int cueBoxLeft, int cueBoxTop, int cueBoxRight, - int cueBoxBottom) { + public void layout(Cue cue, boolean applyEmbeddedStyles, CaptionStyleCompat style, float textSizePx, + float bottomPaddingFraction, int cueBoxLeft, int cueBoxTop, int cueBoxRight, + int cueBoxBottom) { CharSequence cueText = cue.text; if (TextUtils.isEmpty(cueText)) { - // Nothing to draw. + // Nothing to layout. return; } if (!applyEmbeddedStyles) { @@ -169,7 +169,6 @@ import android.util.Log; && this.parentRight == cueBoxRight && this.parentBottom == cueBoxBottom) { // We can use the cached layout. - drawLayout(canvas); return; } @@ -272,19 +271,17 @@ import android.util.Log; this.textLeft = textLeft; this.textTop = textTop; this.textPaddingX = textPaddingX; - - drawLayout(canvas); } /** - * Draws {@link #textLayout} into the provided canvas. + * Draws the background of the {@link #textLayout} into the provided canvas. * - * @param canvas The canvas into which to draw. + * @param canvas The canvas into which to layout. */ - private void drawLayout(Canvas canvas) { + public void drawBackground(Canvas canvas) { final StaticLayout layout = textLayout; if (layout == null) { - // Nothing to draw. + // Nothing to layout. return; } @@ -311,6 +308,24 @@ import android.util.Log; } } + canvas.restoreToCount(saveCount); + } + + /** + * Draws the foreground of the {@link #textLayout} into the provided canvas. + * + * @param canvas The canvas into which to layout. + */ + public void drawForeground(Canvas canvas) { + final StaticLayout layout = textLayout; + if (layout == null) { + // Nothing to layout. + return; + } + + int saveCount = canvas.save(); + canvas.translate(textLeft, textTop); + if (edgeType == CaptionStyleCompat.EDGE_TYPE_OUTLINE) { textPaint.setStrokeJoin(Join.ROUND); textPaint.setStrokeWidth(outlineWidth); @@ -320,7 +335,7 @@ import android.util.Log; } else if (edgeType == CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW) { textPaint.setShadowLayer(shadowRadius, shadowOffset, shadowOffset, edgeColor); } else if (edgeType == CaptionStyleCompat.EDGE_TYPE_RAISED - || edgeType == CaptionStyleCompat.EDGE_TYPE_DEPRESSED) { + || edgeType == CaptionStyleCompat.EDGE_TYPE_DEPRESSED) { boolean raised = edgeType == CaptionStyleCompat.EDGE_TYPE_RAISED; int colorUp = raised ? Color.WHITE : edgeColor; int colorDown = raised ? edgeColor : Color.WHITE; diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java index 0c8d9ef92e..70a0787352 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java @@ -15,6 +15,11 @@ */ 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.text.TextRenderer; +import com.google.android.exoplayer2.util.Util; + import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; @@ -23,10 +28,7 @@ import android.util.AttributeSet; import android.util.TypedValue; import android.view.View; import android.view.accessibility.CaptioningManager; -import com.google.android.exoplayer2.text.CaptionStyleCompat; -import com.google.android.exoplayer2.text.Cue; -import com.google.android.exoplayer2.text.TextRenderer; -import com.google.android.exoplayer2.util.Util; + import java.util.ArrayList; import java.util.List; @@ -230,7 +232,7 @@ public final class SubtitleView extends View implements TextRenderer.Output { int right = getRight() + getPaddingRight(); int bottom = rawBottom - getPaddingBottom(); if (bottom <= top || right <= left) { - // No space to draw subtitles. + // No space to layout subtitles. return; } @@ -242,8 +244,12 @@ public final class SubtitleView extends View implements TextRenderer.Output { } for (int i = 0; i < cueCount; i++) { - painters.get(i).draw(cues.get(i), applyEmbeddedStyles, style, textSizePx, - bottomPaddingFraction, canvas, left, top, right, bottom); + painters.get(i).layout(cues.get(i), applyEmbeddedStyles, style, textSizePx, + bottomPaddingFraction, left, top, right, bottom); + painters.get(i).drawBackground(canvas); + } + for (int i = 0; i < cueCount; i++) { + painters.get(i).drawForeground(canvas); } } From 26fe1dc2384902ba3f3b5c36afca03162fc0dc0d Mon Sep 17 00:00:00 2001 From: Rik Heijdens Date: Thu, 1 Sep 2016 15:01:17 +0200 Subject: [PATCH 10/12] Removed unnecessary member variable --- .../google/android/exoplayer2/text/eia608/Eia608Decoder.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java index 16062ec6eb..48fa816977 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java @@ -208,7 +208,6 @@ public final class Eia608Decoder implements SubtitleDecoder { private int captionMode; private int captionRowCount; - boolean nextRowDown; // The Cue that's currently being built and decoded. private Eia608CueBuilder currentCue; @@ -233,7 +232,6 @@ public final class Eia608Decoder implements SubtitleDecoder { ccData = new ParsableByteArray(); subtitle = new Eia608Subtitle(); cues = new LinkedList<>(); - nextRowDown = false; setCaptionMode(CC_MODE_UNKNOWN); captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT; @@ -322,7 +320,6 @@ public final class Eia608Decoder implements SubtitleDecoder { playbackPositionUs = 0; currentCue = new Eia608CueBuilder(); cues.clear(); - nextRowDown = false; repeatableControlSet = false; repeatableControlCc1 = 0; repeatableControlCc2 = 0; @@ -501,7 +498,7 @@ public final class Eia608Decoder implements SubtitleDecoder { // For PAC layout see: https://en.wikipedia.org/wiki/EIA-608#Control_commands // Parse the "next row down" toggle. - nextRowDown = (cc2 & 0x20) != 0; + boolean nextRowDown = (cc2 & 0x20) != 0; int row = ROW_INDICES[cc1 & 0x7]; if (row != currentCue.getRow() || nextRowDown) { From b87463a85791e1e1600413cedf4c9adfd8615a64 Mon Sep 17 00:00:00 2001 From: Rik Heijdens Date: Wed, 14 Sep 2016 09:32:53 -0400 Subject: [PATCH 11/12] Fixed an off by one error and fixed iterating through the cues --- .../text/eia608/Eia608CueBuilder.java | 8 +++---- .../exoplayer2/text/eia608/Eia608Decoder.java | 21 +++++++++++-------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java index 3531b4dcc2..aee60cb38b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java @@ -1,8 +1,5 @@ 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; @@ -10,6 +7,9 @@ import android.text.style.CharacterStyle; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.util.Assertions; + import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; @@ -73,7 +73,7 @@ import java.util.List; * @return true if rolling was possible. */ public boolean rollUp() { - if (row < 1) { + if (row <= 1) { return false; } setRow(row - 1); diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java index 48fa816977..086178622b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Decoder.java @@ -15,6 +15,13 @@ */ package com.google.android.exoplayer2.text.eia608; +import android.graphics.Color; +import android.graphics.Typeface; +import android.text.style.BackgroundColorSpan; +import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; +import android.text.style.UnderlineSpan; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SubtitleDecoder; @@ -24,14 +31,8 @@ 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.style.BackgroundColorSpan; -import android.text.style.ForegroundColorSpan; -import android.text.style.StyleSpan; -import android.text.style.UnderlineSpan; - import java.util.Collections; +import java.util.Iterator; import java.util.LinkedList; import java.util.TreeSet; @@ -472,10 +473,12 @@ public final class Eia608Decoder implements SubtitleDecoder { // 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) { + Iterator iterator = cues.iterator(); + while (iterator.hasNext()) { + Eia608CueBuilder cue = iterator.next(); // Roll up all the other rows. if (!cue.rollUp()) { - cues.remove(cue); + iterator.remove(); } } currentCue = new Eia608CueBuilder(); From 98a5e199f9580a6a0d67096597e74388c064e587 Mon Sep 17 00:00:00 2001 From: Rik Heijdens Date: Wed, 14 Sep 2016 09:47:07 -0400 Subject: [PATCH 12/12] Corrected vertical positioning --- .../google/android/exoplayer2/text/eia608/Eia608CueBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java index aee60cb38b..ffecf8b730 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608CueBuilder.java @@ -171,7 +171,7 @@ import java.util.List; public Cue build() { closeSpans(); - float cueLine = 10 + (5.33f * row); + float cueLine = 10 + (5.33f * (row - 1)); float cuePosition = 10 + (2.5f * indent); cuePosition = (tabOffset * 2.5f) + cuePosition; return new Cue(new SpannableStringBuilder(captionStringBuilder),