Source: lib/cea/cea_utils.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.cea.CeaUtils');
  7. goog.provide('shaka.cea.CeaUtils.StyledChar');
  8. goog.require('shaka.cea.ICaptionDecoder');
  9. goog.require('shaka.text.Cue');
  10. shaka.cea.CeaUtils = class {
  11. /**
  12. * Emits a closed caption based on the state of the buffer.
  13. * @param {!shaka.text.Cue} topLevelCue
  14. * @param {!string} stream
  15. * @param {!Array<!Array<?shaka.cea.CeaUtils.StyledChar>>} memory
  16. * @param {!number} startTime Start time of the cue.
  17. * @param {!number} endTime End time of the cue.
  18. * @return {?shaka.cea.ICaptionDecoder.ClosedCaption}
  19. */
  20. static getParsedCaption(topLevelCue, stream, memory, startTime, endTime) {
  21. if (startTime >= endTime) {
  22. return null;
  23. }
  24. // Find the first and last row that contains characters.
  25. let firstNonEmptyRow = -1;
  26. let lastNonEmptyRow = -1;
  27. for (let i = 0; i < memory.length; i++) {
  28. if (memory[i].some((e) => e != null && e.getChar().trim() != '')) {
  29. firstNonEmptyRow = i;
  30. break;
  31. }
  32. }
  33. for (let i = memory.length - 1; i >= 0; i--) {
  34. if (memory[i].some((e) => e != null && e.getChar().trim() != '')) {
  35. lastNonEmptyRow = i;
  36. break;
  37. }
  38. }
  39. // Exit early if no non-empty row was found.
  40. if (firstNonEmptyRow === -1 || lastNonEmptyRow === -1) {
  41. return null;
  42. }
  43. // Keeps track of the current styles for a cue being emitted.
  44. let currentUnderline = false;
  45. let currentItalics = false;
  46. let currentTextColor = shaka.cea.CeaUtils.DEFAULT_TXT_COLOR;
  47. let currentBackgroundColor = shaka.cea.CeaUtils.DEFAULT_BG_COLOR;
  48. // Create first cue that will be nested in top level cue. Default styles.
  49. let currentCue = shaka.cea.CeaUtils.createStyledCue(
  50. startTime, endTime, currentUnderline, currentItalics,
  51. currentTextColor, currentBackgroundColor);
  52. // Logic: Reduce rows into a single top level cue containing nested cues.
  53. // Each nested cue corresponds either a style change or a line break.
  54. for (let i = firstNonEmptyRow; i <= lastNonEmptyRow; i++) {
  55. // Find the first and last non-empty characters in this row. We do this so
  56. // no styles creep in before/after the first and last non-empty chars.
  57. const row = memory[i];
  58. let firstNonEmptyCol = -1;
  59. let lastNonEmptyCol = -1;
  60. for (let j = 0; j < row.length; j++) {
  61. if (row[j] != null && row[j].getChar().trim() !== '') {
  62. firstNonEmptyCol = j;
  63. break;
  64. }
  65. }
  66. for (let j = row.length - 1; j >= 0; j--) {
  67. if (row[j] != null && row[j].getChar().trim() !== '') {
  68. lastNonEmptyCol = j;
  69. break;
  70. }
  71. }
  72. // If no non-empty char. was found in this row, it must be a linebreak.
  73. if (firstNonEmptyCol === -1 || lastNonEmptyCol === -1) {
  74. const linebreakCue = shaka.cea.CeaUtils
  75. .createLineBreakCue(startTime, endTime);
  76. topLevelCue.nestedCues.push(linebreakCue);
  77. continue;
  78. }
  79. for (let j = firstNonEmptyCol; j <= lastNonEmptyCol; j++) {
  80. const styledChar = row[j];
  81. // A null between non-empty cells in a row is handled as a space.
  82. if (!styledChar) {
  83. currentCue.payload += ' ';
  84. continue;
  85. }
  86. const underline = styledChar.isUnderlined();
  87. const italics = styledChar.isItalicized();
  88. const textColor = styledChar.getTextColor();
  89. const backgroundColor = styledChar.getBackgroundColor();
  90. // If any style properties have changed, we need to open a new cue.
  91. if (underline != currentUnderline || italics != currentItalics ||
  92. textColor != currentTextColor ||
  93. backgroundColor != currentBackgroundColor) {
  94. // Push the currently built cue and start a new cue, with new styles.
  95. if (currentCue.payload) {
  96. topLevelCue.nestedCues.push(currentCue);
  97. }
  98. currentCue = shaka.cea.CeaUtils.createStyledCue(
  99. startTime, endTime, underline,
  100. italics, textColor, backgroundColor);
  101. currentUnderline = underline;
  102. currentItalics = italics;
  103. currentTextColor = textColor;
  104. currentBackgroundColor = backgroundColor;
  105. }
  106. currentCue.payload += styledChar.getChar();
  107. }
  108. if (currentCue.payload) {
  109. topLevelCue.nestedCues.push(currentCue);
  110. }
  111. // Add a linebreak since the row just ended.
  112. if (i !== lastNonEmptyRow) {
  113. const linebreakCue = shaka.cea.CeaUtils
  114. .createLineBreakCue(startTime, endTime);
  115. topLevelCue.nestedCues.push(linebreakCue);
  116. }
  117. // Create a new cue.
  118. currentCue = shaka.cea.CeaUtils.createStyledCue(
  119. startTime, endTime, currentUnderline, currentItalics,
  120. currentTextColor, currentBackgroundColor);
  121. }
  122. if (topLevelCue.nestedCues.length) {
  123. return {
  124. cue: topLevelCue,
  125. stream,
  126. };
  127. }
  128. return null;
  129. }
  130. /**
  131. * @param {!number} startTime
  132. * @param {!number} endTime
  133. * @param {!boolean} underline
  134. * @param {!boolean} italics
  135. * @param {!string} txtColor
  136. * @param {!string} bgColor
  137. * @return {!shaka.text.Cue}
  138. */
  139. static createStyledCue(startTime, endTime, underline,
  140. italics, txtColor, bgColor) {
  141. const cue = new shaka.text.Cue(startTime, endTime, /* payload= */ '');
  142. if (underline) {
  143. cue.textDecoration.push(shaka.text.Cue.textDecoration.UNDERLINE);
  144. }
  145. if (italics) {
  146. cue.fontStyle = shaka.text.Cue.fontStyle.ITALIC;
  147. }
  148. cue.color = txtColor;
  149. cue.backgroundColor = bgColor;
  150. return cue;
  151. }
  152. /**
  153. * @param {!number} startTime
  154. * @param {!number} endTime
  155. * @return {!shaka.text.Cue}
  156. */
  157. static createLineBreakCue(startTime, endTime) {
  158. const linebreakCue = new shaka.text.Cue(
  159. startTime, endTime, /* payload= */ '');
  160. linebreakCue.lineBreak = true;
  161. return linebreakCue;
  162. }
  163. };
  164. shaka.cea.CeaUtils.StyledChar = class {
  165. /**
  166. * @param {string} character
  167. * @param {boolean} underline
  168. * @param {boolean} italics
  169. * @param {string} backgroundColor
  170. * @param {string} textColor
  171. */
  172. constructor(character, underline, italics, backgroundColor, textColor) {
  173. /**
  174. * @private {!string}
  175. */
  176. this.character_ = character;
  177. /**
  178. * @private {!boolean}
  179. */
  180. this.underline_ = underline;
  181. /**
  182. * @private {!boolean}
  183. */
  184. this.italics_ = italics;
  185. /**
  186. * @private {!string}
  187. */
  188. this.backgroundColor_ = backgroundColor;
  189. /**
  190. * @private {!string}
  191. */
  192. this.textColor_ = textColor;
  193. }
  194. /**
  195. * @return {!string}
  196. */
  197. getChar() {
  198. return this.character_;
  199. }
  200. /**
  201. * @return {!boolean}
  202. */
  203. isUnderlined() {
  204. return this.underline_;
  205. }
  206. /**
  207. * @return {!boolean}
  208. */
  209. isItalicized() {
  210. return this.italics_;
  211. }
  212. /**
  213. * @return {!string}
  214. */
  215. getBackgroundColor() {
  216. return this.backgroundColor_;
  217. }
  218. /**
  219. * @return {!string}
  220. */
  221. getTextColor() {
  222. return this.textColor_;
  223. }
  224. };
  225. /**
  226. * Default background color for text.
  227. * @const {!string}
  228. */
  229. shaka.cea.CeaUtils.DEFAULT_BG_COLOR = 'black';
  230. /**
  231. * Default text color.
  232. * @const {!string}
  233. */
  234. shaka.cea.CeaUtils.DEFAULT_TXT_COLOR = 'white';