Files
shaka-player/lib/cea/cea_utils.js
T
Joey Parrish 562a2d567b chore: Strictly require jsdoc
This enables the eslint rule requiring jsdocs on all class
declarations, function declarations, and methods.

Unfortunately, there are two problems with this:

1. We don't use class _declarations_, we use class _expressions_,
which are not covered by this rule.  So it does not enforce jsdoc at
the class level.
2. We tend to document a class at the class-level, rather than at the
constructor.  But a constructor counts as a method for eslint, so it
requires docs on the constructor.  There is no way to configure it to
make an exception for trivial constructors.

So for all trivial (no-argument) constructors, we add empty jsdocs:
  /** */
  constructor() {

This was quicker and easier than setting up some alternative plugin in
eslint to make an exception for us.

The good news is that this rule caught several undocumented parameters
and places where the jsdoc comment was malformed.  So fixing those
also improves the compiler's ability to enforce types.

Change-Id: Icbc46ed690c94e53d354648a883119524f8fca45
2021-01-09 02:00:31 +00:00

273 lines
7.2 KiB
JavaScript

/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.cea.CeaUtils');
goog.provide('shaka.cea.CeaUtils.StyledChar');
goog.require('shaka.cea.ICaptionDecoder');
goog.require('shaka.text.Cue');
shaka.cea.CeaUtils = class {
/**
* Emits a closed caption based on the state of the buffer.
* @param {!shaka.text.Cue} topLevelCue
* @param {!string} stream
* @param {!Array<!Array<?shaka.cea.CeaUtils.StyledChar>>} memory
* @param {!number} startTime Start time of the cue.
* @param {!number} endTime End time of the cue.
* @return {?shaka.cea.ICaptionDecoder.ClosedCaption}
*/
static getParsedCaption(topLevelCue, stream, memory, startTime, endTime) {
if (startTime >= endTime) {
return null;
}
// Find the first and last row that contains characters.
let firstNonEmptyRow = -1;
let lastNonEmptyRow = -1;
for (let i = 0; i < memory.length; i++) {
if (memory[i].some((e) => e != null && e.getChar().trim() != '')) {
firstNonEmptyRow = i;
break;
}
}
for (let i = memory.length - 1; i >= 0; i--) {
if (memory[i].some((e) => e != null && e.getChar().trim() != '')) {
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.CeaUtils.DEFAULT_TXT_COLOR;
let currentBackgroundColor = shaka.cea.CeaUtils.DEFAULT_BG_COLOR;
// Create first cue that will be nested in top level cue. Default styles.
let currentCue = shaka.cea.CeaUtils.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.
for (let i = firstNonEmptyRow; i <= lastNonEmptyRow; i++) {
// Find the first and last non-empty characters in this row. We do this so
// no styles creep in before/after the first and last non-empty chars.
const row = memory[i];
let firstNonEmptyCol = -1;
let lastNonEmptyCol = -1;
for (let j = 0; j < row.length; j++) {
if (row[j] != null && row[j].getChar().trim() !== '') {
firstNonEmptyCol = j;
break;
}
}
for (let j = row.length - 1; j >= 0; j--) {
if (row[j] != null && row[j].getChar().trim() !== '') {
lastNonEmptyCol = j;
break;
}
}
// If no non-empty char. was found in this row, it must be a linebreak.
if (firstNonEmptyCol === -1 || lastNonEmptyCol === -1) {
const linebreakCue = shaka.cea.CeaUtils
.createLineBreakCue(startTime, endTime);
topLevelCue.nestedCues.push(linebreakCue);
continue;
}
for (let j = firstNonEmptyCol; j <= lastNonEmptyCol; j++) {
const styledChar = row[j];
// A null between non-empty cells in a row is handled as a space.
if (!styledChar) {
currentCue.payload += ' ';
continue;
}
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 = shaka.cea.CeaUtils.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);
}
// Add a linebreak since the row just ended.
if (i !== lastNonEmptyRow) {
const linebreakCue = shaka.cea.CeaUtils
.createLineBreakCue(startTime, endTime);
topLevelCue.nestedCues.push(linebreakCue);
}
// Create a new cue.
currentCue = shaka.cea.CeaUtils.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}
*/
static 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;
}
/**
* @param {!number} startTime
* @param {!number} endTime
* @return {!shaka.text.Cue}
*/
static createLineBreakCue(startTime, endTime) {
const linebreakCue = new shaka.text.Cue(
startTime, endTime, /* payload= */ '');
linebreakCue.lineBreak = true;
return linebreakCue;
}
};
shaka.cea.CeaUtils.StyledChar = class {
/**
* @param {string} character
* @param {boolean} underline
* @param {boolean} italics
* @param {string} backgroundColor
* @param {string} textColor
*/
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_;
}
};
/**
* Default background color for text.
* @const {!string}
*/
shaka.cea.CeaUtils.DEFAULT_BG_COLOR = 'black';
/**
* Default text color.
* @const {!string}
*/
shaka.cea.CeaUtils.DEFAULT_TXT_COLOR = 'white';