caption-stream.js 52 KB

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