caption-stream.js 52 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887
  1. /**
  2. * mux.js
  3. *
  4. * Copyright (c) Brightcove
  5. * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
  6. *
  7. * Reads in-band caption information from a video elementary
  8. * stream. Captions must follow the CEA-708 standard for injection
  9. * into an MPEG-2 transport streams.
  10. * @see https://en.wikipedia.org/wiki/CEA-708
  11. * @see https://www.gpo.gov/fdsys/pkg/CFR-2007-title47-vol1/pdf/CFR-2007-title47-vol1-sec15-119.pdf
  12. */
  13. 'use strict'; // -----------------
  14. // Link To Transport
  15. // -----------------
  16. var Stream = require('../utils/stream');
  17. var cea708Parser = require('../tools/caption-packet-parser');
  18. var CaptionStream = function CaptionStream(options) {
  19. options = options || {};
  20. CaptionStream.prototype.init.call(this); // parse708captions flag, default to true
  21. this.parse708captions_ = typeof options.parse708captions === 'boolean' ? options.parse708captions : true;
  22. this.captionPackets_ = [];
  23. this.ccStreams_ = [new Cea608Stream(0, 0), // eslint-disable-line no-use-before-define
  24. new Cea608Stream(0, 1), // eslint-disable-line no-use-before-define
  25. new Cea608Stream(1, 0), // eslint-disable-line no-use-before-define
  26. new Cea608Stream(1, 1) // eslint-disable-line no-use-before-define
  27. ];
  28. if (this.parse708captions_) {
  29. this.cc708Stream_ = new Cea708Stream({
  30. captionServices: options.captionServices
  31. }); // eslint-disable-line no-use-before-define
  32. }
  33. this.reset(); // forward data and done events from CCs to this CaptionStream
  34. this.ccStreams_.forEach(function (cc) {
  35. cc.on('data', this.trigger.bind(this, 'data'));
  36. cc.on('partialdone', this.trigger.bind(this, 'partialdone'));
  37. cc.on('done', this.trigger.bind(this, 'done'));
  38. }, this);
  39. if (this.parse708captions_) {
  40. this.cc708Stream_.on('data', this.trigger.bind(this, 'data'));
  41. this.cc708Stream_.on('partialdone', this.trigger.bind(this, 'partialdone'));
  42. this.cc708Stream_.on('done', this.trigger.bind(this, 'done'));
  43. }
  44. };
  45. CaptionStream.prototype = new Stream();
  46. CaptionStream.prototype.push = function (event) {
  47. var sei, userData, newCaptionPackets; // only examine SEI NALs
  48. if (event.nalUnitType !== 'sei_rbsp') {
  49. return;
  50. } // parse the sei
  51. sei = cea708Parser.parseSei(event.escapedRBSP); // no payload data, skip
  52. if (!sei.payload) {
  53. return;
  54. } // ignore everything but user_data_registered_itu_t_t35
  55. if (sei.payloadType !== cea708Parser.USER_DATA_REGISTERED_ITU_T_T35) {
  56. return;
  57. } // parse out the user data payload
  58. userData = cea708Parser.parseUserData(sei); // ignore unrecognized userData
  59. if (!userData) {
  60. return;
  61. } // Sometimes, the same segment # will be downloaded twice. To stop the
  62. // caption data from being processed twice, we track the latest dts we've
  63. // received and ignore everything with a dts before that. However, since
  64. // data for a specific dts can be split across packets on either side of
  65. // a segment boundary, we need to make sure we *don't* ignore the packets
  66. // from the *next* segment that have dts === this.latestDts_. By constantly
  67. // tracking the number of packets received with dts === this.latestDts_, we
  68. // know how many should be ignored once we start receiving duplicates.
  69. if (event.dts < this.latestDts_) {
  70. // We've started getting older data, so set the flag.
  71. this.ignoreNextEqualDts_ = true;
  72. return;
  73. } else if (event.dts === this.latestDts_ && this.ignoreNextEqualDts_) {
  74. this.numSameDts_--;
  75. if (!this.numSameDts_) {
  76. // We've received the last duplicate packet, time to start processing again
  77. this.ignoreNextEqualDts_ = false;
  78. }
  79. return;
  80. } // parse out CC data packets and save them for later
  81. newCaptionPackets = cea708Parser.parseCaptionPackets(event.pts, userData);
  82. this.captionPackets_ = this.captionPackets_.concat(newCaptionPackets);
  83. if (this.latestDts_ !== event.dts) {
  84. this.numSameDts_ = 0;
  85. }
  86. this.numSameDts_++;
  87. this.latestDts_ = event.dts;
  88. };
  89. CaptionStream.prototype.flushCCStreams = function (flushType) {
  90. this.ccStreams_.forEach(function (cc) {
  91. return flushType === 'flush' ? cc.flush() : cc.partialFlush();
  92. }, this);
  93. };
  94. CaptionStream.prototype.flushStream = function (flushType) {
  95. // make sure we actually parsed captions before proceeding
  96. if (!this.captionPackets_.length) {
  97. this.flushCCStreams(flushType);
  98. return;
  99. } // In Chrome, the Array#sort function is not stable so add a
  100. // presortIndex that we can use to ensure we get a stable-sort
  101. this.captionPackets_.forEach(function (elem, idx) {
  102. elem.presortIndex = idx;
  103. }); // sort caption byte-pairs based on their PTS values
  104. this.captionPackets_.sort(function (a, b) {
  105. if (a.pts === b.pts) {
  106. return a.presortIndex - b.presortIndex;
  107. }
  108. return a.pts - b.pts;
  109. });
  110. this.captionPackets_.forEach(function (packet) {
  111. if (packet.type < 2) {
  112. // Dispatch packet to the right Cea608Stream
  113. this.dispatchCea608Packet(packet);
  114. } else {
  115. // Dispatch packet to the Cea708Stream
  116. this.dispatchCea708Packet(packet);
  117. }
  118. }, this);
  119. this.captionPackets_.length = 0;
  120. this.flushCCStreams(flushType);
  121. };
  122. CaptionStream.prototype.flush = function () {
  123. return this.flushStream('flush');
  124. }; // Only called if handling partial data
  125. CaptionStream.prototype.partialFlush = function () {
  126. return this.flushStream('partialFlush');
  127. };
  128. CaptionStream.prototype.reset = function () {
  129. this.latestDts_ = null;
  130. this.ignoreNextEqualDts_ = false;
  131. this.numSameDts_ = 0;
  132. this.activeCea608Channel_ = [null, null];
  133. this.ccStreams_.forEach(function (ccStream) {
  134. ccStream.reset();
  135. });
  136. }; // From the CEA-608 spec:
  137. /*
  138. * When XDS sub-packets are interleaved with other services, the end of each sub-packet shall be followed
  139. * by a control pair to change to a different service. When any of the control codes from 0x10 to 0x1F is
  140. * used to begin a control code pair, it indicates the return to captioning or Text data. The control code pair
  141. * and subsequent data should then be processed according to the FCC rules. It may be necessary for the
  142. * line 21 data encoder to automatically insert a control code pair (i.e. RCL, RU2, RU3, RU4, RDC, or RTD)
  143. * to switch to captioning or Text.
  144. */
  145. // With that in mind, we ignore any data between an XDS control code and a
  146. // subsequent closed-captioning control code.
  147. CaptionStream.prototype.dispatchCea608Packet = function (packet) {
  148. // NOTE: packet.type is the CEA608 field
  149. if (this.setsTextOrXDSActive(packet)) {
  150. this.activeCea608Channel_[packet.type] = null;
  151. } else if (this.setsChannel1Active(packet)) {
  152. this.activeCea608Channel_[packet.type] = 0;
  153. } else if (this.setsChannel2Active(packet)) {
  154. this.activeCea608Channel_[packet.type] = 1;
  155. }
  156. if (this.activeCea608Channel_[packet.type] === null) {
  157. // If we haven't received anything to set the active channel, or the
  158. // packets are Text/XDS data, discard the data; we don't want jumbled
  159. // captions
  160. return;
  161. }
  162. this.ccStreams_[(packet.type << 1) + this.activeCea608Channel_[packet.type]].push(packet);
  163. };
  164. CaptionStream.prototype.setsChannel1Active = function (packet) {
  165. return (packet.ccData & 0x7800) === 0x1000;
  166. };
  167. CaptionStream.prototype.setsChannel2Active = function (packet) {
  168. return (packet.ccData & 0x7800) === 0x1800;
  169. };
  170. CaptionStream.prototype.setsTextOrXDSActive = function (packet) {
  171. return (packet.ccData & 0x7100) === 0x0100 || (packet.ccData & 0x78fe) === 0x102a || (packet.ccData & 0x78fe) === 0x182a;
  172. };
  173. CaptionStream.prototype.dispatchCea708Packet = function (packet) {
  174. if (this.parse708captions_) {
  175. this.cc708Stream_.push(packet);
  176. }
  177. }; // ----------------------
  178. // Session to Application
  179. // ----------------------
  180. // This hash maps special and extended character codes to their
  181. // proper Unicode equivalent. The first one-byte key is just a
  182. // non-standard character code. The two-byte keys that follow are
  183. // the extended CEA708 character codes, along with the preceding
  184. // 0x10 extended character byte to distinguish these codes from
  185. // non-extended character codes. Every CEA708 character code that
  186. // is not in this object maps directly to a standard unicode
  187. // character code.
  188. // The transparent space and non-breaking transparent space are
  189. // technically not fully supported since there is no code to
  190. // make them transparent, so they have normal non-transparent
  191. // stand-ins.
  192. // The special closed caption (CC) character isn't a standard
  193. // unicode character, so a fairly similar unicode character was
  194. // chosen in it's place.
  195. var CHARACTER_TRANSLATION_708 = {
  196. 0x7f: 0x266a,
  197. // ♪
  198. 0x1020: 0x20,
  199. // Transparent Space
  200. 0x1021: 0xa0,
  201. // Nob-breaking Transparent Space
  202. 0x1025: 0x2026,
  203. // …
  204. 0x102a: 0x0160,
  205. // Š
  206. 0x102c: 0x0152,
  207. // Œ
  208. 0x1030: 0x2588,
  209. // █
  210. 0x1031: 0x2018,
  211. // ‘
  212. 0x1032: 0x2019,
  213. // ’
  214. 0x1033: 0x201c,
  215. // “
  216. 0x1034: 0x201d,
  217. // ”
  218. 0x1035: 0x2022,
  219. // •
  220. 0x1039: 0x2122,
  221. // ™
  222. 0x103a: 0x0161,
  223. // š
  224. 0x103c: 0x0153,
  225. // œ
  226. 0x103d: 0x2120,
  227. // ℠
  228. 0x103f: 0x0178,
  229. // Ÿ
  230. 0x1076: 0x215b,
  231. // ⅛
  232. 0x1077: 0x215c,
  233. // ⅜
  234. 0x1078: 0x215d,
  235. // ⅝
  236. 0x1079: 0x215e,
  237. // ⅞
  238. 0x107a: 0x23d0,
  239. // ⏐
  240. 0x107b: 0x23a4,
  241. // ⎤
  242. 0x107c: 0x23a3,
  243. // ⎣
  244. 0x107d: 0x23af,
  245. // ⎯
  246. 0x107e: 0x23a6,
  247. // ⎦
  248. 0x107f: 0x23a1,
  249. // ⎡
  250. 0x10a0: 0x3138 // ㄸ (CC char)
  251. };
  252. var get708CharFromCode = function get708CharFromCode(code) {
  253. var newCode = CHARACTER_TRANSLATION_708[code] || code;
  254. if (code & 0x1000 && code === newCode) {
  255. // Invalid extended code
  256. return '';
  257. }
  258. return String.fromCharCode(newCode);
  259. };
  260. var within708TextBlock = function within708TextBlock(b) {
  261. return 0x20 <= b && b <= 0x7f || 0xa0 <= b && b <= 0xff;
  262. };
  263. var Cea708Window = function Cea708Window(windowNum) {
  264. this.windowNum = windowNum;
  265. this.reset();
  266. };
  267. Cea708Window.prototype.reset = function () {
  268. this.clearText();
  269. this.pendingNewLine = false;
  270. this.winAttr = {};
  271. this.penAttr = {};
  272. this.penLoc = {};
  273. this.penColor = {}; // These default values are arbitrary,
  274. // defineWindow will usually override them
  275. this.visible = 0;
  276. this.rowLock = 0;
  277. this.columnLock = 0;
  278. this.priority = 0;
  279. this.relativePositioning = 0;
  280. this.anchorVertical = 0;
  281. this.anchorHorizontal = 0;
  282. this.anchorPoint = 0;
  283. this.rowCount = 1;
  284. this.virtualRowCount = this.rowCount + 1;
  285. this.columnCount = 41;
  286. this.windowStyle = 0;
  287. this.penStyle = 0;
  288. };
  289. Cea708Window.prototype.getText = function () {
  290. return this.rows.join('\n');
  291. };
  292. Cea708Window.prototype.clearText = function () {
  293. this.rows = [''];
  294. this.rowIdx = 0;
  295. };
  296. Cea708Window.prototype.newLine = function (pts) {
  297. if (this.rows.length >= this.virtualRowCount && typeof this.beforeRowOverflow === 'function') {
  298. this.beforeRowOverflow(pts);
  299. }
  300. if (this.rows.length > 0) {
  301. this.rows.push('');
  302. this.rowIdx++;
  303. } // Show all virtual rows since there's no visible scrolling
  304. while (this.rows.length > this.virtualRowCount) {
  305. this.rows.shift();
  306. this.rowIdx--;
  307. }
  308. };
  309. Cea708Window.prototype.isEmpty = function () {
  310. if (this.rows.length === 0) {
  311. return true;
  312. } else if (this.rows.length === 1) {
  313. return this.rows[0] === '';
  314. }
  315. return false;
  316. };
  317. Cea708Window.prototype.addText = function (text) {
  318. this.rows[this.rowIdx] += text;
  319. };
  320. Cea708Window.prototype.backspace = function () {
  321. if (!this.isEmpty()) {
  322. var row = this.rows[this.rowIdx];
  323. this.rows[this.rowIdx] = row.substr(0, row.length - 1);
  324. }
  325. };
  326. var Cea708Service = function Cea708Service(serviceNum, encoding, stream) {
  327. this.serviceNum = serviceNum;
  328. this.text = '';
  329. this.currentWindow = new Cea708Window(-1);
  330. this.windows = [];
  331. this.stream = stream; // Try to setup a TextDecoder if an `encoding` value was provided
  332. if (typeof encoding === 'string') {
  333. this.createTextDecoder(encoding);
  334. }
  335. };
  336. /**
  337. * Initialize service windows
  338. * Must be run before service use
  339. *
  340. * @param {Integer} pts PTS value
  341. * @param {Function} beforeRowOverflow Function to execute before row overflow of a window
  342. */
  343. Cea708Service.prototype.init = function (pts, beforeRowOverflow) {
  344. this.startPts = pts;
  345. for (var win = 0; win < 8; win++) {
  346. this.windows[win] = new Cea708Window(win);
  347. if (typeof beforeRowOverflow === 'function') {
  348. this.windows[win].beforeRowOverflow = beforeRowOverflow;
  349. }
  350. }
  351. };
  352. /**
  353. * Set current window of service to be affected by commands
  354. *
  355. * @param {Integer} windowNum Window number
  356. */
  357. Cea708Service.prototype.setCurrentWindow = function (windowNum) {
  358. this.currentWindow = this.windows[windowNum];
  359. };
  360. /**
  361. * Try to create a TextDecoder if it is natively supported
  362. */
  363. Cea708Service.prototype.createTextDecoder = function (encoding) {
  364. if (typeof TextDecoder === 'undefined') {
  365. this.stream.trigger('log', {
  366. level: 'warn',
  367. message: 'The `encoding` option is unsupported without TextDecoder support'
  368. });
  369. } else {
  370. try {
  371. this.textDecoder_ = new TextDecoder(encoding);
  372. } catch (error) {
  373. this.stream.trigger('log', {
  374. level: 'warn',
  375. message: 'TextDecoder could not be created with ' + encoding + ' encoding. ' + error
  376. });
  377. }
  378. }
  379. };
  380. var Cea708Stream = function Cea708Stream(options) {
  381. options = options || {};
  382. Cea708Stream.prototype.init.call(this);
  383. var self = this;
  384. var captionServices = options.captionServices || {};
  385. var captionServiceEncodings = {};
  386. var serviceProps; // Get service encodings from captionServices option block
  387. Object.keys(captionServices).forEach(function (serviceName) {
  388. serviceProps = captionServices[serviceName];
  389. if (/^SERVICE/.test(serviceName)) {
  390. captionServiceEncodings[serviceName] = serviceProps.encoding;
  391. }
  392. });
  393. this.serviceEncodings = captionServiceEncodings;
  394. this.current708Packet = null;
  395. this.services = {};
  396. this.push = function (packet) {
  397. if (packet.type === 3) {
  398. // 708 packet start
  399. self.new708Packet();
  400. self.add708Bytes(packet);
  401. } else {
  402. if (self.current708Packet === null) {
  403. // This should only happen at the start of a file if there's no packet start.
  404. self.new708Packet();
  405. }
  406. self.add708Bytes(packet);
  407. }
  408. };
  409. };
  410. Cea708Stream.prototype = new Stream();
  411. /**
  412. * Push current 708 packet, create new 708 packet.
  413. */
  414. Cea708Stream.prototype.new708Packet = function () {
  415. if (this.current708Packet !== null) {
  416. this.push708Packet();
  417. }
  418. this.current708Packet = {
  419. data: [],
  420. ptsVals: []
  421. };
  422. };
  423. /**
  424. * Add pts and both bytes from packet into current 708 packet.
  425. */
  426. Cea708Stream.prototype.add708Bytes = function (packet) {
  427. var data = packet.ccData;
  428. var byte0 = data >>> 8;
  429. var byte1 = data & 0xff; // I would just keep a list of packets instead of bytes, but it isn't clear in the spec
  430. // that service blocks will always line up with byte pairs.
  431. this.current708Packet.ptsVals.push(packet.pts);
  432. this.current708Packet.data.push(byte0);
  433. this.current708Packet.data.push(byte1);
  434. };
  435. /**
  436. * Parse completed 708 packet into service blocks and push each service block.
  437. */
  438. Cea708Stream.prototype.push708Packet = function () {
  439. var packet708 = this.current708Packet;
  440. var packetData = packet708.data;
  441. var serviceNum = null;
  442. var blockSize = null;
  443. var i = 0;
  444. var b = packetData[i++];
  445. packet708.seq = b >> 6;
  446. packet708.sizeCode = b & 0x3f; // 0b00111111;
  447. for (; i < packetData.length; i++) {
  448. b = packetData[i++];
  449. serviceNum = b >> 5;
  450. blockSize = b & 0x1f; // 0b00011111
  451. if (serviceNum === 7 && blockSize > 0) {
  452. // Extended service num
  453. b = packetData[i++];
  454. serviceNum = b;
  455. }
  456. this.pushServiceBlock(serviceNum, i, blockSize);
  457. if (blockSize > 0) {
  458. i += blockSize - 1;
  459. }
  460. }
  461. };
  462. /**
  463. * Parse service block, execute commands, read text.
  464. *
  465. * Note: While many of these commands serve important purposes,
  466. * many others just parse out the parameters or attributes, but
  467. * nothing is done with them because this is not a full and complete
  468. * implementation of the entire 708 spec.
  469. *
  470. * @param {Integer} serviceNum Service number
  471. * @param {Integer} start Start index of the 708 packet data
  472. * @param {Integer} size Block size
  473. */
  474. Cea708Stream.prototype.pushServiceBlock = function (serviceNum, start, size) {
  475. var b;
  476. var i = start;
  477. var packetData = this.current708Packet.data;
  478. var service = this.services[serviceNum];
  479. if (!service) {
  480. service = this.initService(serviceNum, i);
  481. }
  482. for (; i < start + size && i < packetData.length; i++) {
  483. b = packetData[i];
  484. if (within708TextBlock(b)) {
  485. i = this.handleText(i, service);
  486. } else if (b === 0x18) {
  487. i = this.multiByteCharacter(i, service);
  488. } else if (b === 0x10) {
  489. i = this.extendedCommands(i, service);
  490. } else if (0x80 <= b && b <= 0x87) {
  491. i = this.setCurrentWindow(i, service);
  492. } else if (0x98 <= b && b <= 0x9f) {
  493. i = this.defineWindow(i, service);
  494. } else if (b === 0x88) {
  495. i = this.clearWindows(i, service);
  496. } else if (b === 0x8c) {
  497. i = this.deleteWindows(i, service);
  498. } else if (b === 0x89) {
  499. i = this.displayWindows(i, service);
  500. } else if (b === 0x8a) {
  501. i = this.hideWindows(i, service);
  502. } else if (b === 0x8b) {
  503. i = this.toggleWindows(i, service);
  504. } else if (b === 0x97) {
  505. i = this.setWindowAttributes(i, service);
  506. } else if (b === 0x90) {
  507. i = this.setPenAttributes(i, service);
  508. } else if (b === 0x91) {
  509. i = this.setPenColor(i, service);
  510. } else if (b === 0x92) {
  511. i = this.setPenLocation(i, service);
  512. } else if (b === 0x8f) {
  513. service = this.reset(i, service);
  514. } else if (b === 0x08) {
  515. // BS: Backspace
  516. service.currentWindow.backspace();
  517. } else if (b === 0x0c) {
  518. // FF: Form feed
  519. service.currentWindow.clearText();
  520. } else if (b === 0x0d) {
  521. // CR: Carriage return
  522. service.currentWindow.pendingNewLine = true;
  523. } else if (b === 0x0e) {
  524. // HCR: Horizontal carriage return
  525. service.currentWindow.clearText();
  526. } else if (b === 0x8d) {
  527. // DLY: Delay, nothing to do
  528. i++;
  529. } else if (b === 0x8e) {// DLC: Delay cancel, nothing to do
  530. } else if (b === 0x03) {// ETX: End Text, don't need to do anything
  531. } else if (b === 0x00) {// Padding
  532. } else {// Unknown command
  533. }
  534. }
  535. };
  536. /**
  537. * Execute an extended command
  538. *
  539. * @param {Integer} i Current index in the 708 packet
  540. * @param {Service} service The service object to be affected
  541. * @return {Integer} New index after parsing
  542. */
  543. Cea708Stream.prototype.extendedCommands = function (i, service) {
  544. var packetData = this.current708Packet.data;
  545. var b = packetData[++i];
  546. if (within708TextBlock(b)) {
  547. i = this.handleText(i, service, {
  548. isExtended: true
  549. });
  550. } else {// Unknown command
  551. }
  552. return i;
  553. };
  554. /**
  555. * Get PTS value of a given byte index
  556. *
  557. * @param {Integer} byteIndex Index of the byte
  558. * @return {Integer} PTS
  559. */
  560. Cea708Stream.prototype.getPts = function (byteIndex) {
  561. // There's 1 pts value per 2 bytes
  562. return this.current708Packet.ptsVals[Math.floor(byteIndex / 2)];
  563. };
  564. /**
  565. * Initializes a service
  566. *
  567. * @param {Integer} serviceNum Service number
  568. * @return {Service} Initialized service object
  569. */
  570. Cea708Stream.prototype.initService = function (serviceNum, i) {
  571. var serviceName = 'SERVICE' + serviceNum;
  572. var self = this;
  573. var serviceName;
  574. var encoding;
  575. if (serviceName in this.serviceEncodings) {
  576. encoding = this.serviceEncodings[serviceName];
  577. }
  578. this.services[serviceNum] = new Cea708Service(serviceNum, encoding, self);
  579. this.services[serviceNum].init(this.getPts(i), function (pts) {
  580. self.flushDisplayed(pts, self.services[serviceNum]);
  581. });
  582. return this.services[serviceNum];
  583. };
  584. /**
  585. * Execute text writing to current window
  586. *
  587. * @param {Integer} i Current index in the 708 packet
  588. * @param {Service} service The service object to be affected
  589. * @return {Integer} New index after parsing
  590. */
  591. Cea708Stream.prototype.handleText = function (i, service, options) {
  592. var isExtended = options && options.isExtended;
  593. var isMultiByte = options && options.isMultiByte;
  594. var packetData = this.current708Packet.data;
  595. var extended = isExtended ? 0x1000 : 0x0000;
  596. var currentByte = packetData[i];
  597. var nextByte = packetData[i + 1];
  598. var win = service.currentWindow;
  599. var char;
  600. var charCodeArray; // Use the TextDecoder if one was created for this service
  601. if (service.textDecoder_ && !isExtended) {
  602. if (isMultiByte) {
  603. charCodeArray = [currentByte, nextByte];
  604. i++;
  605. } else {
  606. charCodeArray = [currentByte];
  607. }
  608. char = service.textDecoder_.decode(new Uint8Array(charCodeArray));
  609. } else {
  610. char = get708CharFromCode(extended | currentByte);
  611. }
  612. if (win.pendingNewLine && !win.isEmpty()) {
  613. win.newLine(this.getPts(i));
  614. }
  615. win.pendingNewLine = false;
  616. win.addText(char);
  617. return i;
  618. };
  619. /**
  620. * Handle decoding of multibyte character
  621. *
  622. * @param {Integer} i Current index in the 708 packet
  623. * @param {Service} service The service object to be affected
  624. * @return {Integer} New index after parsing
  625. */
  626. Cea708Stream.prototype.multiByteCharacter = function (i, service) {
  627. var packetData = this.current708Packet.data;
  628. var firstByte = packetData[i + 1];
  629. var secondByte = packetData[i + 2];
  630. if (within708TextBlock(firstByte) && within708TextBlock(secondByte)) {
  631. i = this.handleText(++i, service, {
  632. isMultiByte: true
  633. });
  634. } else {// Unknown command
  635. }
  636. return i;
  637. };
  638. /**
  639. * Parse and execute the CW# command.
  640. *
  641. * Set the current window.
  642. *
  643. * @param {Integer} i Current index in the 708 packet
  644. * @param {Service} service The service object to be affected
  645. * @return {Integer} New index after parsing
  646. */
  647. Cea708Stream.prototype.setCurrentWindow = function (i, service) {
  648. var packetData = this.current708Packet.data;
  649. var b = packetData[i];
  650. var windowNum = b & 0x07;
  651. service.setCurrentWindow(windowNum);
  652. return i;
  653. };
  654. /**
  655. * Parse and execute the DF# command.
  656. *
  657. * Define a window and set it as the current window.
  658. *
  659. * @param {Integer} i Current index in the 708 packet
  660. * @param {Service} service The service object to be affected
  661. * @return {Integer} New index after parsing
  662. */
  663. Cea708Stream.prototype.defineWindow = function (i, service) {
  664. var packetData = this.current708Packet.data;
  665. var b = packetData[i];
  666. var windowNum = b & 0x07;
  667. service.setCurrentWindow(windowNum);
  668. var win = service.currentWindow;
  669. b = packetData[++i];
  670. win.visible = (b & 0x20) >> 5; // v
  671. win.rowLock = (b & 0x10) >> 4; // rl
  672. win.columnLock = (b & 0x08) >> 3; // cl
  673. win.priority = b & 0x07; // p
  674. b = packetData[++i];
  675. win.relativePositioning = (b & 0x80) >> 7; // rp
  676. win.anchorVertical = b & 0x7f; // av
  677. b = packetData[++i];
  678. win.anchorHorizontal = b; // ah
  679. b = packetData[++i];
  680. win.anchorPoint = (b & 0xf0) >> 4; // ap
  681. win.rowCount = b & 0x0f; // rc
  682. b = packetData[++i];
  683. win.columnCount = b & 0x3f; // cc
  684. b = packetData[++i];
  685. win.windowStyle = (b & 0x38) >> 3; // ws
  686. win.penStyle = b & 0x07; // ps
  687. // The spec says there are (rowCount+1) "virtual rows"
  688. win.virtualRowCount = win.rowCount + 1;
  689. return i;
  690. };
  691. /**
  692. * Parse and execute the SWA command.
  693. *
  694. * Set attributes of the current window.
  695. *
  696. * @param {Integer} i Current index in the 708 packet
  697. * @param {Service} service The service object to be affected
  698. * @return {Integer} New index after parsing
  699. */
  700. Cea708Stream.prototype.setWindowAttributes = function (i, service) {
  701. var packetData = this.current708Packet.data;
  702. var b = packetData[i];
  703. var winAttr = service.currentWindow.winAttr;
  704. b = packetData[++i];
  705. winAttr.fillOpacity = (b & 0xc0) >> 6; // fo
  706. winAttr.fillRed = (b & 0x30) >> 4; // fr
  707. winAttr.fillGreen = (b & 0x0c) >> 2; // fg
  708. winAttr.fillBlue = b & 0x03; // fb
  709. b = packetData[++i];
  710. winAttr.borderType = (b & 0xc0) >> 6; // bt
  711. winAttr.borderRed = (b & 0x30) >> 4; // br
  712. winAttr.borderGreen = (b & 0x0c) >> 2; // bg
  713. winAttr.borderBlue = b & 0x03; // bb
  714. b = packetData[++i];
  715. winAttr.borderType += (b & 0x80) >> 5; // bt
  716. winAttr.wordWrap = (b & 0x40) >> 6; // ww
  717. winAttr.printDirection = (b & 0x30) >> 4; // pd
  718. winAttr.scrollDirection = (b & 0x0c) >> 2; // sd
  719. winAttr.justify = b & 0x03; // j
  720. b = packetData[++i];
  721. winAttr.effectSpeed = (b & 0xf0) >> 4; // es
  722. winAttr.effectDirection = (b & 0x0c) >> 2; // ed
  723. winAttr.displayEffect = b & 0x03; // de
  724. return i;
  725. };
  726. /**
  727. * Gather text from all displayed windows and push a caption to output.
  728. *
  729. * @param {Integer} i Current index in the 708 packet
  730. * @param {Service} service The service object to be affected
  731. */
  732. Cea708Stream.prototype.flushDisplayed = function (pts, service) {
  733. var displayedText = []; // TODO: Positioning not supported, displaying multiple windows will not necessarily
  734. // display text in the correct order, but sample files so far have not shown any issue.
  735. for (var winId = 0; winId < 8; winId++) {
  736. if (service.windows[winId].visible && !service.windows[winId].isEmpty()) {
  737. displayedText.push(service.windows[winId].getText());
  738. }
  739. }
  740. service.endPts = pts;
  741. service.text = displayedText.join('\n\n');
  742. this.pushCaption(service);
  743. service.startPts = pts;
  744. };
  745. /**
  746. * Push a caption to output if the caption contains text.
  747. *
  748. * @param {Service} service The service object to be affected
  749. */
  750. Cea708Stream.prototype.pushCaption = function (service) {
  751. if (service.text !== '') {
  752. this.trigger('data', {
  753. startPts: service.startPts,
  754. endPts: service.endPts,
  755. text: service.text,
  756. stream: 'cc708_' + service.serviceNum
  757. });
  758. service.text = '';
  759. service.startPts = service.endPts;
  760. }
  761. };
  762. /**
  763. * Parse and execute the DSW command.
  764. *
  765. * Set visible property of windows based on the parsed bitmask.
  766. *
  767. * @param {Integer} i Current index in the 708 packet
  768. * @param {Service} service The service object to be affected
  769. * @return {Integer} New index after parsing
  770. */
  771. Cea708Stream.prototype.displayWindows = function (i, service) {
  772. var packetData = this.current708Packet.data;
  773. var b = packetData[++i];
  774. var pts = this.getPts(i);
  775. this.flushDisplayed(pts, service);
  776. for (var winId = 0; winId < 8; winId++) {
  777. if (b & 0x01 << winId) {
  778. service.windows[winId].visible = 1;
  779. }
  780. }
  781. return i;
  782. };
  783. /**
  784. * Parse and execute the HDW command.
  785. *
  786. * Set visible property of windows based on the parsed bitmask.
  787. *
  788. * @param {Integer} i Current index in the 708 packet
  789. * @param {Service} service The service object to be affected
  790. * @return {Integer} New index after parsing
  791. */
  792. Cea708Stream.prototype.hideWindows = function (i, service) {
  793. var packetData = this.current708Packet.data;
  794. var b = packetData[++i];
  795. var pts = this.getPts(i);
  796. this.flushDisplayed(pts, service);
  797. for (var winId = 0; winId < 8; winId++) {
  798. if (b & 0x01 << winId) {
  799. service.windows[winId].visible = 0;
  800. }
  801. }
  802. return i;
  803. };
  804. /**
  805. * Parse and execute the TGW command.
  806. *
  807. * Set visible property of windows based on the parsed bitmask.
  808. *
  809. * @param {Integer} i Current index in the 708 packet
  810. * @param {Service} service The service object to be affected
  811. * @return {Integer} New index after parsing
  812. */
  813. Cea708Stream.prototype.toggleWindows = function (i, service) {
  814. var packetData = this.current708Packet.data;
  815. var b = packetData[++i];
  816. var pts = this.getPts(i);
  817. this.flushDisplayed(pts, service);
  818. for (var winId = 0; winId < 8; winId++) {
  819. if (b & 0x01 << winId) {
  820. service.windows[winId].visible ^= 1;
  821. }
  822. }
  823. return i;
  824. };
  825. /**
  826. * Parse and execute the CLW command.
  827. *
  828. * Clear text of windows based on the parsed bitmask.
  829. *
  830. * @param {Integer} i Current index in the 708 packet
  831. * @param {Service} service The service object to be affected
  832. * @return {Integer} New index after parsing
  833. */
  834. Cea708Stream.prototype.clearWindows = function (i, service) {
  835. var packetData = this.current708Packet.data;
  836. var b = packetData[++i];
  837. var pts = this.getPts(i);
  838. this.flushDisplayed(pts, service);
  839. for (var winId = 0; winId < 8; winId++) {
  840. if (b & 0x01 << winId) {
  841. service.windows[winId].clearText();
  842. }
  843. }
  844. return i;
  845. };
  846. /**
  847. * Parse and execute the DLW command.
  848. *
  849. * Re-initialize windows based on the parsed bitmask.
  850. *
  851. * @param {Integer} i Current index in the 708 packet
  852. * @param {Service} service The service object to be affected
  853. * @return {Integer} New index after parsing
  854. */
  855. Cea708Stream.prototype.deleteWindows = function (i, service) {
  856. var packetData = this.current708Packet.data;
  857. var b = packetData[++i];
  858. var pts = this.getPts(i);
  859. this.flushDisplayed(pts, service);
  860. for (var winId = 0; winId < 8; winId++) {
  861. if (b & 0x01 << winId) {
  862. service.windows[winId].reset();
  863. }
  864. }
  865. return i;
  866. };
  867. /**
  868. * Parse and execute the SPA command.
  869. *
  870. * Set pen attributes of the current window.
  871. *
  872. * @param {Integer} i Current index in the 708 packet
  873. * @param {Service} service The service object to be affected
  874. * @return {Integer} New index after parsing
  875. */
  876. Cea708Stream.prototype.setPenAttributes = function (i, service) {
  877. var packetData = this.current708Packet.data;
  878. var b = packetData[i];
  879. var penAttr = service.currentWindow.penAttr;
  880. b = packetData[++i];
  881. penAttr.textTag = (b & 0xf0) >> 4; // tt
  882. penAttr.offset = (b & 0x0c) >> 2; // o
  883. penAttr.penSize = b & 0x03; // s
  884. b = packetData[++i];
  885. penAttr.italics = (b & 0x80) >> 7; // i
  886. penAttr.underline = (b & 0x40) >> 6; // u
  887. penAttr.edgeType = (b & 0x38) >> 3; // et
  888. penAttr.fontStyle = b & 0x07; // fs
  889. return i;
  890. };
  891. /**
  892. * Parse and execute the SPC command.
  893. *
  894. * Set pen color of the current window.
  895. *
  896. * @param {Integer} i Current index in the 708 packet
  897. * @param {Service} service The service object to be affected
  898. * @return {Integer} New index after parsing
  899. */
  900. Cea708Stream.prototype.setPenColor = function (i, service) {
  901. var packetData = this.current708Packet.data;
  902. var b = packetData[i];
  903. var penColor = service.currentWindow.penColor;
  904. b = packetData[++i];
  905. penColor.fgOpacity = (b & 0xc0) >> 6; // fo
  906. penColor.fgRed = (b & 0x30) >> 4; // fr
  907. penColor.fgGreen = (b & 0x0c) >> 2; // fg
  908. penColor.fgBlue = b & 0x03; // fb
  909. b = packetData[++i];
  910. penColor.bgOpacity = (b & 0xc0) >> 6; // bo
  911. penColor.bgRed = (b & 0x30) >> 4; // br
  912. penColor.bgGreen = (b & 0x0c) >> 2; // bg
  913. penColor.bgBlue = b & 0x03; // bb
  914. b = packetData[++i];
  915. penColor.edgeRed = (b & 0x30) >> 4; // er
  916. penColor.edgeGreen = (b & 0x0c) >> 2; // eg
  917. penColor.edgeBlue = b & 0x03; // eb
  918. return i;
  919. };
  920. /**
  921. * Parse and execute the SPL command.
  922. *
  923. * Set pen location of the current window.
  924. *
  925. * @param {Integer} i Current index in the 708 packet
  926. * @param {Service} service The service object to be affected
  927. * @return {Integer} New index after parsing
  928. */
  929. Cea708Stream.prototype.setPenLocation = function (i, service) {
  930. var packetData = this.current708Packet.data;
  931. var b = packetData[i];
  932. var penLoc = service.currentWindow.penLoc; // Positioning isn't really supported at the moment, so this essentially just inserts a linebreak
  933. service.currentWindow.pendingNewLine = true;
  934. b = packetData[++i];
  935. penLoc.row = b & 0x0f; // r
  936. b = packetData[++i];
  937. penLoc.column = b & 0x3f; // c
  938. return i;
  939. };
  940. /**
  941. * Execute the RST command.
  942. *
  943. * Reset service to a clean slate. Re-initialize.
  944. *
  945. * @param {Integer} i Current index in the 708 packet
  946. * @param {Service} service The service object to be affected
  947. * @return {Service} Re-initialized service
  948. */
  949. Cea708Stream.prototype.reset = function (i, service) {
  950. var pts = this.getPts(i);
  951. this.flushDisplayed(pts, service);
  952. return this.initService(service.serviceNum, i);
  953. }; // This hash maps non-ASCII, special, and extended character codes to their
  954. // proper Unicode equivalent. The first keys that are only a single byte
  955. // are the non-standard ASCII characters, which simply map the CEA608 byte
  956. // to the standard ASCII/Unicode. The two-byte keys that follow are the CEA608
  957. // character codes, but have their MSB bitmasked with 0x03 so that a lookup
  958. // can be performed regardless of the field and data channel on which the
  959. // character code was received.
  960. var CHARACTER_TRANSLATION = {
  961. 0x2a: 0xe1,
  962. // á
  963. 0x5c: 0xe9,
  964. // é
  965. 0x5e: 0xed,
  966. // í
  967. 0x5f: 0xf3,
  968. // ó
  969. 0x60: 0xfa,
  970. // ú
  971. 0x7b: 0xe7,
  972. // ç
  973. 0x7c: 0xf7,
  974. // ÷
  975. 0x7d: 0xd1,
  976. // Ñ
  977. 0x7e: 0xf1,
  978. // ñ
  979. 0x7f: 0x2588,
  980. // █
  981. 0x0130: 0xae,
  982. // ®
  983. 0x0131: 0xb0,
  984. // °
  985. 0x0132: 0xbd,
  986. // ½
  987. 0x0133: 0xbf,
  988. // ¿
  989. 0x0134: 0x2122,
  990. // ™
  991. 0x0135: 0xa2,
  992. // ¢
  993. 0x0136: 0xa3,
  994. // £
  995. 0x0137: 0x266a,
  996. // ♪
  997. 0x0138: 0xe0,
  998. // à
  999. 0x0139: 0xa0,
  1000. //
  1001. 0x013a: 0xe8,
  1002. // è
  1003. 0x013b: 0xe2,
  1004. // â
  1005. 0x013c: 0xea,
  1006. // ê
  1007. 0x013d: 0xee,
  1008. // î
  1009. 0x013e: 0xf4,
  1010. // ô
  1011. 0x013f: 0xfb,
  1012. // û
  1013. 0x0220: 0xc1,
  1014. // Á
  1015. 0x0221: 0xc9,
  1016. // É
  1017. 0x0222: 0xd3,
  1018. // Ó
  1019. 0x0223: 0xda,
  1020. // Ú
  1021. 0x0224: 0xdc,
  1022. // Ü
  1023. 0x0225: 0xfc,
  1024. // ü
  1025. 0x0226: 0x2018,
  1026. // ‘
  1027. 0x0227: 0xa1,
  1028. // ¡
  1029. 0x0228: 0x2a,
  1030. // *
  1031. 0x0229: 0x27,
  1032. // '
  1033. 0x022a: 0x2014,
  1034. // —
  1035. 0x022b: 0xa9,
  1036. // ©
  1037. 0x022c: 0x2120,
  1038. // ℠
  1039. 0x022d: 0x2022,
  1040. // •
  1041. 0x022e: 0x201c,
  1042. // “
  1043. 0x022f: 0x201d,
  1044. // ”
  1045. 0x0230: 0xc0,
  1046. // À
  1047. 0x0231: 0xc2,
  1048. // Â
  1049. 0x0232: 0xc7,
  1050. // Ç
  1051. 0x0233: 0xc8,
  1052. // È
  1053. 0x0234: 0xca,
  1054. // Ê
  1055. 0x0235: 0xcb,
  1056. // Ë
  1057. 0x0236: 0xeb,
  1058. // ë
  1059. 0x0237: 0xce,
  1060. // Î
  1061. 0x0238: 0xcf,
  1062. // Ï
  1063. 0x0239: 0xef,
  1064. // ï
  1065. 0x023a: 0xd4,
  1066. // Ô
  1067. 0x023b: 0xd9,
  1068. // Ù
  1069. 0x023c: 0xf9,
  1070. // ù
  1071. 0x023d: 0xdb,
  1072. // Û
  1073. 0x023e: 0xab,
  1074. // «
  1075. 0x023f: 0xbb,
  1076. // »
  1077. 0x0320: 0xc3,
  1078. // Ã
  1079. 0x0321: 0xe3,
  1080. // ã
  1081. 0x0322: 0xcd,
  1082. // Í
  1083. 0x0323: 0xcc,
  1084. // Ì
  1085. 0x0324: 0xec,
  1086. // ì
  1087. 0x0325: 0xd2,
  1088. // Ò
  1089. 0x0326: 0xf2,
  1090. // ò
  1091. 0x0327: 0xd5,
  1092. // Õ
  1093. 0x0328: 0xf5,
  1094. // õ
  1095. 0x0329: 0x7b,
  1096. // {
  1097. 0x032a: 0x7d,
  1098. // }
  1099. 0x032b: 0x5c,
  1100. // \
  1101. 0x032c: 0x5e,
  1102. // ^
  1103. 0x032d: 0x5f,
  1104. // _
  1105. 0x032e: 0x7c,
  1106. // |
  1107. 0x032f: 0x7e,
  1108. // ~
  1109. 0x0330: 0xc4,
  1110. // Ä
  1111. 0x0331: 0xe4,
  1112. // ä
  1113. 0x0332: 0xd6,
  1114. // Ö
  1115. 0x0333: 0xf6,
  1116. // ö
  1117. 0x0334: 0xdf,
  1118. // ß
  1119. 0x0335: 0xa5,
  1120. // ¥
  1121. 0x0336: 0xa4,
  1122. // ¤
  1123. 0x0337: 0x2502,
  1124. // │
  1125. 0x0338: 0xc5,
  1126. // Å
  1127. 0x0339: 0xe5,
  1128. // å
  1129. 0x033a: 0xd8,
  1130. // Ø
  1131. 0x033b: 0xf8,
  1132. // ø
  1133. 0x033c: 0x250c,
  1134. // ┌
  1135. 0x033d: 0x2510,
  1136. // ┐
  1137. 0x033e: 0x2514,
  1138. // └
  1139. 0x033f: 0x2518 // ┘
  1140. };
  1141. var getCharFromCode = function getCharFromCode(code) {
  1142. if (code === null) {
  1143. return '';
  1144. }
  1145. code = CHARACTER_TRANSLATION[code] || code;
  1146. return String.fromCharCode(code);
  1147. }; // the index of the last row in a CEA-608 display buffer
  1148. var BOTTOM_ROW = 14; // This array is used for mapping PACs -> row #, since there's no way of
  1149. // getting it through bit logic.
  1150. var ROWS = [0x1100, 0x1120, 0x1200, 0x1220, 0x1500, 0x1520, 0x1600, 0x1620, 0x1700, 0x1720, 0x1000, 0x1300, 0x1320, 0x1400, 0x1420]; // CEA-608 captions are rendered onto a 34x15 matrix of character
  1151. // cells. The "bottom" row is the last element in the outer array.
  1152. var createDisplayBuffer = function createDisplayBuffer() {
  1153. var result = [],
  1154. i = BOTTOM_ROW + 1;
  1155. while (i--) {
  1156. result.push('');
  1157. }
  1158. return result;
  1159. };
  1160. var Cea608Stream = function Cea608Stream(field, dataChannel) {
  1161. Cea608Stream.prototype.init.call(this);
  1162. this.field_ = field || 0;
  1163. this.dataChannel_ = dataChannel || 0;
  1164. this.name_ = 'CC' + ((this.field_ << 1 | this.dataChannel_) + 1);
  1165. this.setConstants();
  1166. this.reset();
  1167. this.push = function (packet) {
  1168. var data, swap, char0, char1, text; // remove the parity bits
  1169. data = packet.ccData & 0x7f7f; // ignore duplicate control codes; the spec demands they're sent twice
  1170. if (data === this.lastControlCode_) {
  1171. this.lastControlCode_ = null;
  1172. return;
  1173. } // Store control codes
  1174. if ((data & 0xf000) === 0x1000) {
  1175. this.lastControlCode_ = data;
  1176. } else if (data !== this.PADDING_) {
  1177. this.lastControlCode_ = null;
  1178. }
  1179. char0 = data >>> 8;
  1180. char1 = data & 0xff;
  1181. if (data === this.PADDING_) {
  1182. return;
  1183. } else if (data === this.RESUME_CAPTION_LOADING_) {
  1184. this.mode_ = 'popOn';
  1185. } else if (data === this.END_OF_CAPTION_) {
  1186. // If an EOC is received while in paint-on mode, the displayed caption
  1187. // text should be swapped to non-displayed memory as if it was a pop-on
  1188. // caption. Because of that, we should explicitly switch back to pop-on
  1189. // mode
  1190. this.mode_ = 'popOn';
  1191. this.clearFormatting(packet.pts); // if a caption was being displayed, it's gone now
  1192. this.flushDisplayed(packet.pts); // flip memory
  1193. swap = this.displayed_;
  1194. this.displayed_ = this.nonDisplayed_;
  1195. this.nonDisplayed_ = swap; // start measuring the time to display the caption
  1196. this.startPts_ = packet.pts;
  1197. } else if (data === this.ROLL_UP_2_ROWS_) {
  1198. this.rollUpRows_ = 2;
  1199. this.setRollUp(packet.pts);
  1200. } else if (data === this.ROLL_UP_3_ROWS_) {
  1201. this.rollUpRows_ = 3;
  1202. this.setRollUp(packet.pts);
  1203. } else if (data === this.ROLL_UP_4_ROWS_) {
  1204. this.rollUpRows_ = 4;
  1205. this.setRollUp(packet.pts);
  1206. } else if (data === this.CARRIAGE_RETURN_) {
  1207. this.clearFormatting(packet.pts);
  1208. this.flushDisplayed(packet.pts);
  1209. this.shiftRowsUp_();
  1210. this.startPts_ = packet.pts;
  1211. } else if (data === this.BACKSPACE_) {
  1212. if (this.mode_ === 'popOn') {
  1213. this.nonDisplayed_[this.row_] = this.nonDisplayed_[this.row_].slice(0, -1);
  1214. } else {
  1215. this.displayed_[this.row_] = this.displayed_[this.row_].slice(0, -1);
  1216. }
  1217. } else if (data === this.ERASE_DISPLAYED_MEMORY_) {
  1218. this.flushDisplayed(packet.pts);
  1219. this.displayed_ = createDisplayBuffer();
  1220. } else if (data === this.ERASE_NON_DISPLAYED_MEMORY_) {
  1221. this.nonDisplayed_ = createDisplayBuffer();
  1222. } else if (data === this.RESUME_DIRECT_CAPTIONING_) {
  1223. if (this.mode_ !== 'paintOn') {
  1224. // NOTE: This should be removed when proper caption positioning is
  1225. // implemented
  1226. this.flushDisplayed(packet.pts);
  1227. this.displayed_ = createDisplayBuffer();
  1228. }
  1229. this.mode_ = 'paintOn';
  1230. this.startPts_ = packet.pts; // Append special characters to caption text
  1231. } else if (this.isSpecialCharacter(char0, char1)) {
  1232. // Bitmask char0 so that we can apply character transformations
  1233. // regardless of field and data channel.
  1234. // Then byte-shift to the left and OR with char1 so we can pass the
  1235. // entire character code to `getCharFromCode`.
  1236. char0 = (char0 & 0x03) << 8;
  1237. text = getCharFromCode(char0 | char1);
  1238. this[this.mode_](packet.pts, text);
  1239. this.column_++; // Append extended characters to caption text
  1240. } else if (this.isExtCharacter(char0, char1)) {
  1241. // Extended characters always follow their "non-extended" equivalents.
  1242. // IE if a "è" is desired, you'll always receive "eè"; non-compliant
  1243. // decoders are supposed to drop the "è", while compliant decoders
  1244. // backspace the "e" and insert "è".
  1245. // Delete the previous character
  1246. if (this.mode_ === 'popOn') {
  1247. this.nonDisplayed_[this.row_] = this.nonDisplayed_[this.row_].slice(0, -1);
  1248. } else {
  1249. this.displayed_[this.row_] = this.displayed_[this.row_].slice(0, -1);
  1250. } // Bitmask char0 so that we can apply character transformations
  1251. // regardless of field and data channel.
  1252. // Then byte-shift to the left and OR with char1 so we can pass the
  1253. // entire character code to `getCharFromCode`.
  1254. char0 = (char0 & 0x03) << 8;
  1255. text = getCharFromCode(char0 | char1);
  1256. this[this.mode_](packet.pts, text);
  1257. this.column_++; // Process mid-row codes
  1258. } else if (this.isMidRowCode(char0, char1)) {
  1259. // Attributes are not additive, so clear all formatting
  1260. this.clearFormatting(packet.pts); // According to the standard, mid-row codes
  1261. // should be replaced with spaces, so add one now
  1262. this[this.mode_](packet.pts, ' ');
  1263. this.column_++;
  1264. if ((char1 & 0xe) === 0xe) {
  1265. this.addFormatting(packet.pts, ['i']);
  1266. }
  1267. if ((char1 & 0x1) === 0x1) {
  1268. this.addFormatting(packet.pts, ['u']);
  1269. } // Detect offset control codes and adjust cursor
  1270. } else if (this.isOffsetControlCode(char0, char1)) {
  1271. // Cursor position is set by indent PAC (see below) in 4-column
  1272. // increments, with an additional offset code of 1-3 to reach any
  1273. // of the 32 columns specified by CEA-608. So all we need to do
  1274. // here is increment the column cursor by the given offset.
  1275. this.column_ += char1 & 0x03; // Detect PACs (Preamble Address Codes)
  1276. } else if (this.isPAC(char0, char1)) {
  1277. // There's no logic for PAC -> row mapping, so we have to just
  1278. // find the row code in an array and use its index :(
  1279. var row = ROWS.indexOf(data & 0x1f20); // Configure the caption window if we're in roll-up mode
  1280. if (this.mode_ === 'rollUp') {
  1281. // This implies that the base row is incorrectly set.
  1282. // As per the recommendation in CEA-608(Base Row Implementation), defer to the number
  1283. // of roll-up rows set.
  1284. if (row - this.rollUpRows_ + 1 < 0) {
  1285. row = this.rollUpRows_ - 1;
  1286. }
  1287. this.setRollUp(packet.pts, row);
  1288. }
  1289. if (row !== this.row_) {
  1290. // formatting is only persistent for current row
  1291. this.clearFormatting(packet.pts);
  1292. this.row_ = row;
  1293. } // All PACs can apply underline, so detect and apply
  1294. // (All odd-numbered second bytes set underline)
  1295. if (char1 & 0x1 && this.formatting_.indexOf('u') === -1) {
  1296. this.addFormatting(packet.pts, ['u']);
  1297. }
  1298. if ((data & 0x10) === 0x10) {
  1299. // We've got an indent level code. Each successive even number
  1300. // increments the column cursor by 4, so we can get the desired
  1301. // column position by bit-shifting to the right (to get n/2)
  1302. // and multiplying by 4.
  1303. this.column_ = ((data & 0xe) >> 1) * 4;
  1304. }
  1305. if (this.isColorPAC(char1)) {
  1306. // it's a color code, though we only support white, which
  1307. // can be either normal or italicized. white italics can be
  1308. // either 0x4e or 0x6e depending on the row, so we just
  1309. // bitwise-and with 0xe to see if italics should be turned on
  1310. if ((char1 & 0xe) === 0xe) {
  1311. this.addFormatting(packet.pts, ['i']);
  1312. }
  1313. } // We have a normal character in char0, and possibly one in char1
  1314. } else if (this.isNormalChar(char0)) {
  1315. if (char1 === 0x00) {
  1316. char1 = null;
  1317. }
  1318. text = getCharFromCode(char0);
  1319. text += getCharFromCode(char1);
  1320. this[this.mode_](packet.pts, text);
  1321. this.column_ += text.length;
  1322. } // finish data processing
  1323. };
  1324. };
  1325. Cea608Stream.prototype = new Stream(); // Trigger a cue point that captures the current state of the
  1326. // display buffer
  1327. Cea608Stream.prototype.flushDisplayed = function (pts) {
  1328. var content = this.displayed_ // remove spaces from the start and end of the string
  1329. .map(function (row, index) {
  1330. try {
  1331. return row.trim();
  1332. } catch (e) {
  1333. // Ordinarily, this shouldn't happen. However, caption
  1334. // parsing errors should not throw exceptions and
  1335. // break playback.
  1336. this.trigger('log', {
  1337. level: 'warn',
  1338. message: 'Skipping a malformed 608 caption at index ' + index + '.'
  1339. });
  1340. return '';
  1341. }
  1342. }, this) // combine all text rows to display in one cue
  1343. .join('\n') // and remove blank rows from the start and end, but not the middle
  1344. .replace(/^\n+|\n+$/g, '');
  1345. if (content.length) {
  1346. this.trigger('data', {
  1347. startPts: this.startPts_,
  1348. endPts: pts,
  1349. text: content,
  1350. stream: this.name_
  1351. });
  1352. }
  1353. };
  1354. /**
  1355. * Zero out the data, used for startup and on seek
  1356. */
  1357. Cea608Stream.prototype.reset = function () {
  1358. this.mode_ = 'popOn'; // When in roll-up mode, the index of the last row that will
  1359. // actually display captions. If a caption is shifted to a row
  1360. // with a lower index than this, it is cleared from the display
  1361. // buffer
  1362. this.topRow_ = 0;
  1363. this.startPts_ = 0;
  1364. this.displayed_ = createDisplayBuffer();
  1365. this.nonDisplayed_ = createDisplayBuffer();
  1366. this.lastControlCode_ = null; // Track row and column for proper line-breaking and spacing
  1367. this.column_ = 0;
  1368. this.row_ = BOTTOM_ROW;
  1369. this.rollUpRows_ = 2; // This variable holds currently-applied formatting
  1370. this.formatting_ = [];
  1371. };
  1372. /**
  1373. * Sets up control code and related constants for this instance
  1374. */
  1375. Cea608Stream.prototype.setConstants = function () {
  1376. // The following attributes have these uses:
  1377. // ext_ : char0 for mid-row codes, and the base for extended
  1378. // chars (ext_+0, ext_+1, and ext_+2 are char0s for
  1379. // extended codes)
  1380. // control_: char0 for control codes, except byte-shifted to the
  1381. // left so that we can do this.control_ | CONTROL_CODE
  1382. // offset_: char0 for tab offset codes
  1383. //
  1384. // It's also worth noting that control codes, and _only_ control codes,
  1385. // differ between field 1 and field2. Field 2 control codes are always
  1386. // their field 1 value plus 1. That's why there's the "| field" on the
  1387. // control value.
  1388. if (this.dataChannel_ === 0) {
  1389. this.BASE_ = 0x10;
  1390. this.EXT_ = 0x11;
  1391. this.CONTROL_ = (0x14 | this.field_) << 8;
  1392. this.OFFSET_ = 0x17;
  1393. } else if (this.dataChannel_ === 1) {
  1394. this.BASE_ = 0x18;
  1395. this.EXT_ = 0x19;
  1396. this.CONTROL_ = (0x1c | this.field_) << 8;
  1397. this.OFFSET_ = 0x1f;
  1398. } // Constants for the LSByte command codes recognized by Cea608Stream. This
  1399. // list is not exhaustive. For a more comprehensive listing and semantics see
  1400. // http://www.gpo.gov/fdsys/pkg/CFR-2010-title47-vol1/pdf/CFR-2010-title47-vol1-sec15-119.pdf
  1401. // Padding
  1402. this.PADDING_ = 0x0000; // Pop-on Mode
  1403. this.RESUME_CAPTION_LOADING_ = this.CONTROL_ | 0x20;
  1404. this.END_OF_CAPTION_ = this.CONTROL_ | 0x2f; // Roll-up Mode
  1405. this.ROLL_UP_2_ROWS_ = this.CONTROL_ | 0x25;
  1406. this.ROLL_UP_3_ROWS_ = this.CONTROL_ | 0x26;
  1407. this.ROLL_UP_4_ROWS_ = this.CONTROL_ | 0x27;
  1408. this.CARRIAGE_RETURN_ = this.CONTROL_ | 0x2d; // paint-on mode
  1409. this.RESUME_DIRECT_CAPTIONING_ = this.CONTROL_ | 0x29; // Erasure
  1410. this.BACKSPACE_ = this.CONTROL_ | 0x21;
  1411. this.ERASE_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2c;
  1412. this.ERASE_NON_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2e;
  1413. };
  1414. /**
  1415. * Detects if the 2-byte packet data is a special character
  1416. *
  1417. * Special characters have a second byte in the range 0x30 to 0x3f,
  1418. * with the first byte being 0x11 (for data channel 1) or 0x19 (for
  1419. * data channel 2).
  1420. *
  1421. * @param {Integer} char0 The first byte
  1422. * @param {Integer} char1 The second byte
  1423. * @return {Boolean} Whether the 2 bytes are an special character
  1424. */
  1425. Cea608Stream.prototype.isSpecialCharacter = function (char0, char1) {
  1426. return char0 === this.EXT_ && char1 >= 0x30 && char1 <= 0x3f;
  1427. };
  1428. /**
  1429. * Detects if the 2-byte packet data is an extended character
  1430. *
  1431. * Extended characters have a second byte in the range 0x20 to 0x3f,
  1432. * with the first byte being 0x12 or 0x13 (for data channel 1) or
  1433. * 0x1a or 0x1b (for data channel 2).
  1434. *
  1435. * @param {Integer} char0 The first byte
  1436. * @param {Integer} char1 The second byte
  1437. * @return {Boolean} Whether the 2 bytes are an extended character
  1438. */
  1439. Cea608Stream.prototype.isExtCharacter = function (char0, char1) {
  1440. return (char0 === this.EXT_ + 1 || char0 === this.EXT_ + 2) && char1 >= 0x20 && char1 <= 0x3f;
  1441. };
  1442. /**
  1443. * Detects if the 2-byte packet is a mid-row code
  1444. *
  1445. * Mid-row codes have a second byte in the range 0x20 to 0x2f, with
  1446. * the first byte being 0x11 (for data channel 1) or 0x19 (for data
  1447. * channel 2).
  1448. *
  1449. * @param {Integer} char0 The first byte
  1450. * @param {Integer} char1 The second byte
  1451. * @return {Boolean} Whether the 2 bytes are a mid-row code
  1452. */
  1453. Cea608Stream.prototype.isMidRowCode = function (char0, char1) {
  1454. return char0 === this.EXT_ && char1 >= 0x20 && char1 <= 0x2f;
  1455. };
  1456. /**
  1457. * Detects if the 2-byte packet is an offset control code
  1458. *
  1459. * Offset control codes have a second byte in the range 0x21 to 0x23,
  1460. * with the first byte being 0x17 (for data channel 1) or 0x1f (for
  1461. * data channel 2).
  1462. *
  1463. * @param {Integer} char0 The first byte
  1464. * @param {Integer} char1 The second byte
  1465. * @return {Boolean} Whether the 2 bytes are an offset control code
  1466. */
  1467. Cea608Stream.prototype.isOffsetControlCode = function (char0, char1) {
  1468. return char0 === this.OFFSET_ && char1 >= 0x21 && char1 <= 0x23;
  1469. };
  1470. /**
  1471. * Detects if the 2-byte packet is a Preamble Address Code
  1472. *
  1473. * PACs have a first byte in the range 0x10 to 0x17 (for data channel 1)
  1474. * or 0x18 to 0x1f (for data channel 2), with the second byte in the
  1475. * range 0x40 to 0x7f.
  1476. *
  1477. * @param {Integer} char0 The first byte
  1478. * @param {Integer} char1 The second byte
  1479. * @return {Boolean} Whether the 2 bytes are a PAC
  1480. */
  1481. Cea608Stream.prototype.isPAC = function (char0, char1) {
  1482. return char0 >= this.BASE_ && char0 < this.BASE_ + 8 && char1 >= 0x40 && char1 <= 0x7f;
  1483. };
  1484. /**
  1485. * Detects if a packet's second byte is in the range of a PAC color code
  1486. *
  1487. * PAC color codes have the second byte be in the range 0x40 to 0x4f, or
  1488. * 0x60 to 0x6f.
  1489. *
  1490. * @param {Integer} char1 The second byte
  1491. * @return {Boolean} Whether the byte is a color PAC
  1492. */
  1493. Cea608Stream.prototype.isColorPAC = function (char1) {
  1494. return char1 >= 0x40 && char1 <= 0x4f || char1 >= 0x60 && char1 <= 0x7f;
  1495. };
  1496. /**
  1497. * Detects if a single byte is in the range of a normal character
  1498. *
  1499. * Normal text bytes are in the range 0x20 to 0x7f.
  1500. *
  1501. * @param {Integer} char The byte
  1502. * @return {Boolean} Whether the byte is a normal character
  1503. */
  1504. Cea608Stream.prototype.isNormalChar = function (char) {
  1505. return char >= 0x20 && char <= 0x7f;
  1506. };
  1507. /**
  1508. * Configures roll-up
  1509. *
  1510. * @param {Integer} pts Current PTS
  1511. * @param {Integer} newBaseRow Used by PACs to slide the current window to
  1512. * a new position
  1513. */
  1514. Cea608Stream.prototype.setRollUp = function (pts, newBaseRow) {
  1515. // Reset the base row to the bottom row when switching modes
  1516. if (this.mode_ !== 'rollUp') {
  1517. this.row_ = BOTTOM_ROW;
  1518. this.mode_ = 'rollUp'; // Spec says to wipe memories when switching to roll-up
  1519. this.flushDisplayed(pts);
  1520. this.nonDisplayed_ = createDisplayBuffer();
  1521. this.displayed_ = createDisplayBuffer();
  1522. }
  1523. if (newBaseRow !== undefined && newBaseRow !== this.row_) {
  1524. // move currently displayed captions (up or down) to the new base row
  1525. for (var i = 0; i < this.rollUpRows_; i++) {
  1526. this.displayed_[newBaseRow - i] = this.displayed_[this.row_ - i];
  1527. this.displayed_[this.row_ - i] = '';
  1528. }
  1529. }
  1530. if (newBaseRow === undefined) {
  1531. newBaseRow = this.row_;
  1532. }
  1533. this.topRow_ = newBaseRow - this.rollUpRows_ + 1;
  1534. }; // Adds the opening HTML tag for the passed character to the caption text,
  1535. // and keeps track of it for later closing
  1536. Cea608Stream.prototype.addFormatting = function (pts, format) {
  1537. this.formatting_ = this.formatting_.concat(format);
  1538. var text = format.reduce(function (text, format) {
  1539. return text + '<' + format + '>';
  1540. }, '');
  1541. this[this.mode_](pts, text);
  1542. }; // Adds HTML closing tags for current formatting to caption text and
  1543. // clears remembered formatting
  1544. Cea608Stream.prototype.clearFormatting = function (pts) {
  1545. if (!this.formatting_.length) {
  1546. return;
  1547. }
  1548. var text = this.formatting_.reverse().reduce(function (text, format) {
  1549. return text + '</' + format + '>';
  1550. }, '');
  1551. this.formatting_ = [];
  1552. this[this.mode_](pts, text);
  1553. }; // Mode Implementations
  1554. Cea608Stream.prototype.popOn = function (pts, text) {
  1555. var baseRow = this.nonDisplayed_[this.row_]; // buffer characters
  1556. baseRow += text;
  1557. this.nonDisplayed_[this.row_] = baseRow;
  1558. };
  1559. Cea608Stream.prototype.rollUp = function (pts, text) {
  1560. var baseRow = this.displayed_[this.row_];
  1561. baseRow += text;
  1562. this.displayed_[this.row_] = baseRow;
  1563. };
  1564. Cea608Stream.prototype.shiftRowsUp_ = function () {
  1565. var i; // clear out inactive rows
  1566. for (i = 0; i < this.topRow_; i++) {
  1567. this.displayed_[i] = '';
  1568. }
  1569. for (i = this.row_ + 1; i < BOTTOM_ROW + 1; i++) {
  1570. this.displayed_[i] = '';
  1571. } // shift displayed rows up
  1572. for (i = this.topRow_; i < this.row_; i++) {
  1573. this.displayed_[i] = this.displayed_[i + 1];
  1574. } // clear out the bottom row
  1575. this.displayed_[this.row_] = '';
  1576. };
  1577. Cea608Stream.prototype.paintOn = function (pts, text) {
  1578. var baseRow = this.displayed_[this.row_];
  1579. baseRow += text;
  1580. this.displayed_[this.row_] = baseRow;
  1581. }; // exports
  1582. module.exports = {
  1583. CaptionStream: CaptionStream,
  1584. Cea608Stream: Cea608Stream,
  1585. Cea708Stream: Cea708Stream
  1586. };