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