Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EIA608 Styling #1312

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer.text.eia608;

import android.graphics.Color;

/* package */ final class ClosedCaptionCtrl extends ClosedCaption {

/**
Expand Down Expand Up @@ -59,7 +61,6 @@

public static final byte BACKSPACE = 0x21;


public static final byte MID_ROW_CHAN_1 = 0x11;
public static final byte MID_ROW_CHAN_2 = 0x19;

Expand All @@ -69,6 +70,45 @@
public static final byte TAB_OFFSET_CHAN_1 = 0x17;
public static final byte TAB_OFFSET_CHAN_2 = 0x1F;

public static final int COLUMN_INDEX_BASE = 1; // standard uses 1 based index for positioning

private static final int[] COLOR_VALUES = {
Color.WHITE,
Color.GREEN,
Color.BLUE,
Color.CYAN,
Color.RED,
Color.YELLOW,
Color.MAGENTA,
Color.TRANSPARENT // Last color setting should be kept
};

// for debug purposes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove all the debug stuff from committed code? Feel free to keep it locally, but it's probably >50% of the whole change to this file! Note that getIndentation is also not used anywhere, so that method can go or be deferred until a subsequent change also.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added getIndentation() while I was trying to figure out the meaning of the bits of the stream. Although it is not useful without full positioning, it might help anyone how is attempting to update the code.
Now, when you stop at a break point, Android Studio is able to write out every piece of information about the incoming Commands instead of a single HEX value. Shall I remove it? (Either ways I will add most of it back with the next change set implementing correct positioning).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be preferable to remove it until it's needed, yes. It makes it easier to find "the change that added positioning" if its contained within a single change, as opposed to some of it arriving first :).

private static final String[] COLOR_NAMES = {
"WHITE",
"GREEN",
"BLUE",
"CYAN",
"RED",
"YELLOW",
"MAGENTA",
"KEEP PREVIOUS COLOR" // Used to turn on Italics and UnderLine.
};

// The final position of the text should be one of the predefined ROWs of the display.
// The target row is selected by 3 bits of CC1 and 1 bit of CC2. So let's have an array of 16
// indices that can be addressed by the incoming 4 bits.
// There is one duplication, row 11 is the default, where the bit in CC2 is not used for row
// selection, so the first 2 value will be 11: if the CC1 has the value of 0x10 or 0x18 than the
// target row is '11' irrespective of CC2.
final static short[] ROW_INDICES = { 11, 11, 1, 2, 3, 4, 12, 13, 14, 15, 5, 6, 7, 8, 9, 10 };

// 608 parser drops the first bits of the incoming data (parity bit), so both bytes has the max
// value of 0x7F.
// For Closed Captioning, cc1 values in the range 0 - 0xF are not defined, they are used as XDS
// control values (independent from closed caption control codes), so probably the parser should
// drop such values: the following functions do not really expect cc1 values
// outside the 0x10 - 0x7F range.
public final byte cc1;
public final byte cc2;

Expand All @@ -91,11 +131,179 @@ public boolean isTabOffsetCode() {
}

public boolean isPreambleAddressCode() {
// Note: Current implementation might throw compile warnings as cc2 cannot be bigger than
// 0x7F when we call this function.
// Preamble Code could also be checked as
// cc1 & 0x70 == 0x10 // bits required: 0bx001xxxx
// and
// cc2 & 0x40 == 0x40 // bits required: 0bx1xxxxxx
return (cc1 >= 0x10 && cc1 <= 0x1F) && (cc2 >= 0x40 && cc2 <= 0x7F);
}

public boolean isRepeatable() {
return cc1 >= 0x10 && cc1 <= 0x1F;
}

// for debug purposes only
public String getMidRowCodeMeaning() {
String styleStr = COLOR_NAMES[ getMidRowControlColorIdx() ];

if (isUnderline()) {
styleStr += " + UNDERLINE";
}

return styleStr;
}

// returns the index that can be used to address the predefined arrays for selecting the color
// based on the CC2 byte. Only call this function, if you are sure, that this is a mid row
// command.
private int getMidRowControlColorIdx() {
return (cc2 - 0x20) / 2; // first color is 0x20, than every second value is a new color
}

// note: TRANSPARENT should be handled carefully! That is used for Italics and underline changes
int getMidRowColorValue() {
return COLOR_VALUES[ getMidRowControlColorIdx() ];
}

// returns 0 based index (while the standard mentions channel 1 and 2)!
public int getPreambleAddressCodeChannel() {
// the first byte is between 0x10 - 0x17 for channel 1 and
// between 0x18 - 0x1F for channel 2.
return (cc1 > 0x17) ? 1 : 0;
}

// returns a 1 based index!
public int getPreambleAddressCodePositionRow() {
int cc1Bits = (cc1 & 0x7);
int cc2bit = (cc2 & 0x20) > 0 ? 1 : 0;

int index = (cc1Bits << 1) + cc2bit;

return ROW_INDICES[ index ];
}

// for debug purposes
public String getPreambleAddressCodeMeaning() {
return "Row:"+getPreambleAddressCodePositionRow()
+ "; Col:" + getPreambleAddressCodePositionColumn()
+ "; Color:" + getPreambleColorName()
+ "; italic:" + isPreambleItalic()
+ "; underline:" + isUnderline();
}

// color values are defined for CC2 between 0x40 and 0x4F only. Higher values only set indentation
private int getPreambleColorIdx() {
// The bits of the two bytes of the Preamble Are:
// CC1: 0bP001CRRR CC2: 0b1RSSSSU
// P is the parity bit
// C is the 'Channel selector'
// R is a bit that is used for 'Row selection'
// U is the 'Underline Flag'
// S is a bit used for 'Style selection'

// so for color selection we are only interested in the 4 bits of cc2 marked with 'S'. If the
// most significant one is set, than the color is White, and the leftover bits mean indentation.
if((cc2 & 0x10)> 0) {
return 0; // the index of white
}

// The last bit is irrelevant, that is the "Underline Flag" -> we will divide by 2.
// Let's mask the 4 bits we are interested in: 0b11110 = 0x1E.
return (cc2 & 0x1E) / 2;
}

public int getPreambleColor() {
return COLOR_VALUES[getPreambleColorIdx()];
}

// for debug purposes
public String getPreambleColorName() {
return COLOR_NAMES[getPreambleColorIdx()];
}

public boolean isPreambleItalic() {
// The last bit ("Underline Flag") is irrelevant
// The 6th least significant bit is the part of the Row selection, so only the bits 2-5 count.
// For italic we need the least significant bits to be exactly 0b0111x so the entire byte is
// 0bxxR0111U, where x is "don't care", R is the 'Row Selector', U is the 'Underline Flag'
return (cc2 & 0x1E) == 0xE;
}

// Indentation is the column address of the Caption on the final display. The useful area of the
// screen should be divided into 32 equal columns. The useful area is the center 80% of the
// screen (10-10% should be left empty on both left and right side). Column 0 and 33 can be
// used to display a "solid space to improve legibility", but cannot contain displayable
// characters.
// A single column can be further divided into 4 equal parts. Tabs can be used (0-3) for
// positioning inside a column.
// Fun fact: the standard says that for screens with aspect ration of 16x9, the screen should be
// divided into 42 equal columns instead of 32. So the text positioning should be depending on
// the aspect ratio of the video content. What if the content is letterboxed? Than aspect ration
// checks will return incorrect results. I do not introduce this dependency at this time.
// See Federal Communications Commission §79.102
public int getPreambleAddressCodePositionColumn() {
// The bits of the two bytes of the Preamble Are:
// CC1: 0bP001CRRR CC2: 0b1RSSSSU
// P is the parity bit for
// C is the 'Channel selector'
// R is a bit that is used for 'Row selection'
// U is the 'Underline Flag'
// S is a bit used for 'Style selection'

// we are again interested in the 4 bits marked with S. If the most significant one is set to 1,
// than the bits mean indentation, color otherwise.
if ((cc2 & 0x10) == 0) {
return COLUMN_INDEX_BASE; // the S bits mean a color value.
}

// If we are here, the S bits mean indentation.
// Lets remove the unnecessary bits
int indentValue = (cc2 & 0xE) >> 1; // Note: we can only have values between 0 and 7 from now on.

// the required indentation is 4 times of this value represented by the bits.
// Note: Indentation is between 0 and 28 columns this way, and there can be 0-3 tabs also added
// by following TAB offset codes.
return indentValue * 4 + COLUMN_INDEX_BASE;
}

// last bit of the styling commands (Preamble Address Code or Mid Row CODE) is "Underline Flag".
public boolean isUnderline() {
return (cc2 & 1) == 1;
}

// for debug purposes
public String getMiscControlCodeMeaning() {
switch ( cc2 ) {
case RESUME_CAPTION_LOADING: return "RESUME_CAPTION_LOADING";
case ROLL_UP_CAPTIONS_2_ROWS: return "ROLL_UP_CAPTIONS_2_ROWS";
case ROLL_UP_CAPTIONS_3_ROWS: return "ROLL_UP_CAPTIONS_3_ROWS";
case ROLL_UP_CAPTIONS_4_ROWS: return "ROLL_UP_CAPTIONS_4_ROWS";
case RESUME_DIRECT_CAPTIONING: return "RESUME_DIRECT_CAPTIONING";
case END_OF_CAPTION: return "END_OF_CAPTION";
case ERASE_DISPLAYED_MEMORY: return "ERASE_DISPLAYED_MEMORY";
case CARRIAGE_RETURN: return "CARRIAGE_RETURN";
case ERASE_NON_DISPLAYED_MEMORY: return "ERASE_NON_DISPLAYED_MEMORY";
case BACKSPACE: return "BACKSPACE";
}
return "UNKNOWN";
}

// for debug purposes, useful as the IDE shows this string value of the commands while debugging
@Override
public String toString() {

if (isPreambleAddressCode()) {
return "PAC: " + getPreambleAddressCodeMeaning();
} else if (isMidRowCode()) {
return "MRC: " + getMidRowCodeMeaning();
} else if (isTabOffsetCode()) {
return "TAB: " + (cc2 - 0x20) ;
} else if (isMiscCode()){
return "MISC: " + getMiscControlCodeMeaning();
}

return "UNKNOWN - CC1:" + Integer.toHexString(cc1) + "; CC2:" + Integer.toHexString(cc2);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,9 @@ public ClosedCaptionText(String text) {
this.text = text;
}

// for debug purposes
@Override
public String toString() {
return text;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.text.eia608;

import android.text.Layout;
import android.text.SpannableStringBuilder;
import com.google.android.exoplayer.text.Cue;

public class Eia608CueBuilder {
private SpannableStringBuilder text;
private float position;
private float line;
private int rowIndex;

public Eia608CueBuilder() {
reset();
}

public Eia608CueBuilder(Eia608CueBuilder other) {
text = new SpannableStringBuilder(other.text);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no idea what changed, but this line started throwing exception with the same content if other.text is null. So an extra check is probably necessary here :)

position = other.position;
line = other.line;
rowIndex = other.rowIndex;
}

public void reset() {
text = null;
position = Cue.DIMEN_UNSET;
line = Cue.DIMEN_UNSET;
rowIndex = 15; // bottom row is the default
}

public Cue build() {
return new Cue(text, Layout.Alignment.ALIGN_NORMAL,
line, Cue.LINE_TYPE_FRACTION, Cue.ANCHOR_TYPE_START,
position, Cue.TYPE_UNSET, Cue.DIMEN_UNSET);
}

public boolean isEmpty() {
return (text == null) || (text.length() == 0);
}

public Eia608CueBuilder setText(SpannableStringBuilder aText) {
text = aText;
return this;
}

public void setRow(int rowIdx) {
if ((rowIdx < 1) || (15 < rowIdx)) {
this.line = 0.9f;
return;
}

rowIndex = rowIdx; // saved for roll-up feature

// 10% of screen is left of for safety reasons (analog overscan)
// the leftover 80% is divided into 15 equal rows. The problem is that the font size
// must match the row line height for this, so at the moment, I scale to 90% instead, to avoid
// overlap of the borders around the rows.
// -1 as the row and column indices are 1 based in the spec
this.line = (rowIdx - 1) / 15f * 0.9f + 0.05f;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs minor updates to get closer to the original standard. To fit 15 lines into the safe area, previously I set 15 points (position is the top left corner of a line) evenly in the middle 80% of the screen. But positioning the bottom line exactly to 80% would cause the text of that last line go under its top left corner - effectively outside the intended safe area.

So let's distribute "80% - font size" evenly:

final float FONT_SIZE_PERCENTAGE = 0.8f / 15f;
this.line = (rowIdx - 1) / 15f * (0.8f - FONT_SIZE_PERCENTAGE) + 0.1f;

But this pulls all the lines closer to each other, and the default subtitle rendering might cause overlapping edges with the default font and subtitle background setting. I solved this by reordering the calls in cuePainter:

  • Draw all cue backgrounds first
  • then Draw all texts
    This way there are no issues of any overlaps with the default fonts and settings.

}

/**
* Decrease the current row and reposition the captions to the new location
* @return true if rolling was possible
*/
public boolean rollUp() {
if (rowIndex <= 1) {
return false;
}

setRow(rowIndex - 1);
return true;
}

public void setColumn(int columnIdx, int additionalTabs) {
// the original standard defines 32 columns for the safe area of the screen (middle 80%)
// but it also mentions that widescreen displays should use 42 columns. As the incoming data
// does not know about the display size, maybe if the content uses widescreen aspect ratio,
// than it will contain 42 columns. I never met such, but we should not forget about it...
if (columnIdx < 1 || 32 < columnIdx) {
this.position = 0.1f;
return;
}

// -1 as the row and column indices are 1 based in the spec
this.position = (((columnIdx - 1 + additionalTabs) / 32f) * 0.8f) + 0.1f;
}
}
Loading