Source: lib/cea/cea708_window.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.cea.Cea708Window');
  7. goog.require('shaka.cea.CeaUtils');
  8. goog.require('shaka.cea.CeaUtils.StyledChar');
  9. goog.require('shaka.text.Cue');
  10. goog.require('shaka.text.CueRegion');
  11. /**
  12. * CEA-708 Window. Each CEA-708 service owns 8 of these.
  13. */
  14. shaka.cea.Cea708Window = class {
  15. /**
  16. * @param {number} windowNum
  17. * @param {number} parentService
  18. */
  19. constructor(windowNum, parentService) {
  20. /**
  21. * Number for the parent service (1 - 63).
  22. * @private {number}
  23. */
  24. this.parentService_ = parentService;
  25. /**
  26. * A number from 0 - 7 indicating the window number in the
  27. * service that owns this window.
  28. * @private {number}
  29. */
  30. this.windowNum_ = windowNum;
  31. /**
  32. * Indicates whether this window is visible.
  33. * @private {boolean}
  34. */
  35. this.visible_ = false;
  36. /**
  37. * Indicates whether the horizontal and vertical anchors coordinates specify
  38. * a percentage of the screen, or physical coordinates on the screen.
  39. * @private {boolean}
  40. */
  41. this.relativeToggle_ = false;
  42. /**
  43. * Horizontal anchor. Loosely corresponds to a WebVTT viewport X anchor.
  44. * @private {number}
  45. */
  46. this.horizontalAnchor_ = 0;
  47. /**
  48. * Vertical anchor. Loosely corresponds to a WebVTT viewport Y anchor.
  49. * @private {number}
  50. */
  51. this.verticalAnchor_ = 0;
  52. /**
  53. * If valid, ranges from 0 to 8, specifying one of 9 locations on window:
  54. * 0________1________2
  55. * | | |
  56. * 3________4________5
  57. * | | |
  58. * 6________7________8
  59. * Diagram is valid as per CEA-708-E section 8.4.4.
  60. * Each of these locations corresponds to a WebVTT region's "region anchor".
  61. * @private {number}
  62. */
  63. this.anchorId_ = 0;
  64. /**
  65. * Indicates the number of rows in this window's buffer/memory.
  66. * @private {number}
  67. */
  68. this.rowCount_ = 0;
  69. /**
  70. * Indicates the number of columns in this window's buffer/memory.
  71. * @private {number}
  72. */
  73. this.colCount_ = 0;
  74. /**
  75. * Center by default.
  76. * @private {!shaka.cea.Cea708Window.TextJustification}
  77. */
  78. this.justification_ = shaka.cea.Cea708Window.TextJustification.CENTER;
  79. /**
  80. * An array of rows of styled characters, representing the
  81. * current text and styling of text in this window.
  82. * @private {!Array<!Array<?shaka.cea.CeaUtils.StyledChar>>}
  83. */
  84. this.memory_ = [];
  85. /**
  86. * @private {number}
  87. */
  88. this.startTime_ = 0;
  89. /**
  90. * Row that the current pen is pointing at.
  91. * @private {number}
  92. */
  93. this.row_ = 0;
  94. /**
  95. * Column that the current pen is pointing at.
  96. * @private {number}
  97. */
  98. this.col_ = 0;
  99. /**
  100. * Indicates whether the current pen position is italicized.
  101. * @private {boolean}
  102. */
  103. this.italics_ = false;
  104. /**
  105. * Indicates whether the current pen position is underlined.
  106. * @private {boolean}
  107. */
  108. this.underline_ = false;
  109. /**
  110. * Indicates the text color at the current pen position.
  111. * @private {string}
  112. */
  113. this.textColor_ = shaka.cea.CeaUtils.DEFAULT_TXT_COLOR;
  114. /**
  115. * Indicates the background color at the current pen position.
  116. * @private {string}
  117. */
  118. this.backgroundColor_ = shaka.cea.CeaUtils.DEFAULT_BG_COLOR;
  119. this.resetMemory();
  120. }
  121. /**
  122. * @param {boolean} visible
  123. * @param {number} verticalAnchor
  124. * @param {number} horizontalAnchor
  125. * @param {number} anchorId
  126. * @param {boolean} relativeToggle
  127. * @param {number} rowCount
  128. * @param {number} colCount
  129. */
  130. defineWindow(visible, verticalAnchor, horizontalAnchor, anchorId,
  131. relativeToggle, rowCount, colCount) {
  132. this.visible_ = visible;
  133. this.verticalAnchor_ = verticalAnchor;
  134. this.horizontalAnchor_ = horizontalAnchor;
  135. this.anchorId_ = anchorId;
  136. this.relativeToggle_ = relativeToggle;
  137. this.rowCount_ = rowCount;
  138. this.colCount_ = colCount;
  139. }
  140. /**
  141. * Resets the memory buffer.
  142. */
  143. resetMemory() {
  144. this.memory_ = [];
  145. for (let i = 0; i < shaka.cea.Cea708Window.MAX_ROWS; i++) {
  146. this.memory_.push(this.createNewRow_());
  147. }
  148. }
  149. /**
  150. * Allocates and returns a new row.
  151. * @return {!Array<?shaka.cea.CeaUtils.StyledChar>}
  152. * @private
  153. */
  154. createNewRow_() {
  155. const row = [];
  156. for (let j = 0; j < shaka.cea.Cea708Window.MAX_COLS; j++) {
  157. row.push(null);
  158. }
  159. return row;
  160. }
  161. /**
  162. * Sets the unicode value for a char at the current pen location.
  163. * @param {string} char
  164. */
  165. setCharacter(char) {
  166. // Check if the pen is out of bounds.
  167. if (!this.isPenInBounds_()) {
  168. return;
  169. }
  170. const cea708Char = new shaka.cea.CeaUtils.StyledChar(
  171. char, this.underline_, this.italics_,
  172. this.backgroundColor_, this.textColor_);
  173. this.memory_[this.row_][this.col_] = cea708Char;
  174. // Increment column
  175. this.col_ ++;
  176. }
  177. /**
  178. * Erases a character from the buffer and moves the pen back.
  179. */
  180. backspace() {
  181. if (!this.isPenInBounds_()) {
  182. return;
  183. }
  184. // Check if a backspace can be done.
  185. if (this.col_ <= 0 && this.row_ <= 0) {
  186. return;
  187. }
  188. if (this.col_ <= 0) {
  189. // Move pen back a row.
  190. this.col_ = this.colCount_ - 1;
  191. this.row_--;
  192. } else {
  193. // Move pen back a column.
  194. this.col_--;
  195. }
  196. // Erase the character occupied at that position.
  197. this.memory_[this.row_][this.col_] = null;
  198. }
  199. /**
  200. * @return {boolean}
  201. * @private
  202. */
  203. isPenInBounds_() {
  204. const inRowBounds = this.row_ < this.rowCount_ && this.row_ >= 0;
  205. const inColBounds = this.col_ < this.colCount_ && this.col_ >= 0;
  206. return inRowBounds && inColBounds;
  207. }
  208. /**
  209. * @return {boolean}
  210. */
  211. isVisible() {
  212. return this.visible_;
  213. }
  214. /**
  215. * Moves up <count> rows in the buffer.
  216. * @param {number} count
  217. * @private
  218. */
  219. moveUpRows_(count) {
  220. let dst = 0; // Row each row should be moved to.
  221. // Move existing rows up by <count>.
  222. for (let i = count; i < shaka.cea.Cea708Window.MAX_ROWS; i++, dst++) {
  223. this.memory_[dst] = this.memory_[i];
  224. }
  225. // Create <count> new rows at the bottom.
  226. for (let i = 0; i < count; i++, dst++) {
  227. this.memory_[dst] = this.createNewRow_();
  228. }
  229. }
  230. /**
  231. * Handles CR. Increments row - if last row, "roll up" all rows by one.
  232. */
  233. carriageReturn() {
  234. if (this.row_ + 1 >= this.rowCount_) {
  235. this.moveUpRows_(1);
  236. this.col_ = 0;
  237. return;
  238. }
  239. this.row_++;
  240. this.col_ = 0;
  241. }
  242. /**
  243. * Handles HCR. Moves the pen to the beginning of the cur. row and clears it.
  244. */
  245. horizontalCarriageReturn() {
  246. this.memory_[this.row_] = this.createNewRow_();
  247. this.col_ = 0;
  248. }
  249. /**
  250. * @param {number} endTime
  251. * @param {number} serviceNumber Number of the service emitting this caption.
  252. * @return {?shaka.extern.ICaptionDecoder.ClosedCaption}
  253. */
  254. forceEmit(endTime, serviceNumber) {
  255. const stream = `svc${serviceNumber}`;
  256. const TextJustification = shaka.cea.Cea708Window.TextJustification;
  257. const topLevelCue = new shaka.text.Cue(
  258. this.startTime_, endTime, /* payload= */ '');
  259. if (this.justification_ === TextJustification.LEFT) {
  260. // LEFT justified.
  261. topLevelCue.textAlign = shaka.text.Cue.textAlign.LEFT;
  262. } else if (this.justification_ === TextJustification.RIGHT) {
  263. // RIGHT justified.
  264. topLevelCue.textAlign = shaka.text.Cue.textAlign.RIGHT;
  265. } else {
  266. // CENTER justified. Both FULL and CENTER are handled as CENTER justified.
  267. topLevelCue.textAlign = shaka.text.Cue.textAlign.CENTER;
  268. }
  269. this.adjustRegion_(topLevelCue.region);
  270. const caption = shaka.cea.CeaUtils.getParsedCaption(
  271. topLevelCue, stream, this.memory_, this.startTime_, endTime);
  272. if (caption) {
  273. // If a caption is being emitted, then the next caption's start time
  274. // should be no less than this caption's end time.
  275. this.setStartTime(endTime);
  276. }
  277. return caption;
  278. }
  279. /**
  280. * @param {number} row
  281. * @param {number} col
  282. */
  283. setPenLocation(row, col) {
  284. this.row_ = row;
  285. this.col_ = col;
  286. }
  287. /**
  288. * @param {string} backgroundColor
  289. */
  290. setPenBackgroundColor(backgroundColor) {
  291. this.backgroundColor_ = backgroundColor;
  292. }
  293. /**
  294. * @param {string} textColor
  295. */
  296. setPenTextColor(textColor) {
  297. this.textColor_ = textColor;
  298. }
  299. /**
  300. * @param {boolean} underline
  301. */
  302. setPenUnderline(underline) {
  303. this.underline_ = underline;
  304. }
  305. /**
  306. * @param {boolean} italics
  307. */
  308. setPenItalics(italics) {
  309. this.italics_ = italics;
  310. }
  311. /** Reset the pen to 0,0 with default styling. */
  312. resetPen() {
  313. this.row_ = 0;
  314. this.col_ = 0;
  315. this.underline_ = false;
  316. this.italics_ = false;
  317. this.textColor_ = shaka.cea.CeaUtils.DEFAULT_TXT_COLOR;
  318. this.backgroundColor_ = shaka.cea.CeaUtils.DEFAULT_BG_COLOR;
  319. }
  320. /**
  321. * @param {!shaka.cea.Cea708Window.TextJustification} justification
  322. */
  323. setJustification(justification) {
  324. this.justification_ = justification;
  325. }
  326. /**
  327. * Sets the window to visible.
  328. */
  329. display() {
  330. this.visible_ = true;
  331. }
  332. /**
  333. * Sets the window to invisible.
  334. */
  335. hide() {
  336. this.visible_ = false;
  337. }
  338. /**
  339. * Toggles the visibility of the window.
  340. */
  341. toggle() {
  342. this.visible_ = !this.visible_;
  343. }
  344. /**
  345. * Sets the start time for the cue to be emitted.
  346. * @param {number} pts
  347. */
  348. setStartTime(pts) {
  349. this.startTime_ = pts;
  350. }
  351. /**
  352. * Support window positioning by mapping anchor related values to CueRegion.
  353. * https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/608toVTT.html#positioning-in-cea-708
  354. * @param {shaka.text.CueRegion} region
  355. * @private
  356. */
  357. adjustRegion_(region) {
  358. if (this.parentService_) {
  359. region.id += 'svc' + this.parentService_;
  360. }
  361. region.id += 'win' + this.windowNum_;
  362. region.height = this.rowCount_;
  363. region.width = this.colCount_;
  364. region.heightUnits = shaka.text.CueRegion.units.LINES;
  365. region.widthUnits = shaka.text.CueRegion.units.LINES;
  366. region.viewportAnchorX = this.horizontalAnchor_;
  367. region.viewportAnchorY = this.verticalAnchor_;
  368. // WebVTT's region viewport anchors are technically always in percentages.
  369. // However, we don't know the aspect ratio of the video at this point,
  370. // which determines how we interpret the horizontal anchor.
  371. // So, we expose the additional flag to reflect whether these viewport
  372. // anchor values can be used as is or should be converted
  373. // to percentages.
  374. region.viewportAnchorUnits = this.relativeToggle_ ?
  375. shaka.text.CueRegion.units.PERCENTAGE : shaka.text.CueRegion.units.LINES;
  376. const AnchorId = shaka.cea.Cea708Window.AnchorId;
  377. switch (this.anchorId_) {
  378. case AnchorId.UPPER_LEFT:
  379. region.regionAnchorX = 0;
  380. region.regionAnchorY = 0;
  381. break;
  382. case AnchorId.UPPER_CENTER:
  383. region.regionAnchorX = 50;
  384. region.regionAnchorY = 0;
  385. break;
  386. case AnchorId.UPPER_RIGHT:
  387. region.regionAnchorX = 100;
  388. region.regionAnchorY = 0;
  389. break;
  390. case AnchorId.MIDDLE_LEFT:
  391. region.regionAnchorX = 0;
  392. region.regionAnchorY = 50;
  393. break;
  394. case AnchorId.MIDDLE_CENTER:
  395. region.regionAnchorX = 50;
  396. region.regionAnchorY = 50;
  397. break;
  398. case AnchorId.MIDDLE_RIGHT:
  399. region.regionAnchorX = 100;
  400. region.regionAnchorY = 50;
  401. break;
  402. case AnchorId.LOWER_LEFT:
  403. region.regionAnchorX = 0;
  404. region.regionAnchorY = 100;
  405. break;
  406. case AnchorId.LOWER_CENTER:
  407. region.regionAnchorX = 50;
  408. region.regionAnchorY = 100;
  409. break;
  410. case AnchorId.LOWER_RIGHT:
  411. region.regionAnchorX = 100;
  412. region.regionAnchorY = 100;
  413. break;
  414. }
  415. }
  416. };
  417. /**
  418. * Caption type.
  419. * @const @enum {number}
  420. */
  421. shaka.cea.Cea708Window.TextJustification = {
  422. LEFT: 0,
  423. RIGHT: 1,
  424. CENTER: 2,
  425. FULL: 3,
  426. };
  427. /**
  428. * Possible AnchorId values.
  429. * @const @enum {number}
  430. */
  431. shaka.cea.Cea708Window.AnchorId = {
  432. UPPER_LEFT: 0,
  433. UPPER_CENTER: 1,
  434. UPPER_RIGHT: 2,
  435. MIDDLE_LEFT: 3,
  436. MIDDLE_CENTER: 4,
  437. MIDDLE_RIGHT: 5,
  438. LOWER_LEFT: 6,
  439. LOWER_CENTER: 7,
  440. LOWER_RIGHT: 8,
  441. };
  442. /**
  443. * Can be indexed 0-31 for 4:3 format, and 0-41 for 16:9 formats.
  444. * Thus the absolute maximum is 42 columns for the 16:9 format.
  445. * @private @const {number}
  446. */
  447. shaka.cea.Cea708Window.MAX_COLS = 42;
  448. /**
  449. * Maximum of 16 rows that can be indexed from 0 to 15.
  450. * @private @const {number}
  451. */
  452. shaka.cea.Cea708Window.MAX_ROWS = 16;