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