UNPKG

shaka-player

Version:
308 lines (263 loc) 8.21 kB
/*! @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.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.extern.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'; /** * NALU type for Supplemental Enhancement Information (SEI) for H.264. * @const {number} */ shaka.cea.CeaUtils.H264_NALU_TYPE_SEI = 0x06; /** * NALU type for Supplemental Enhancement Information (SEI) for H.265. * @const {number} */ shaka.cea.CeaUtils.H265_PREFIX_NALU_TYPE_SEI = 0x27; /** * NALU type for Supplemental Enhancement Information (SEI) for H.265. * @const {number} */ shaka.cea.CeaUtils.H265_SUFFIX_NALU_TYPE_SEI = 0x28; /** * NALU type for Supplemental Enhancement Information (SEI) for H.266. * @const {number} */ shaka.cea.CeaUtils.H266_PREFIX_NALU_TYPE_SEI = 0x17; /** * NALU type for Supplemental Enhancement Information (SEI) for H.266. * @const {number} */ shaka.cea.CeaUtils.H266_SUFFIX_NALU_TYPE_SEI = 0x18; /** * Default timescale value for a track. * @const {number} */ shaka.cea.CeaUtils.DEFAULT_TIMESCALE_VALUE = 90000;