/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.cea.Cea608DataChannel');
goog.require('shaka.cea.Cea608Memory');
goog.require('shaka.cea.CeaUtils');
goog.require('shaka.log');
/**
* 608 closed captions channel.
*/
shaka.cea.Cea608DataChannel = class {
/**
* @param {number} fieldNum Field number.
* @param {number} channelNum Channel number.
*/
constructor(fieldNum, channelNum) {
/**
* Current Caption Type.
* @public {!shaka.cea.Cea608DataChannel.CaptionType}
*/
this.type_ = shaka.cea.Cea608DataChannel.CaptionType.NONE;
/**
* Text buffer for CEA-608 "text mode". Although, we don't emit text mode.
* So, this buffer serves as a no-op placeholder, just in case we receive
* captions that toggle text mode.
* @private @const {!shaka.cea.Cea608Memory}
*/
this.text_ =
new shaka.cea.Cea608Memory(fieldNum, channelNum);
/**
* Displayed memory.
* @private {!shaka.cea.Cea608Memory}
*/
this.displayedMemory_ =
new shaka.cea.Cea608Memory(fieldNum, channelNum);
/**
* Non-displayed memory.
* @private {!shaka.cea.Cea608Memory}
*/
this.nonDisplayedMemory_ =
new shaka.cea.Cea608Memory(fieldNum, channelNum);
/**
* Points to current buffer.
* @private {!shaka.cea.Cea608Memory}
*/
this.curbuf_ = this.nonDisplayedMemory_;
/**
* End time of the previous caption, serves as start time of next caption.
* @private {number}
*/
this.prevEndTime_ = 0;
/**
* Last control pair, 16 bits representing byte 1 and byte 2
* @private {?number}
*/
this.lastcp_ = null;
}
/**
* Resets channel state.
*/
reset() {
this.type_ = shaka.cea.Cea608DataChannel.CaptionType.NONE;
this.curbuf_ = this.nonDisplayedMemory_;
this.lastcp_ = null;
this.displayedMemory_.reset();
this.nonDisplayedMemory_.reset();
this.text_.reset();
}
/**
* Set the initial PTS, which may not be 0 if we start decoding at a later
* point in the stream. Without this, the first cue's startTime can be way
* off.
*
* @param {number} pts
*/
firstPts(pts) {
this.prevEndTime_ = pts;
}
/**
* Gets the row index from a Preamble Address Code byte pair.
* @param {number} b1 Byte 1.
* @param {number} b2 Byte 2.
* @return {number} Row index.
* @private
*/
pacToRow_(b1, b2) {
const ccrowtab = [
11, 11, // 0x00 or 0x01
1, 2, // 0x02 -> 0x03
3, 4, // 0x04 -> 0x05
12, 13, // 0x06 -> 0x07
14, 15, // 0x08 -> 0x09
5, 6, // 0x0A -> 0x0B
7, 8, // 0x0C -> 0x0D
9, 10, // 0x0E -> 0x0F
];
return ccrowtab[((b1 & 0x07) << 1) | ((b2 >> 5) & 0x01)];
}
/**
* PAC - Preamble Address Code.
* b1 is of the form |P|0|0|1|C|0|ROW|
* b2 is of the form |P|1|N|ATTRIBUTE|U|
* @param {number} b1 Byte 1.
* @param {number} b2 Byte 2.
* @private
*/
controlPac_(b1, b2) {
const row = this.pacToRow_(b1, b2);
// Set up the defaults.
let textColor = shaka.cea.CeaUtils.DEFAULT_TXT_COLOR;
let italics = false;
let indent = null;
// Get PAC index;
let pacIndex;
if (b2 > 0x5f) {
pacIndex = b2 - 0x60;
} else {
pacIndex = b2 - 0x40;
}
if (pacIndex <= 0xd) {
const colorIndex = Math.floor(pacIndex / 2);
textColor = shaka.cea.Cea608DataChannel.TEXT_COLORS[colorIndex];
} else if (pacIndex <= 0xf) {
italics = true; // color stays white
} else {
indent = Math.floor((pacIndex - 0x10) / 2);
}
// PACs toggle underline on the last bit of b2.
const underline = (b2 & 0x01) === 0x01;
if (this.type_ === shaka.cea.Cea608DataChannel.CaptionType.TEXT) {
// Don't execute the PAC if in text mode.
return;
}
// Execute the PAC.
const buf = this.curbuf_;
// Move entire scroll window to a new base in rollup mode.
if (this.type_ === shaka.cea.Cea608DataChannel.CaptionType.ROLLUP &&
row !== buf.getRow()) {
const oldTopRow = 1 + buf.getRow() - buf.getScrollSize();
const newTopRow = 1 + row - buf.getScrollSize();
// Shift up the scroll window.
buf.moveRows(newTopRow, oldTopRow, buf.getScrollSize());
// Clear everything outside of the new scroll window.
buf.resetRows(0, newTopRow - 1);
buf.resetRows(row + 1,
shaka.cea.Cea608Memory.CC_ROWS - row);
}
buf.setRow(row);
buf.setUnderline(underline);
buf.setItalics(italics);
buf.setTextColor(textColor);
buf.setIndent(indent);
// Clear the background color, since new row (PAC) should reset ALL styles.
buf.setBackgroundColor(shaka.cea.CeaUtils.DEFAULT_BG_COLOR);
}
/**
* Mid-Row control code handler.
* @param {number} b2 Byte #2.
* @private
*/
controlMidrow_(b2) {
// Clear all pre-existing midrow style attributes.
this.curbuf_.setUnderline(false);
this.curbuf_.setItalics(false);
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.CeaUtils.DEFAULT_TXT_COLOR;
let italics = false;
// Midrow codes set underline on last (LSB) bit.
const underline = (b2 & 0x01) === 0x01;
// b2 has the form |P|0|1|0|STYLE|U|
textColor = shaka.cea.Cea608DataChannel.TEXT_COLORS[(b2 & 0xe) >> 1];
if (textColor === 'white_italics') {
textColor = 'white';
italics = true;
}
this.curbuf_.setUnderline(underline);
this.curbuf_.setItalics(italics);
this.curbuf_.setTextColor(textColor);
}
/**
* Background attribute control code handler.
* @param {number} b1 Byte #1
* @param {number} b2 Byte #2.
* @private
*/
controlBackgroundAttribute_(b1, b2) {
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];
}
this.curbuf_.setBackgroundColor(backgroundColor);
}
/**
* The Cea608DataChannel control methods implement all CC control operations.
* @param {!shaka.cea.Cea608DataChannel.Cea608Packet} ccPacket
* @return {?shaka.extern.ICaptionDecoder.ClosedCaption}
* @private
*/
controlMiscellaneous_(ccPacket) {
const MiscCmd = shaka.cea.Cea608DataChannel.MiscCmd_;
const b2 = ccPacket.ccData2;
const pts = ccPacket.pts;
let parsedClosedCaption = null;
switch (b2) {
case MiscCmd.RCL:
this.controlRcl_();
break;
case MiscCmd.BS:
this.controlBs_();
break;
// unused (alarm off and alarm on)
case MiscCmd.AOD:
case MiscCmd.AON:
break;
case MiscCmd.DER:
// Delete to End of Row. Not implemented since position not supported.
break;
case MiscCmd.RU2:
parsedClosedCaption = this.controlRu_(2, pts);
break;
case MiscCmd.RU3:
parsedClosedCaption = this.controlRu_(3, pts);
break;
case MiscCmd.RU4:
parsedClosedCaption = this.controlRu_(4, pts);
break;
case MiscCmd.FON:
this.controlFon_();
break;
case MiscCmd.RDC:
this.controlRdc_(pts);
break;
case MiscCmd.TR:
this.controlTr_();
break;
case MiscCmd.RTD:
this.controlRtd_();
break;
case MiscCmd.EDM:
parsedClosedCaption = this.controlEdm_(pts);
break;
case MiscCmd.CR:
parsedClosedCaption = this.controlCr_(pts);
break;
case MiscCmd.ENM:
this.controlEnm_();
break;
case MiscCmd.EOC:
parsedClosedCaption = this.controlEoc_(pts);
break;
}
return parsedClosedCaption;
}
/**
* Handles CR - Carriage Return (Start new row).
* CR only affects scroll windows (Rollup and Text modes).
* Any currently buffered line needs to be emitted, along
* with a window scroll action.
* @param {number} pts in seconds.
* @return {?shaka.extern.ICaptionDecoder.ClosedCaption}
* @private
*/
controlCr_(pts) {
const buf = this.curbuf_;
// Only rollup and text mode is affected, but we don't emit text mode.
if (this.type_ !== shaka.cea.Cea608DataChannel.CaptionType.ROLLUP) {
return null;
}
// Force out the scroll window since the top row will cleared.
const parsedClosedCaption = buf.forceEmit(this.prevEndTime_, pts);
// Calculate the top of the scroll window.
const toprow = (buf.getRow() - buf.getScrollSize()) + 1;
// Shift up the window one row higher.
buf.moveRows(toprow - 1, toprow, buf.getScrollSize());
// Clear out anything that's outside of our current scroll window.
buf.resetRows(0, toprow - 1);
buf.resetRows(buf.getRow(), shaka.cea.Cea608Memory.CC_ROWS - buf.getRow());
// Update the end time so the next caption emits starting at this time.
this.prevEndTime_ = pts;
return parsedClosedCaption;
}
/**
* Handles RU2, RU3, RU4 - Roll-Up, N rows.
* If in TEXT, POPON or PAINTON, any displayed captions are erased.
* This means must force emit entire display buffer.
* @param {number} scrollSize New scroll window size.
* @param {number} pts
* @return {?shaka.extern.ICaptionDecoder.ClosedCaption}
* @private
*/
controlRu_(scrollSize, pts) {
this.curbuf_ = this.displayedMemory_; // Point to displayed memory
const buf = this.curbuf_;
let parsedClosedCaption = null;
// For any type except rollup and text mode, it should be emitted,
// and memories cleared.
if (this.type_ !== shaka.cea.Cea608DataChannel.CaptionType.ROLLUP &&
this.type_ !== shaka.cea.Cea608DataChannel.CaptionType.TEXT) {
parsedClosedCaption = buf.forceEmit(this.prevEndTime_, pts);
// Clear both memories.
this.displayedMemory_.eraseBuffer();
this.nonDisplayedMemory_.eraseBuffer();
// Rollup base row defaults to the last row (15).
buf.setRow(shaka.cea.Cea608Memory.CC_ROWS);
}
this.type_ = shaka.cea.Cea608DataChannel.CaptionType.ROLLUP;
// Set the new rollup window size.
buf.setScrollSize(scrollSize);
return parsedClosedCaption;
}
/**
* Handles flash on.
* @private
*/
controlFon_() {
this.curbuf_.addChar(
shaka.cea.Cea608Memory.CharSet.BASIC_NORTH_AMERICAN,
' '.charCodeAt(0));
}
/**
* Handles EDM - Erase Displayed Mem
* Mode check:
* EDM affects all captioning modes (but not Text mode);
* @param {number} pts
* @return {?shaka.extern.ICaptionDecoder.ClosedCaption}
* @private
*/
controlEdm_(pts) {
const buf = this.displayedMemory_;
let parsedClosedCaption = null;
if (this.type_ !== shaka.cea.Cea608DataChannel.CaptionType.TEXT) {
// Clearing displayed memory means we now know how long
// its contents were displayed, so force it out.
parsedClosedCaption = buf.forceEmit(this.prevEndTime_, pts);
}
buf.resetAllRows();
return parsedClosedCaption;
}
/**
* Handles RDC - Resume Direct Captions. Initiates Paint-On captioning mode.
* RDC does not affect current display, so nothing needs to be forced out yet.
* @param {number} pts in seconds
* @private
*/
controlRdc_(pts) {
this.type_ = shaka.cea.Cea608DataChannel.CaptionType.PAINTON;
// Point to displayed memory.
this.curbuf_ = this.displayedMemory_;
// No scroll window now.
this.curbuf_.setScrollSize(0);
// The next paint-on caption needs this time as the start time.
this.prevEndTime_ = pts;
}
/**
* Handles ENM - Erase Nondisplayed Mem
* @private
*/
controlEnm_() {
this.nonDisplayedMemory_.resetAllRows();
}
/**
* Handles EOC - End Of Caption (flip mem)
* This forces Pop-On mode, and swaps the displayed and nondisplayed memories.
* @private
* @param {number} pts
* @return {?shaka.extern.ICaptionDecoder.ClosedCaption}
*/
controlEoc_(pts) {
let parsedClosedCaption = null;
if (this.type_ !== shaka.cea.Cea608DataChannel.CaptionType.TEXT) {
parsedClosedCaption =
this.displayedMemory_.forceEmit(this.prevEndTime_, pts);
}
// Swap memories
const buf = this.nonDisplayedMemory_;
this.nonDisplayedMemory_ = this.displayedMemory_; // Swap buffers
this.displayedMemory_ = buf;
// Enter Pop-On mode.
this.controlRcl_();
// The caption ended, and so the previous end time should be updated.
this.prevEndTime_ = pts;
return parsedClosedCaption;
}
/**
* Handles RCL - Resume Caption Loading
* Initiates Pop-On style captioning. No need to force anything out upon
* entering Pop-On mode because it does not affect the current display.
* @private
*/
controlRcl_() {
this.type_ = shaka.cea.Cea608DataChannel.CaptionType.POPON;
this.curbuf_ = this.nonDisplayedMemory_;
// No scroll window now
this.curbuf_.setScrollSize(0);
}
/**
* Handles BS - BackSpace.
* @private
*/
controlBs_() {
this.curbuf_.eraseChar();
}
/**
* Handles TR - Text Restart.
* Clears text buffer and resumes Text Mode.
* @private
*/
controlTr_() {
this.text_.reset();
this.controlRtd_(); // Put into text mode.
}
/**
* Handles RTD - Resume Text Display.
* Resumes text mode. No need to force anything out, because Text Mode doesn't
* affect current display. Also, this decoder does not emit Text Mode anyway.
* @private
*/
controlRtd_() {
shaka.log.warnOnce('Cea608DataChannel',
'CEA-608 text mode entered, but is unsupported');
this.curbuf_ = this.text_;
this.type_ = shaka.cea.Cea608DataChannel.CaptionType.TEXT;
}
/**
* Handles a Basic North American byte pair.
* @param {number} b1 Byte 1.
* @param {number} b2 Byte 2.
*/
handleBasicNorthAmericanChar(b1, b2) {
this.curbuf_.addChar(
shaka.cea.Cea608Memory.CharSet.BASIC_NORTH_AMERICAN, b1);
this.curbuf_.addChar(
shaka.cea.Cea608Memory.CharSet.BASIC_NORTH_AMERICAN, b2);
}
/**
* Handles an Extended Western European byte pair.
* @param {number} b1 Byte 1.
* @param {number} b2 Byte 2.
* @private
*/
handleExtendedWesternEuropeanChar_(b1, b2) {
// Get the char set from the LSB, which is the char set toggle bit.
const charSet = b1 & 0x01 ?
shaka.cea.Cea608Memory.CharSet.PORTUGUESE_GERMAN:
shaka.cea.Cea608Memory.CharSet.SPANISH_FRENCH;
this.curbuf_.addChar(charSet, b2);
}
/**
* Handles a tab offset.
*
* @param {number} offset
* @private
*/
handleOffset_(offset) {
this.curbuf_.setOffset(offset);
}
/**
* Decodes control code.
* Three types of control codes:
* Preamble Address Codes, Mid-Row Codes, and Miscellaneous Control Codes.
* @param {!shaka.cea.Cea608DataChannel.Cea608Packet} ccPacket
* @return {?shaka.extern.ICaptionDecoder.ClosedCaption}
*/
handleControlCode(ccPacket) {
const b1 = ccPacket.ccData1;
const b2 = ccPacket.ccData2;
// FCC wants control codes transmitted twice, and that will often be
// seen in broadcast captures. If the very next frame has a duplicate
// control code, that duplicate is ignored. Note that this only applies
// to the very next frame, and only for one match.
if (this.lastcp_ === ((b1 << 8) | b2)) {
this.lastcp_ = null;
return null;
}
// Remember valid control code for checking in next frame!
this.lastcp_ = (b1 << 8) | b2;
if (this.isPAC_(b1, b2)) {
this.controlPac_(b1, b2);
} else if (this.isMidrowStyleChange_(b1, b2)) {
this.controlMidrow_(b2);
} else if (this.isBackgroundAttribute_(b1, b2)) {
this.controlBackgroundAttribute_(b1, b2);
} else if (this.isSpecialNorthAmericanChar_(b1, b2)) {
this.curbuf_.addChar(
shaka.cea.Cea608Memory.CharSet.SPECIAL_NORTH_AMERICAN, b2);
} else if (this.isExtendedWesternEuropeanChar_(b1, b2)) {
this.handleExtendedWesternEuropeanChar_(b1, b2);
} else if (this.isMiscellaneous_(b1, b2)) {
return this.controlMiscellaneous_(ccPacket);
} else if (this.isOffset_(b1, b2)) {
const offset = b2 - 0x20;
this.handleOffset_(offset);
}
return null;
}
/**
* Checks if this is a Miscellaneous control code.
* @param {number} b1 Byte 1.
* @param {number} b2 Byte 2.
* @return {boolean}
* @private
*/
isMiscellaneous_(b1, b2) {
// For Miscellaneous Control Codes, the bytes take the following form:
// b1 -> |0|0|0|1|C|1|0|F|
// b2 -> |0|0|1|0|X|X|X|X|
return ((b1 & 0xf6) === 0x14) && ((b2 & 0xf0) === 0x20);
}
/**
* Checks if this is a offset control code.
* @param {number} b1 Byte 1.
* @param {number} b2 Byte 2.
* @return {boolean}
* @private
*/
isOffset_(b1, b2) {
return (b1 == 0x17 || b1 == 0x1f) && b2 >= 0x21 && b2 <= 0x23;
}
/**
* Checks if this is a PAC control code.
* @param {number} b1 Byte 1.
* @param {number} b2 Byte 2.
* @return {boolean}
* @private
*/
isPAC_(b1, b2) {
// For Preamble Address Codes, the bytes take the following form:
// b1 -> |0|0|0|1|X|X|X|X|
// b2 -> |0|1|X|X|X|X|X|X|
return ((b1 & 0xf0) === 0x10) && ((b2 & 0xc0) === 0x40);
}
/**
* Checks if this is a Midrow style change control code.
* @param {number} b1 Byte 1.
* @param {number} b2 Byte 2.
* @return {boolean}
* @private
*/
isMidrowStyleChange_(b1, b2) {
// For Midrow Control Codes, the bytes take the following form:
// b1 -> |0|0|0|1|C|0|0|1|
// b2 -> |0|0|1|0|X|X|X|X|
return ((b1 & 0xf7) === 0x11) && ((b2 & 0xf0) === 0x20);
}
/**
* Checks if this is a background attribute control code.
* @param {number} b1 Byte 1.
* @param {number} b2 Byte 2.
* @return {boolean}
* @private
*/
isBackgroundAttribute_(b1, b2) {
// For Background Attribute Codes, the bytes take the following form:
// Bg provided: b1 -> |0|0|0|1|C|0|0|0| b2 -> |0|0|1|0|COLOR|T|
// No Bg: b1 -> |0|0|0|1|C|1|1|1| b2 -> |0|0|1|0|1|1|0|1|
return (((b1 & 0xf7) === 0x10) && ((b2 & 0xf0) === 0x20)) ||
(((b1 & 0xf7) === 0x17) && ((b2 & 0xff) === 0x2D));
}
/**
* Checks if the character is in the Special North American char. set.
* @param {number} b1 Byte 1.
* @param {number} b2 Byte 2.
* @return {boolean}
* @private
*/
isSpecialNorthAmericanChar_(b1, b2) {
// The bytes take the following form:
// b1 -> |0|0|0|1|C|0|0|1|
// b2 -> |0|0|1|1| CHAR |
return ((b1 & 0xf7) === 0x11) && ((b2 & 0xf0) === 0x30);
}
/**
* Checks if the character is in the Extended Western European char. set.
* @param {number} b1 Byte 1.
* @param {number} b2 Byte 2.
* @return {boolean}
* @private
*/
isExtendedWesternEuropeanChar_(b1, b2) {
// The bytes take the following form:
// b1 -> |0|0|0|1|C|0|1|S|
// b2 -> |0|0|1|CHARACTER|
return ((b1 & 0xf6) === 0x12) && ((b2 & 0xe0) === 0x20);
}
/**
* Checks if the data contains a control code.
* @param {number} b1 Byte 1.
* @return {boolean}
*/
static isControlCode(b1) {
// For control codes, the first byte takes the following form:
// b1 -> |P|0|0|1|X|X|X|X|
return (b1 & 0x70) === 0x10;
}
/**
* Checks if the data contains a XDS control code.
* @param {number} b1 Byte 1.
* @return {boolean}
*/
static isXdsControlCode(b1) {
return b1 >= 0x01 && b1 <= 0x0F;
}
};
/**
* Command codes.
* @enum {number}
* @private
*/
shaka.cea.Cea608DataChannel.MiscCmd_ = {
// "RCL - Resume Caption Loading"
RCL: 0x20,
// "BS - BackSpace"
BS: 0x21,
// "AOD - Unused (alarm off)"
AOD: 0x22,
// "AON - Unused (alarm on)"
AON: 0x23,
// "DER - Delete to End of Row"
DER: 0x24,
// "RU2 - Roll-Up, 2 rows"
RU2: 0x25,
// "RU3 - Roll-Up, 3 rows"
RU3: 0x26,
// "RU4 - Roll-Up, 4 rows"
RU4: 0x27,
// "FON - Flash On"
FON: 0x28,
// "RDC - Resume Direct Captions"
RDC: 0x29,
// "TR - Text Restart"
TR: 0x2a,
// "RTD - Resume Text Display"
RTD: 0x2b,
// "EDM - Erase Displayed Mem"
EDM: 0x2c,
// "CR - Carriage return"
CR: 0x2d,
// "ENM - Erase Nondisplayed Mem"
ENM: 0x2e,
// "EOC - End Of Caption (flip mem)"
EOC: 0x2f,
};
/**
* Caption type.
* @private @const @enum {number}
*/
shaka.cea.Cea608DataChannel.CaptionType = {
NONE: 0,
POPON: 1,
PAINTON: 2,
ROLLUP: 3,
TEXT: 4,
};
/**
* @const {!Array<string>}
*/
shaka.cea.Cea608DataChannel.BG_COLORS = [
'black',
'green',
'blue',
'cyan',
'red',
'yellow',
'magenta',
'black',
];
/**
* @const {!Array<string>}
*/
shaka.cea.Cea608DataChannel.TEXT_COLORS = [
'white',
'green',
'blue',
'cyan',
'red',
'yellow',
'magenta',
'white_italics',
];
/**
* Style associated with a cue.
* @typedef {{
* textColor: ?string,
* backgroundColor: ?string,
* italics: ?boolean,
* underline: ?boolean
* }}
*/
shaka.cea.Cea608DataChannel.Style;
/**
* CEA closed captions packet.
* @typedef {{
* pts: number,
* type: number,
* ccData1: number,
* ccData2: number,
* order: number
* }}
*
* @property {number} pts
* Presentation timestamp (in second) at which this packet was received.
* @property {number} type
* Type of the packet. Either 0 or 1, representing the CEA-608 field.
* @property {number} ccData1 CEA-608 byte 1.
* @property {number} ccData2 CEA-608 byte 2.
* @property {number} order
* A number indicating the order this packet was received in a sequence
* of packets. Used to break ties in a stable sorting algorithm
*/
shaka.cea.Cea608DataChannel.Cea608Packet;