Skip to content

Commit

Permalink
feat(cea): CEA-708 Decoder (#2807)
Browse files Browse the repository at this point in the history
This pertains to #2648 (although this is a new feature, not a replacement) and #1404. A CEA-708 decoder that follows the CEA-708-E standard, decodes closed caption data from User Data Registered by Rec. ITU-T T.35 SEI messages, and returns them as cues in Shaka's internal cue format. Furthermore, this pull request fixes and cements some of the logic surrounding CEA-608 and CEA-708 tag parsing on the Dash Manifest Parser.

Format:
Similar to the CEA-608 decoder, cues are emitted in Shaka's internal format (lib/text/cue.js). This decoder makes use of nested cues. The top level cue is always a blank cue with no text, and each nested cue inside it contains text, as well as a specific style, or linebreak cues to facilitate line breaks. This also allows for inline style (color, italics, underline) changes.

Details:
- ASCII (G0), Latin-1 (G1), and CEA-708 specific charsets (G2 and G3) all supported.
- Underlines, colors, and Italics supported, set as a property on each nested cue.
- Positioning of text is supported. (Exception: In CEA-708 the default positioning is left, in this decoder it is centered.)
- Positioning of windows not supported, but relevant fields that could be used to support this are extracted and left as a TODO.
  • Loading branch information
muhammadharis authored Sep 10, 2020
1 parent 9a59f4d commit 54ff2d8
Show file tree
Hide file tree
Showing 27 changed files with 3,717 additions and 778 deletions.
4 changes: 4 additions & 0 deletions build/types/cea
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# Inband closed caption support.

+../../lib/cea/cea_decoder.js
+../../lib/cea/cea_utils.js
+../../lib/cea/cea608_data_channel.js
+../../lib/cea/cea608_memory.js
+../../lib/cea/cea708_service.js
+../../lib/cea/cea708_window.js
+../../lib/cea/dtvcc_packet_builder.js
+../../lib/cea/i_caption_decoder.js
+../../lib/cea/i_cea_parser.js
+../../lib/cea/mp4_cea_parser.js
Expand Down
11 changes: 6 additions & 5 deletions lib/cea/cea608_data_channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
goog.provide('shaka.cea.Cea608DataChannel');

goog.require('shaka.cea.Cea608Memory');
goog.require('shaka.cea.CeaUtils');


/**
Expand Down Expand Up @@ -114,7 +115,7 @@ shaka.cea.Cea608DataChannel = class {
const attr = (b2 & 0x1E) >> 1;

// Set up the defaults.
let textColor = shaka.cea.Cea608Memory.DEFAULT_TXT_COLOR;
let textColor = shaka.cea.CeaUtils.DEFAULT_TXT_COLOR;
let italics = false;


Expand Down Expand Up @@ -157,7 +158,7 @@ shaka.cea.Cea608DataChannel = class {
this.curbuf_.setTextColor(textColor);

// Clear the background color, since new row (PAC) should reset ALL styles.
this.curbuf_.setBackgroundColor(shaka.cea.Cea608Memory.DEFAULT_BG_COLOR);
this.curbuf_.setBackgroundColor(shaka.cea.CeaUtils.DEFAULT_BG_COLOR);
}

/**
Expand All @@ -169,13 +170,13 @@ shaka.cea.Cea608DataChannel = class {
// Clear all pre-existing midrow style attributes.
this.curbuf_.setUnderline(false);
this.curbuf_.setItalics(false);
this.curbuf_.setTextColor(shaka.cea.Cea608Memory.DEFAULT_TXT_COLOR);
this.curbuf_.setTextColor(shaka.cea.CeaUtils.DEFAULT_TXT_COLOR);

// Mid-row attrs use a space.
this.curbuf_.addChar(
shaka.cea.Cea608Memory.CharSet.BASIC_NORTH_AMERICAN, ' '.charCodeAt(0));

let textColor = shaka.cea.Cea608Memory.DEFAULT_TXT_COLOR;
let textColor = shaka.cea.CeaUtils.DEFAULT_TXT_COLOR;
let italics = false;

// Midrow codes set underline on last (LSB) bit.
Expand All @@ -200,7 +201,7 @@ shaka.cea.Cea608DataChannel = class {
* @private
*/
controlBackgroundAttribute_(b1, b2) {
let backgroundColor = shaka.cea.Cea608Memory.DEFAULT_BG_COLOR;
let backgroundColor = shaka.cea.CeaUtils.DEFAULT_BG_COLOR;
if ((b1 & 0x07) === 0x0) {
// If background provided, last 3 bits of b1 are |0|0|0|. Color is in b2.
backgroundColor = shaka.cea.Cea608DataChannel.BG_COLORS[(b2 & 0xe) >> 1];
Expand Down
204 changes: 9 additions & 195 deletions lib/cea/cea608_memory.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
*/

goog.provide('shaka.cea.Cea608Memory');
goog.provide('shaka.cea.Cea608Char');

goog.require('shaka.cea.CeaUtils');
goog.require('shaka.text.Cue');


Expand All @@ -21,7 +21,7 @@ shaka.cea.Cea608Memory = class {
constructor(fieldNum, channelNum) {
/**
* Buffer for storing decoded characters.
* @private @const {!Array<!Array<!shaka.cea.Cea608Char>>}
* @private @const {!Array<!Array<!shaka.cea.CeaUtils.StyledChar>>}
*/
this.rows_ = [];

Expand Down Expand Up @@ -62,12 +62,12 @@ shaka.cea.Cea608Memory = class {
/**
* @private {!string}
*/
this.textColor_ = shaka.cea.Cea608Memory.DEFAULT_TXT_COLOR;
this.textColor_ = shaka.cea.CeaUtils.DEFAULT_TXT_COLOR;

/**
* @private {!string}
*/
this.backgroundColor_ = shaka.cea.Cea608Memory.DEFAULT_BG_COLOR;
this.backgroundColor_ = shaka.cea.CeaUtils.DEFAULT_BG_COLOR;

this.reset();
}
Expand All @@ -80,121 +80,10 @@ shaka.cea.Cea608Memory = class {
*/
forceEmit(startTime, endTime) {
const stream = `CC${(this.fieldNum_<< 1) | this.channelNum_ +1}`;

// Find the first and last row that contains characters.
let firstNonEmptyRow = -1;
let lastNonEmptyRow = -1;

for (let i = 0; i < this.rows_.length; i++) {
if (this.rows_[i].length) {
firstNonEmptyRow = i;
break;
}
}

for (let i = this.rows_.length - 1; i >= 0; i--) {
if (this.rows_[i].length) {
lastNonEmptyRow = i;
break;
}
}

// Exit early if no non-empty row was found.
if (firstNonEmptyRow === -1 || lastNonEmptyRow === -1) {
return null;
}

// Keeps track of the current styles for a cue being emitted.
let currentUnderline = false;
let currentItalics = false;
let currentTextColor = shaka.cea.Cea608Memory.DEFAULT_TXT_COLOR;
let currentBackgroundColor = shaka.cea.Cea608Memory.DEFAULT_BG_COLOR;

// Create first cue that will be nested in top level cue. Default styles.
let currentCue = this.createStyledCue_(startTime, endTime,
currentUnderline, currentItalics,
currentTextColor, currentBackgroundColor);

// Logic: Reduce rows into a single top level cue containing nested cues.
// Each nested cue corresponds either a style change or a line break.
const topLevelCue = new shaka.text.Cue(
startTime, endTime, /* payload= */ '');

for (let i = firstNonEmptyRow; i <= lastNonEmptyRow; i++) {
for (const styledChar of this.rows_[i]) {
const underline = styledChar.isUnderlined();
const italics = styledChar.isItalicized();
const textColor = styledChar.getTextColor();
const backgroundColor = styledChar.getBackgroundColor();

// If any style properties have changed, we need to open a new cue.
if (underline != currentUnderline || italics != currentItalics ||
textColor != currentTextColor ||
backgroundColor != currentBackgroundColor) {
// Push the currently built cue and start a new cue, with new styles.
if (currentCue.payload) {
topLevelCue.nestedCues.push(currentCue);
}
currentCue = this.createStyledCue_(startTime, endTime,
underline, italics, textColor, backgroundColor);

currentUnderline = underline;
currentItalics = italics;
currentTextColor = textColor;
currentBackgroundColor = backgroundColor;
}

currentCue.payload += styledChar.getChar();
}
if (currentCue.payload) {
topLevelCue.nestedCues.push(currentCue);
}

// Create and push a linebreak cue to create a new line.
if (i !== lastNonEmptyRow) {
const spacerCue = new shaka.text.Cue(
startTime, endTime, /* payload= */ '');
spacerCue.spacer = true;
topLevelCue.nestedCues.push(spacerCue);
}

// Create a new cue.
currentCue = this.createStyledCue_(startTime, endTime,
currentUnderline, currentItalics,
currentTextColor, currentBackgroundColor);
}

if (topLevelCue.nestedCues.length) {
return {
cue: topLevelCue,
stream,
};
}

return null;
}

/**
* @param {!number} startTime
* @param {!number} endTime
* @param {!boolean} underline
* @param {!boolean} italics
* @param {!string} txtColor
* @param {!string} bgColor
* @return {!shaka.text.Cue}
* @private
*/
createStyledCue_(startTime, endTime, underline, italics, txtColor, bgColor) {
const cue = new shaka.text.Cue(startTime, endTime, /* payload= */ '');
if (underline) {
cue.textDecoration.push(shaka.text.Cue.textDecoration.UNDERLINE);
}
if (italics) {
cue.fontStyle = shaka.text.Cue.fontStyle.ITALIC;
}
cue.color = txtColor;
cue.backgroundColor = bgColor;
return cue;
return shaka.cea.CeaUtils.getParsedCaption(
topLevelCue, stream, this.rows_, startTime, endTime);
}

/**
Expand Down Expand Up @@ -273,8 +162,9 @@ shaka.cea.Cea608Memory = class {
}

if (char) {
const styledChar = new shaka.cea.Cea608Char(char, this.underline_,
this.italics_, this.backgroundColor_, this.textColor_);
const styledChar = new shaka.cea.CeaUtils.StyledChar(
char, this.underline_, this.italics_,
this.backgroundColor_, this.textColor_);
this.rows_[this.row_].push(styledChar);
}
}
Expand Down Expand Up @@ -360,88 +250,12 @@ shaka.cea.Cea608Memory = class {
}
};

shaka.cea.Cea608Char = class {
constructor(character, underline, italics, backgroundColor, textColor) {
/**
* @private {!string}
*/
this.character_ = character;

/**
* @private {!boolean}
*/
this.underline_ = underline;

/**
* @private {!boolean}
*/
this.italics_ = italics;

/**
* @private {!string}
*/
this.backgroundColor_ = backgroundColor;

/**
* @private {!string}
*/
this.textColor_ = textColor;
}

/**
* @return {!string}
*/
getChar() {
return this.character_;
}

/**
* @return {!boolean}
*/
isUnderlined() {
return this.underline_;
}

/**
* @return {!boolean}
*/
isItalicized() {
return this.italics_;
}

/**
* @return {!string}
*/
getBackgroundColor() {
return this.backgroundColor_;
}

/**
* @return {!string}
*/
getTextColor() {
return this.textColor_;
}
};

/**
* Maximum number of rows in the buffer.
* @const {!number}
*/
shaka.cea.Cea608Memory.CC_ROWS = 15;

/**
* Default background color for text.
* @const {!string}
*/
shaka.cea.Cea608Memory.DEFAULT_BG_COLOR = 'black';

/**
* Default text color.
* @const {!string}
*/
shaka.cea.Cea608Memory.DEFAULT_TXT_COLOR = 'white';

/**
* Characters sets.
* @const @enum {!number}
Expand Down
Loading

0 comments on commit 54ff2d8

Please sign in to comment.