Source: lib/cea/cea_decoder.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.cea.CeaDecoder');
  7. goog.require('shaka.cea.Cea608DataChannel');
  8. goog.require('shaka.cea.Cea708Service');
  9. goog.require('shaka.cea.DtvccPacketBuilder');
  10. goog.require('shaka.cea.ICaptionDecoder');
  11. goog.require('shaka.log');
  12. goog.require('shaka.util.DataViewReader');
  13. goog.require('shaka.util.Error');
  14. goog.requireType('shaka.cea.DtvccPacket');
  15. /**
  16. * CEA-X08 captions decoder. Currently only CEA-608 supported.
  17. * @implements {shaka.cea.ICaptionDecoder}
  18. */
  19. shaka.cea.CeaDecoder = class {
  20. /** */
  21. constructor() {
  22. /**
  23. * An array of CEA-608 closed caption data extracted for decoding.
  24. * @private {!Array<!shaka.cea.Cea608DataChannel.Cea608Packet>}
  25. */
  26. this.cea608DataArray_ = [];
  27. /**
  28. * An array of CEA-708 closed caption data extracted for decoding.
  29. * @private {!Array<!shaka.cea.Cea708Service.Cea708Byte>}
  30. */
  31. this.cea708DataArray_ = [];
  32. /**
  33. * A DTVCC Packet builder for CEA-708 data.
  34. * @private {!shaka.cea.DtvccPacketBuilder}
  35. */
  36. this.dtvccPacketBuilder_ = new shaka.cea.DtvccPacketBuilder();
  37. /**
  38. * Number of consecutive bad frames decoded on CEA-608.
  39. * @private {number}
  40. */
  41. this.badFrames_ = 0;
  42. /**
  43. * A map containing the stream for each mode.
  44. * @private {!Map<string, !shaka.cea.Cea608DataChannel>}
  45. */
  46. this.cea608ModeToStream_ = new Map([
  47. ['CC1', new shaka.cea.Cea608DataChannel(0, 0)], // F1 + C1 -> CC1
  48. ['CC2', new shaka.cea.Cea608DataChannel(0, 1)], // F1 + C2 -> CC2
  49. ['CC3', new shaka.cea.Cea608DataChannel(1, 0)], // F2 + C1 -> CC3
  50. ['CC4', new shaka.cea.Cea608DataChannel(1, 1)], // F2 + C2 -> CC4
  51. ]);
  52. /**
  53. * The current channel that is active on CEA-608 field 1.
  54. * @private {number}
  55. */
  56. this.currentField1Channel_ = 0;
  57. /**
  58. * The current channel that is active on CEA-608 field 2.
  59. * @private {number}
  60. */
  61. this.currentField2Channel_ = 0;
  62. /**
  63. * Map of service number to CEA-708 services, initially empty. Since there
  64. * can be up to 63 services, they are created dynamically only when needed.
  65. * @private {!Map<number, shaka.cea.Cea708Service>}
  66. */
  67. this.serviceNumberToService_ = new Map();
  68. this.reset();
  69. }
  70. /**
  71. * Clears the decoder.
  72. * @override
  73. */
  74. clear() {
  75. this.badFrames_ = 0;
  76. this.cea608DataArray_ = [];
  77. this.cea708DataArray_ = [];
  78. this.dtvccPacketBuilder_.clear();
  79. this.reset();
  80. // Clear all the CEA-708 services.
  81. for (const service of this.serviceNumberToService_.values()) {
  82. service.clear();
  83. }
  84. }
  85. /**
  86. * Resets the decoder.
  87. */
  88. reset() {
  89. this.currentField1Channel_ = 0;
  90. this.currentField2Channel_ = 0;
  91. for (const stream of this.cea608ModeToStream_.values()) {
  92. stream.reset();
  93. }
  94. }
  95. /**
  96. * Extracts closed caption bytes from CEA-X08 packets from the stream based on
  97. * ANSI/SCTE 128 and A/53, Part 4.
  98. * @override
  99. */
  100. extract(userDataSeiMessage, pts) {
  101. const reader = new shaka.util.DataViewReader(
  102. userDataSeiMessage, shaka.util.DataViewReader.Endianness.BIG_ENDIAN);
  103. if (reader.readUint8() !== shaka.cea.CeaDecoder.USA_COUNTRY_CODE) {
  104. return;
  105. }
  106. if (reader.readUint16() !== shaka.cea.CeaDecoder.ATSC_PROVIDER_CODE) {
  107. return;
  108. }
  109. if (reader.readUint32() !== shaka.cea.CeaDecoder.ATSC1_USER_IDENTIFIER) {
  110. return;
  111. }
  112. // user_data_type_code: 0x03 - cc_data()
  113. if (reader.readUint8() !== 0x03) {
  114. return;
  115. }
  116. // 1 bit reserved
  117. // 1 bit process_cc_data_flag
  118. // 1 bit zero_bit
  119. // 5 bits cc_count
  120. const captionData = reader.readUint8();
  121. // If process_cc_data_flag is not set, do not process this data.
  122. if ((captionData & 0x40) === 0) {
  123. return;
  124. }
  125. const count = captionData & 0x1f;
  126. // 8 bits reserved
  127. reader.skip(1);
  128. for (let i = 0; i < count; i++) {
  129. const cc = reader.readUint8();
  130. // When ccValid is 0, the next two bytes should be discarded.
  131. const ccValid = (cc & 0x04) >> 2;
  132. const ccData1 = reader.readUint8();
  133. const ccData2 = reader.readUint8();
  134. if (ccValid) {
  135. const ccType = cc & 0x03;
  136. // Send the packet to the appropriate data array (CEA-608 or CEA-708).
  137. if (ccType === shaka.cea.CeaDecoder.NTSC_CC_FIELD_1 ||
  138. ccType === shaka.cea.CeaDecoder.NTSC_CC_FIELD_2) {
  139. // CEA-608 NTSC (Line 21) Data.
  140. this.cea608DataArray_.push({
  141. pts,
  142. type: ccType,
  143. ccData1,
  144. ccData2,
  145. order: this.cea608DataArray_.length,
  146. });
  147. } else {
  148. // CEA-708 DTVCC Data.
  149. this.cea708DataArray_.push({
  150. pts,
  151. type: ccType,
  152. value: ccData1,
  153. order: this.cea708DataArray_.length,
  154. });
  155. // The second byte should always be labelled as DTVCC packet data.
  156. // Even if this pair was a DTVCC packet start, only the first byte
  157. // contains header info, and the second byte is just packet data.
  158. this.cea708DataArray_.push({
  159. pts,
  160. type: shaka.cea.DtvccPacketBuilder.DTVCC_PACKET_DATA,
  161. value: ccData2,
  162. order: this.cea708DataArray_.length,
  163. });
  164. }
  165. }
  166. }
  167. }
  168. /**
  169. * Decodes extracted closed caption data.
  170. * @override
  171. */
  172. decode() {
  173. /** @type {!Array.<!shaka.cea.ICaptionDecoder.ClosedCaption>} */
  174. const parsedClosedCaptions = [];
  175. // In some versions of Chrome, and other browsers, the default sorting
  176. // algorithm isn't stable. This comparator sorts on presentation
  177. // timestamp, and breaks ties on receive order (position in array).
  178. const stableComparator =
  179. (p1, p2) => (p1.pts - p2.pts) || (p1.order - p2.order);
  180. this.cea608DataArray_.sort(stableComparator);
  181. this.cea708DataArray_.sort(stableComparator);
  182. // CEA-608 packets are just byte pairs. Decode all of them.
  183. for (const cea608Packet of this.cea608DataArray_) {
  184. const parsedClosedCaption = this.decodeCea608_(cea608Packet);
  185. if (parsedClosedCaption) {
  186. parsedClosedCaptions.push(parsedClosedCaption);
  187. }
  188. }
  189. // CEA-708 packets are DTVCC packets composed of many byte pairs. Add all
  190. // byte pairs to the packet builder, and process + clear any ready packets.
  191. for (const cea708Byte of this.cea708DataArray_) {
  192. this.dtvccPacketBuilder_.addByte(cea708Byte);
  193. }
  194. const dtvccPackets = this.dtvccPacketBuilder_.getBuiltPackets();
  195. for (const dtvccPacket of dtvccPackets) {
  196. const closedCaptions = this.decodeCea708_(dtvccPacket);
  197. parsedClosedCaptions.push(...closedCaptions);
  198. }
  199. // Clear all processed data.
  200. this.dtvccPacketBuilder_.clearBuiltPackets();
  201. this.cea608DataArray_ = [];
  202. this.cea708DataArray_ = [];
  203. return parsedClosedCaptions;
  204. }
  205. /**
  206. * Decodes a CEA-608 closed caption packet based on ANSI/CEA-608.
  207. * @param {shaka.cea.Cea608DataChannel.Cea608Packet} ccPacket
  208. * @return {?shaka.cea.ICaptionDecoder.ClosedCaption}
  209. * @private
  210. */
  211. decodeCea608_(ccPacket) {
  212. const fieldNum = ccPacket.type;
  213. // If this packet is a control code, then it also sets the channel.
  214. // For control codes, cc_data_1 has the form |P|0|0|1|C|X|X|X|.
  215. // "C" is the channel bit. It indicates whether to set C2 active.
  216. if (shaka.cea.Cea608DataChannel.isControlCode(ccPacket.ccData1)) {
  217. const channelNum = (ccPacket.ccData1 >> 3) & 0x01; // Get channel bit.
  218. // Change the stream based on the field, and the new channel
  219. if (fieldNum === 0) {
  220. this.currentField1Channel_ = channelNum;
  221. } else {
  222. this.currentField2Channel_ = channelNum;
  223. }
  224. }
  225. // Get the correct stream for this caption packet (CC1, ..., CC4)
  226. const selectedChannel = fieldNum ?
  227. this.currentField2Channel_ : this.currentField1Channel_;
  228. const selectedMode = `CC${(fieldNum << 1) | selectedChannel + 1}`;
  229. const selectedStream = this.cea608ModeToStream_.get(selectedMode);
  230. // Check for bad frames (bad pairs). This can be two 0xff, two 0x00, or any
  231. // byte of even parity. ccData1 and ccData2 should be uint8 of odd parity.
  232. if ((ccPacket.ccData1 === 0xff && ccPacket.ccData2 === 0xff) ||
  233. (!ccPacket.ccData1 && !ccPacket.ccData2) ||
  234. !this.isOddParity_(ccPacket.ccData1) ||
  235. !this.isOddParity_(ccPacket.ccData2)) {
  236. // Per CEA-608-B C.21, reset the memory after 45 consecutive bad frames.
  237. if (++this.badFrames_ >= 45) {
  238. this.reset();
  239. }
  240. return null;
  241. }
  242. this.badFrames_ = 0;
  243. // Remove the MSB (parity bit).
  244. ccPacket.ccData1 &= 0x7f;
  245. ccPacket.ccData2 &= 0x7f;
  246. // Check for empty captions and skip them.
  247. if (!ccPacket.ccData1 && !ccPacket.ccData2) {
  248. return null;
  249. }
  250. // Process the clean CC data pair.
  251. let parsedClosedCaption = null;
  252. if (shaka.cea.Cea608DataChannel.isControlCode(ccPacket.ccData1)) {
  253. parsedClosedCaption = selectedStream.handleControlCode(ccPacket);
  254. } else {
  255. // Handle as a Basic North American Character.
  256. selectedStream.handleBasicNorthAmericanChar(
  257. ccPacket.ccData1, ccPacket.ccData2);
  258. }
  259. return parsedClosedCaption;
  260. }
  261. /**
  262. * Decodes a CEA-708 DTVCC packet based on ANSI/CTA-708-E.
  263. * @param {shaka.cea.DtvccPacket} dtvccPacket
  264. * @return {!Array<!shaka.cea.ICaptionDecoder.ClosedCaption>}
  265. * @private
  266. */
  267. decodeCea708_(dtvccPacket) {
  268. const parsedClosedCaptions = [];
  269. try {
  270. while (dtvccPacket.hasMoreData()) {
  271. // Process a service block.
  272. const serviceBlockHeader = dtvccPacket.readByte().value;
  273. // First 3 bits are service number, next 5 are block size,
  274. // representing the number of bytes coming in this block
  275. // (discluding a possible extended service block header byte)
  276. let serviceNumber = (serviceBlockHeader & 0xe0) >> 5;
  277. const blockSize = serviceBlockHeader & 0x1f;
  278. if (serviceNumber === /* 0b111 */ 0x07 && blockSize != 0) {
  279. // 2 bits null padding, 6 bits extended service number
  280. const extendedServiceBlockHeader = dtvccPacket.readByte().value;
  281. serviceNumber = extendedServiceBlockHeader & 0x3f;
  282. }
  283. // As per CEA-708-E, service number 0 is invalid, and should be ignored.
  284. if (serviceNumber != 0) {
  285. // If the service doesn't already exist, create it.
  286. if (!this.serviceNumberToService_.has(serviceNumber)) {
  287. const service = new shaka.cea.Cea708Service(serviceNumber);
  288. this.serviceNumberToService_.set(serviceNumber, service);
  289. }
  290. const service = this.serviceNumberToService_.get(serviceNumber);
  291. // Process all control codes.
  292. const startPos = dtvccPacket.getPosition();
  293. // Execute this loop `blockSize` times, to decode the control codes.
  294. while (dtvccPacket.getPosition() - startPos < blockSize) {
  295. const closedCaption = service.handleCea708ControlCode(dtvccPacket);
  296. if (closedCaption) {
  297. parsedClosedCaptions.push(closedCaption);
  298. }
  299. } // position < end of block
  300. } // serviceNumber != 0
  301. } // hasMoreData
  302. } catch (error) {
  303. if (error instanceof shaka.util.Error &&
  304. error.code === shaka.util.Error.Code.BUFFER_READ_OUT_OF_BOUNDS) {
  305. shaka.log.warnOnce('CEA708_INVALID_DATA',
  306. 'Buffer read out of bounds / invalid CEA-708 Data.');
  307. } else {
  308. // This is an unexpected error, and should be rethrown.
  309. throw error;
  310. }
  311. }
  312. return parsedClosedCaptions;
  313. }
  314. /**
  315. * Checks if a byte has odd parity (Odd number of 1s in binary).
  316. * @param {number} byte
  317. * @return {boolean} True if the byte has odd parity.
  318. * @private
  319. */
  320. isOddParity_(byte) {
  321. let parity = 0;
  322. while (byte) {
  323. parity ^= (byte & 1); // toggle parity if low bit is 1
  324. byte >>= 1; // shift away the low bit
  325. }
  326. return parity === 1;
  327. }
  328. };
  329. /**
  330. * itu_t_35_provider_code for ATSC user_data
  331. * @private @const {number}
  332. */
  333. shaka.cea.CeaDecoder.ATSC_PROVIDER_CODE = 0x0031;
  334. /**
  335. * When provider is ATSC user data, the ATSC_user_identifier code
  336. * for ATSC1_data is "GA94" (0x47413934)
  337. * @private @const {number}
  338. */
  339. shaka.cea.CeaDecoder.ATSC1_USER_IDENTIFIER = 0x47413934;
  340. /**
  341. * @private @const {number}
  342. */
  343. shaka.cea.CeaDecoder.NTSC_CC_FIELD_1 = 0;
  344. /**
  345. * @private @const {number}
  346. */
  347. shaka.cea.CeaDecoder.NTSC_CC_FIELD_2 = 1;
  348. /**
  349. * 0xB5 is USA's code (Rec. ITU-T T.35)
  350. * @private @const {number}
  351. */
  352. shaka.cea.CeaDecoder.USA_COUNTRY_CODE = 0xb5;