caption-stream.js 54 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814
  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. // Converts an array of bytes to a unicode hex string.
  602. function toHexString(byteArray) {
  603. return byteArray.map((byte) => {
  604. return ('0' + (byte & 0xFF).toString(16)).slice(-2);
  605. }).join('');
  606. };
  607. if (isMultiByte) {
  608. charCodeArray = [currentByte, nextByte];
  609. i++;
  610. } else {
  611. charCodeArray = [currentByte];
  612. }
  613. // Use the TextDecoder if one was created for this service
  614. if (service.textDecoder_ && !isExtended) {
  615. char = service.textDecoder_.decode(new Uint8Array(charCodeArray));
  616. } else {
  617. // We assume any multi-byte char without a decoder is unicode.
  618. if (isMultiByte) {
  619. const unicode = toHexString(charCodeArray);
  620. // Takes a unicode hex string and creates a single character.
  621. char = String.fromCharCode(parseInt(unicode, 16));
  622. } else {
  623. char = get708CharFromCode(extended | currentByte);
  624. }
  625. }
  626. if (win.pendingNewLine && !win.isEmpty()) {
  627. win.newLine(this.getPts(i));
  628. }
  629. win.pendingNewLine = false;
  630. win.addText(char);
  631. return i;
  632. };
  633. /**
  634. * Handle decoding of multibyte character
  635. *
  636. * @param {Integer} i Current index in the 708 packet
  637. * @param {Service} service The service object to be affected
  638. * @return {Integer} New index after parsing
  639. */
  640. Cea708Stream.prototype.multiByteCharacter = function (i, service) {
  641. var packetData = this.current708Packet.data;
  642. var firstByte = packetData[i + 1];
  643. var secondByte = packetData[i + 2];
  644. if (within708TextBlock(firstByte) && within708TextBlock(secondByte)) {
  645. i = this.handleText(++i, service, {isMultiByte: true});
  646. } else {
  647. // Unknown command
  648. }
  649. return i;
  650. };
  651. /**
  652. * Parse and execute the CW# command.
  653. *
  654. * Set the current window.
  655. *
  656. * @param {Integer} i Current index in the 708 packet
  657. * @param {Service} service The service object to be affected
  658. * @return {Integer} New index after parsing
  659. */
  660. Cea708Stream.prototype.setCurrentWindow = function(i, service) {
  661. var packetData = this.current708Packet.data;
  662. var b = packetData[i];
  663. var windowNum = b & 0x07;
  664. service.setCurrentWindow(windowNum);
  665. return i;
  666. };
  667. /**
  668. * Parse and execute the DF# command.
  669. *
  670. * Define a window and set it as the current window.
  671. *
  672. * @param {Integer} i Current index in the 708 packet
  673. * @param {Service} service The service object to be affected
  674. * @return {Integer} New index after parsing
  675. */
  676. Cea708Stream.prototype.defineWindow = function(i, service) {
  677. var packetData = this.current708Packet.data;
  678. var b = packetData[i];
  679. var windowNum = b & 0x07;
  680. service.setCurrentWindow(windowNum);
  681. var win = service.currentWindow;
  682. b = packetData[++i];
  683. win.visible = (b & 0x20) >> 5; // v
  684. win.rowLock = (b & 0x10) >> 4; // rl
  685. win.columnLock = (b & 0x08) >> 3; // cl
  686. win.priority = b & 0x07; // p
  687. b = packetData[++i];
  688. win.relativePositioning = (b & 0x80) >> 7; // rp
  689. win.anchorVertical = b & 0x7f; // av
  690. b = packetData[++i];
  691. win.anchorHorizontal = b; // ah
  692. b = packetData[++i];
  693. win.anchorPoint = (b & 0xf0) >> 4; // ap
  694. win.rowCount = b & 0x0f; // rc
  695. b = packetData[++i];
  696. win.columnCount = b & 0x3f; // cc
  697. b = packetData[++i];
  698. win.windowStyle = (b & 0x38) >> 3; // ws
  699. win.penStyle = b & 0x07; // ps
  700. // The spec says there are (rowCount+1) "virtual rows"
  701. win.virtualRowCount = win.rowCount + 1;
  702. return i;
  703. };
  704. /**
  705. * Parse and execute the SWA command.
  706. *
  707. * Set attributes of the current window.
  708. *
  709. * @param {Integer} i Current index in the 708 packet
  710. * @param {Service} service The service object to be affected
  711. * @return {Integer} New index after parsing
  712. */
  713. Cea708Stream.prototype.setWindowAttributes = function(i, service) {
  714. var packetData = this.current708Packet.data;
  715. var b = packetData[i];
  716. var winAttr = service.currentWindow.winAttr;
  717. b = packetData[++i];
  718. winAttr.fillOpacity = (b & 0xc0) >> 6; // fo
  719. winAttr.fillRed = (b & 0x30) >> 4; // fr
  720. winAttr.fillGreen = (b & 0x0c) >> 2; // fg
  721. winAttr.fillBlue = b & 0x03; // fb
  722. b = packetData[++i];
  723. winAttr.borderType = (b & 0xc0) >> 6; // bt
  724. winAttr.borderRed = (b & 0x30) >> 4; // br
  725. winAttr.borderGreen = (b & 0x0c) >> 2; // bg
  726. winAttr.borderBlue = b & 0x03; // bb
  727. b = packetData[++i];
  728. winAttr.borderType += (b & 0x80) >> 5; // bt
  729. winAttr.wordWrap = (b & 0x40) >> 6; // ww
  730. winAttr.printDirection = (b & 0x30) >> 4; // pd
  731. winAttr.scrollDirection = (b & 0x0c) >> 2; // sd
  732. winAttr.justify = b & 0x03; // j
  733. b = packetData[++i];
  734. winAttr.effectSpeed = (b & 0xf0) >> 4; // es
  735. winAttr.effectDirection = (b & 0x0c) >> 2; // ed
  736. winAttr.displayEffect = b & 0x03; // de
  737. return i;
  738. };
  739. /**
  740. * Gather text from all displayed windows and push a caption to output.
  741. *
  742. * @param {Integer} i Current index in the 708 packet
  743. * @param {Service} service The service object to be affected
  744. */
  745. Cea708Stream.prototype.flushDisplayed = function(pts, service) {
  746. var displayedText = [];
  747. // TODO: Positioning not supported, displaying multiple windows will not necessarily
  748. // display text in the correct order, but sample files so far have not shown any issue.
  749. for (var winId = 0; winId < 8; winId++) {
  750. if (service.windows[winId].visible && !service.windows[winId].isEmpty()) {
  751. displayedText.push(service.windows[winId].getText());
  752. }
  753. }
  754. service.endPts = pts;
  755. service.text = displayedText.join('\n\n');
  756. this.pushCaption(service);
  757. service.startPts = pts;
  758. };
  759. /**
  760. * Push a caption to output if the caption contains text.
  761. *
  762. * @param {Service} service The service object to be affected
  763. */
  764. Cea708Stream.prototype.pushCaption = function(service) {
  765. if (service.text !== '') {
  766. this.trigger('data', {
  767. startPts: service.startPts,
  768. endPts: service.endPts,
  769. text: service.text,
  770. stream: 'cc708_' + service.serviceNum
  771. });
  772. service.text = '';
  773. service.startPts = service.endPts;
  774. }
  775. };
  776. /**
  777. * Parse and execute the DSW command.
  778. *
  779. * Set visible property of windows based on the parsed bitmask.
  780. *
  781. * @param {Integer} i Current index in the 708 packet
  782. * @param {Service} service The service object to be affected
  783. * @return {Integer} New index after parsing
  784. */
  785. Cea708Stream.prototype.displayWindows = function(i, service) {
  786. var packetData = this.current708Packet.data;
  787. var b = packetData[++i];
  788. var pts = this.getPts(i);
  789. this.flushDisplayed(pts, service);
  790. for (var winId = 0; winId < 8; winId++) {
  791. if (b & (0x01 << winId)) {
  792. service.windows[winId].visible = 1;
  793. }
  794. }
  795. return i;
  796. };
  797. /**
  798. * Parse and execute the HDW command.
  799. *
  800. * Set visible property of windows based on the parsed bitmask.
  801. *
  802. * @param {Integer} i Current index in the 708 packet
  803. * @param {Service} service The service object to be affected
  804. * @return {Integer} New index after parsing
  805. */
  806. Cea708Stream.prototype.hideWindows = function(i, service) {
  807. var packetData = this.current708Packet.data;
  808. var b = packetData[++i];
  809. var pts = this.getPts(i);
  810. this.flushDisplayed(pts, service);
  811. for (var winId = 0; winId < 8; winId++) {
  812. if (b & (0x01 << winId)) {
  813. service.windows[winId].visible = 0;
  814. }
  815. }
  816. return i;
  817. };
  818. /**
  819. * Parse and execute the TGW command.
  820. *
  821. * Set visible property of windows based on the parsed bitmask.
  822. *
  823. * @param {Integer} i Current index in the 708 packet
  824. * @param {Service} service The service object to be affected
  825. * @return {Integer} New index after parsing
  826. */
  827. Cea708Stream.prototype.toggleWindows = function(i, service) {
  828. var packetData = this.current708Packet.data;
  829. var b = packetData[++i];
  830. var pts = this.getPts(i);
  831. this.flushDisplayed(pts, service);
  832. for (var winId = 0; winId < 8; winId++) {
  833. if (b & (0x01 << winId)) {
  834. service.windows[winId].visible ^= 1;
  835. }
  836. }
  837. return i;
  838. };
  839. /**
  840. * Parse and execute the CLW command.
  841. *
  842. * Clear text of windows based on the parsed bitmask.
  843. *
  844. * @param {Integer} i Current index in the 708 packet
  845. * @param {Service} service The service object to be affected
  846. * @return {Integer} New index after parsing
  847. */
  848. Cea708Stream.prototype.clearWindows = function(i, service) {
  849. var packetData = this.current708Packet.data;
  850. var b = packetData[++i];
  851. var pts = this.getPts(i);
  852. this.flushDisplayed(pts, service);
  853. for (var winId = 0; winId < 8; winId++) {
  854. if (b & (0x01 << winId)) {
  855. service.windows[winId].clearText();
  856. }
  857. }
  858. return i;
  859. };
  860. /**
  861. * Parse and execute the DLW command.
  862. *
  863. * Re-initialize windows based on the parsed bitmask.
  864. *
  865. * @param {Integer} i Current index in the 708 packet
  866. * @param {Service} service The service object to be affected
  867. * @return {Integer} New index after parsing
  868. */
  869. Cea708Stream.prototype.deleteWindows = function(i, service) {
  870. var packetData = this.current708Packet.data;
  871. var b = packetData[++i];
  872. var pts = this.getPts(i);
  873. this.flushDisplayed(pts, service);
  874. for (var winId = 0; winId < 8; winId++) {
  875. if (b & (0x01 << winId)) {
  876. service.windows[winId].reset();
  877. }
  878. }
  879. return i;
  880. };
  881. /**
  882. * Parse and execute the SPA command.
  883. *
  884. * Set pen attributes of the current window.
  885. *
  886. * @param {Integer} i Current index in the 708 packet
  887. * @param {Service} service The service object to be affected
  888. * @return {Integer} New index after parsing
  889. */
  890. Cea708Stream.prototype.setPenAttributes = function(i, service) {
  891. var packetData = this.current708Packet.data;
  892. var b = packetData[i];
  893. var penAttr = service.currentWindow.penAttr;
  894. b = packetData[++i];
  895. penAttr.textTag = (b & 0xf0) >> 4; // tt
  896. penAttr.offset = (b & 0x0c) >> 2; // o
  897. penAttr.penSize = b & 0x03; // s
  898. b = packetData[++i];
  899. penAttr.italics = (b & 0x80) >> 7; // i
  900. penAttr.underline = (b & 0x40) >> 6; // u
  901. penAttr.edgeType = (b & 0x38) >> 3; // et
  902. penAttr.fontStyle = b & 0x07; // fs
  903. return i;
  904. };
  905. /**
  906. * Parse and execute the SPC command.
  907. *
  908. * Set pen color of the current window.
  909. *
  910. * @param {Integer} i Current index in the 708 packet
  911. * @param {Service} service The service object to be affected
  912. * @return {Integer} New index after parsing
  913. */
  914. Cea708Stream.prototype.setPenColor = function(i, service) {
  915. var packetData = this.current708Packet.data;
  916. var b = packetData[i];
  917. var penColor = service.currentWindow.penColor;
  918. b = packetData[++i];
  919. penColor.fgOpacity = (b & 0xc0) >> 6; // fo
  920. penColor.fgRed = (b & 0x30) >> 4; // fr
  921. penColor.fgGreen = (b & 0x0c) >> 2; // fg
  922. penColor.fgBlue = b & 0x03; // fb
  923. b = packetData[++i];
  924. penColor.bgOpacity = (b & 0xc0) >> 6; // bo
  925. penColor.bgRed = (b & 0x30) >> 4; // br
  926. penColor.bgGreen = (b & 0x0c) >> 2; // bg
  927. penColor.bgBlue = b & 0x03; // bb
  928. b = packetData[++i];
  929. penColor.edgeRed = (b & 0x30) >> 4; // er
  930. penColor.edgeGreen = (b & 0x0c) >> 2; // eg
  931. penColor.edgeBlue = b & 0x03; // eb
  932. return i;
  933. };
  934. /**
  935. * Parse and execute the SPL command.
  936. *
  937. * Set pen location of the current window.
  938. *
  939. * @param {Integer} i Current index in the 708 packet
  940. * @param {Service} service The service object to be affected
  941. * @return {Integer} New index after parsing
  942. */
  943. Cea708Stream.prototype.setPenLocation = function(i, service) {
  944. var packetData = this.current708Packet.data;
  945. var b = packetData[i];
  946. var penLoc = service.currentWindow.penLoc;
  947. // Positioning isn't really supported at the moment, so this essentially just inserts a linebreak
  948. service.currentWindow.pendingNewLine = true;
  949. b = packetData[++i];
  950. penLoc.row = b & 0x0f; // r
  951. b = packetData[++i];
  952. penLoc.column = b & 0x3f; // c
  953. return i;
  954. };
  955. /**
  956. * Execute the RST command.
  957. *
  958. * Reset service to a clean slate. Re-initialize.
  959. *
  960. * @param {Integer} i Current index in the 708 packet
  961. * @param {Service} service The service object to be affected
  962. * @return {Service} Re-initialized service
  963. */
  964. Cea708Stream.prototype.reset = function(i, service) {
  965. var pts = this.getPts(i);
  966. this.flushDisplayed(pts, service);
  967. return this.initService(service.serviceNum, i);
  968. };
  969. // This hash maps non-ASCII, special, and extended character codes to their
  970. // proper Unicode equivalent. The first keys that are only a single byte
  971. // are the non-standard ASCII characters, which simply map the CEA608 byte
  972. // to the standard ASCII/Unicode. The two-byte keys that follow are the CEA608
  973. // character codes, but have their MSB bitmasked with 0x03 so that a lookup
  974. // can be performed regardless of the field and data channel on which the
  975. // character code was received.
  976. var CHARACTER_TRANSLATION = {
  977. 0x2a: 0xe1, // á
  978. 0x5c: 0xe9, // é
  979. 0x5e: 0xed, // í
  980. 0x5f: 0xf3, // ó
  981. 0x60: 0xfa, // ú
  982. 0x7b: 0xe7, // ç
  983. 0x7c: 0xf7, // ÷
  984. 0x7d: 0xd1, // Ñ
  985. 0x7e: 0xf1, // ñ
  986. 0x7f: 0x2588, // █
  987. 0x0130: 0xae, // ®
  988. 0x0131: 0xb0, // °
  989. 0x0132: 0xbd, // ½
  990. 0x0133: 0xbf, // ¿
  991. 0x0134: 0x2122, // ™
  992. 0x0135: 0xa2, // ¢
  993. 0x0136: 0xa3, // £
  994. 0x0137: 0x266a, // ♪
  995. 0x0138: 0xe0, // à
  996. 0x0139: 0xa0, //
  997. 0x013a: 0xe8, // è
  998. 0x013b: 0xe2, // â
  999. 0x013c: 0xea, // ê
  1000. 0x013d: 0xee, // î
  1001. 0x013e: 0xf4, // ô
  1002. 0x013f: 0xfb, // û
  1003. 0x0220: 0xc1, // Á
  1004. 0x0221: 0xc9, // É
  1005. 0x0222: 0xd3, // Ó
  1006. 0x0223: 0xda, // Ú
  1007. 0x0224: 0xdc, // Ü
  1008. 0x0225: 0xfc, // ü
  1009. 0x0226: 0x2018, // ‘
  1010. 0x0227: 0xa1, // ¡
  1011. 0x0228: 0x2a, // *
  1012. 0x0229: 0x27, // '
  1013. 0x022a: 0x2014, // —
  1014. 0x022b: 0xa9, // ©
  1015. 0x022c: 0x2120, // ℠
  1016. 0x022d: 0x2022, // •
  1017. 0x022e: 0x201c, // “
  1018. 0x022f: 0x201d, // ”
  1019. 0x0230: 0xc0, // À
  1020. 0x0231: 0xc2, // Â
  1021. 0x0232: 0xc7, // Ç
  1022. 0x0233: 0xc8, // È
  1023. 0x0234: 0xca, // Ê
  1024. 0x0235: 0xcb, // Ë
  1025. 0x0236: 0xeb, // ë
  1026. 0x0237: 0xce, // Î
  1027. 0x0238: 0xcf, // Ï
  1028. 0x0239: 0xef, // ï
  1029. 0x023a: 0xd4, // Ô
  1030. 0x023b: 0xd9, // Ù
  1031. 0x023c: 0xf9, // ù
  1032. 0x023d: 0xdb, // Û
  1033. 0x023e: 0xab, // «
  1034. 0x023f: 0xbb, // »
  1035. 0x0320: 0xc3, // Ã
  1036. 0x0321: 0xe3, // ã
  1037. 0x0322: 0xcd, // Í
  1038. 0x0323: 0xcc, // Ì
  1039. 0x0324: 0xec, // ì
  1040. 0x0325: 0xd2, // Ò
  1041. 0x0326: 0xf2, // ò
  1042. 0x0327: 0xd5, // Õ
  1043. 0x0328: 0xf5, // õ
  1044. 0x0329: 0x7b, // {
  1045. 0x032a: 0x7d, // }
  1046. 0x032b: 0x5c, // \
  1047. 0x032c: 0x5e, // ^
  1048. 0x032d: 0x5f, // _
  1049. 0x032e: 0x7c, // |
  1050. 0x032f: 0x7e, // ~
  1051. 0x0330: 0xc4, // Ä
  1052. 0x0331: 0xe4, // ä
  1053. 0x0332: 0xd6, // Ö
  1054. 0x0333: 0xf6, // ö
  1055. 0x0334: 0xdf, // ß
  1056. 0x0335: 0xa5, // ¥
  1057. 0x0336: 0xa4, // ¤
  1058. 0x0337: 0x2502, // │
  1059. 0x0338: 0xc5, // Å
  1060. 0x0339: 0xe5, // å
  1061. 0x033a: 0xd8, // Ø
  1062. 0x033b: 0xf8, // ø
  1063. 0x033c: 0x250c, // ┌
  1064. 0x033d: 0x2510, // ┐
  1065. 0x033e: 0x2514, // └
  1066. 0x033f: 0x2518 // ┘
  1067. };
  1068. var getCharFromCode = function(code) {
  1069. if (code === null) {
  1070. return '';
  1071. }
  1072. code = CHARACTER_TRANSLATION[code] || code;
  1073. return String.fromCharCode(code);
  1074. };
  1075. // the index of the last row in a CEA-608 display buffer
  1076. var BOTTOM_ROW = 14;
  1077. // This array is used for mapping PACs -> row #, since there's no way of
  1078. // getting it through bit logic.
  1079. var ROWS = [0x1100, 0x1120, 0x1200, 0x1220, 0x1500, 0x1520, 0x1600, 0x1620,
  1080. 0x1700, 0x1720, 0x1000, 0x1300, 0x1320, 0x1400, 0x1420];
  1081. // CEA-608 captions are rendered onto a 34x15 matrix of character
  1082. // cells. The "bottom" row is the last element in the outer array.
  1083. // We keep track of positioning information as we go by storing the
  1084. // number of indentations and the tab offset in this buffer.
  1085. var createDisplayBuffer = function() {
  1086. var result = [], i = BOTTOM_ROW + 1;
  1087. while (i--) {
  1088. result.push({ text: '', indent: 0, offset: 0 });
  1089. }
  1090. return result;
  1091. };
  1092. var Cea608Stream = function(field, dataChannel) {
  1093. Cea608Stream.prototype.init.call(this);
  1094. this.field_ = field || 0;
  1095. this.dataChannel_ = dataChannel || 0;
  1096. this.name_ = 'CC' + (((this.field_ << 1) | this.dataChannel_) + 1);
  1097. this.setConstants();
  1098. this.reset();
  1099. this.push = function(packet) {
  1100. var data, swap, char0, char1, text;
  1101. // remove the parity bits
  1102. data = packet.ccData & 0x7f7f;
  1103. // ignore duplicate control codes; the spec demands they're sent twice
  1104. if (data === this.lastControlCode_) {
  1105. this.lastControlCode_ = null;
  1106. return;
  1107. }
  1108. // Store control codes
  1109. if ((data & 0xf000) === 0x1000) {
  1110. this.lastControlCode_ = data;
  1111. } else if (data !== this.PADDING_) {
  1112. this.lastControlCode_ = null;
  1113. }
  1114. char0 = data >>> 8;
  1115. char1 = data & 0xff;
  1116. if (data === this.PADDING_) {
  1117. return;
  1118. } else if (data === this.RESUME_CAPTION_LOADING_) {
  1119. this.mode_ = 'popOn';
  1120. } else if (data === this.END_OF_CAPTION_) {
  1121. // If an EOC is received while in paint-on mode, the displayed caption
  1122. // text should be swapped to non-displayed memory as if it was a pop-on
  1123. // caption. Because of that, we should explicitly switch back to pop-on
  1124. // mode
  1125. this.mode_ = 'popOn';
  1126. this.clearFormatting(packet.pts);
  1127. // if a caption was being displayed, it's gone now
  1128. this.flushDisplayed(packet.pts);
  1129. // flip memory
  1130. swap = this.displayed_;
  1131. this.displayed_ = this.nonDisplayed_;
  1132. this.nonDisplayed_ = swap;
  1133. // start measuring the time to display the caption
  1134. this.startPts_ = packet.pts;
  1135. } else if (data === this.ROLL_UP_2_ROWS_) {
  1136. this.rollUpRows_ = 2;
  1137. this.setRollUp(packet.pts);
  1138. } else if (data === this.ROLL_UP_3_ROWS_) {
  1139. this.rollUpRows_ = 3;
  1140. this.setRollUp(packet.pts);
  1141. } else if (data === this.ROLL_UP_4_ROWS_) {
  1142. this.rollUpRows_ = 4;
  1143. this.setRollUp(packet.pts);
  1144. } else if (data === this.CARRIAGE_RETURN_) {
  1145. this.clearFormatting(packet.pts);
  1146. this.flushDisplayed(packet.pts);
  1147. this.shiftRowsUp_();
  1148. this.startPts_ = packet.pts;
  1149. } else if (data === this.BACKSPACE_) {
  1150. if (this.mode_ === 'popOn') {
  1151. this.nonDisplayed_[this.row_].text = this.nonDisplayed_[this.row_].text.slice(0, -1);
  1152. } else {
  1153. this.displayed_[this.row_].text = this.displayed_[this.row_].text.slice(0, -1);
  1154. }
  1155. } else if (data === this.ERASE_DISPLAYED_MEMORY_) {
  1156. this.flushDisplayed(packet.pts);
  1157. this.displayed_ = createDisplayBuffer();
  1158. } else if (data === this.ERASE_NON_DISPLAYED_MEMORY_) {
  1159. this.nonDisplayed_ = createDisplayBuffer();
  1160. } else if (data === this.RESUME_DIRECT_CAPTIONING_) {
  1161. if (this.mode_ !== 'paintOn') {
  1162. // NOTE: This should be removed when proper caption positioning is
  1163. // implemented
  1164. this.flushDisplayed(packet.pts);
  1165. this.displayed_ = createDisplayBuffer();
  1166. }
  1167. this.mode_ = 'paintOn';
  1168. this.startPts_ = packet.pts;
  1169. // Append special characters to caption text
  1170. } else if (this.isSpecialCharacter(char0, char1)) {
  1171. // Bitmask char0 so that we can apply character transformations
  1172. // regardless of field and data channel.
  1173. // Then byte-shift to the left and OR with char1 so we can pass the
  1174. // entire character code to `getCharFromCode`.
  1175. char0 = (char0 & 0x03) << 8;
  1176. text = getCharFromCode(char0 | char1);
  1177. this[this.mode_](packet.pts, text);
  1178. this.column_++;
  1179. // Append extended characters to caption text
  1180. } else if (this.isExtCharacter(char0, char1)) {
  1181. // Extended characters always follow their "non-extended" equivalents.
  1182. // IE if a "è" is desired, you'll always receive "eè"; non-compliant
  1183. // decoders are supposed to drop the "è", while compliant decoders
  1184. // backspace the "e" and insert "è".
  1185. // Delete the previous character
  1186. if (this.mode_ === 'popOn') {
  1187. this.nonDisplayed_[this.row_].text = this.nonDisplayed_[this.row_].text.slice(0, -1);
  1188. } else {
  1189. this.displayed_[this.row_].text = this.displayed_[this.row_].text.slice(0, -1);
  1190. }
  1191. // Bitmask char0 so that we can apply character transformations
  1192. // regardless of field and data channel.
  1193. // Then byte-shift to the left and OR with char1 so we can pass the
  1194. // entire character code to `getCharFromCode`.
  1195. char0 = (char0 & 0x03) << 8;
  1196. text = getCharFromCode(char0 | char1);
  1197. this[this.mode_](packet.pts, text);
  1198. this.column_++;
  1199. // Process mid-row codes
  1200. } else if (this.isMidRowCode(char0, char1)) {
  1201. // Attributes are not additive, so clear all formatting
  1202. this.clearFormatting(packet.pts);
  1203. // According to the standard, mid-row codes
  1204. // should be replaced with spaces, so add one now
  1205. this[this.mode_](packet.pts, ' ');
  1206. this.column_++;
  1207. if ((char1 & 0xe) === 0xe) {
  1208. this.addFormatting(packet.pts, ['i']);
  1209. }
  1210. if ((char1 & 0x1) === 0x1) {
  1211. this.addFormatting(packet.pts, ['u']);
  1212. }
  1213. // Detect offset control codes and adjust cursor
  1214. } else if (this.isOffsetControlCode(char0, char1)) {
  1215. // Cursor position is set by indent PAC (see below) in 4-column
  1216. // increments, with an additional offset code of 1-3 to reach any
  1217. // of the 32 columns specified by CEA-608. So all we need to do
  1218. // here is increment the column cursor by the given offset.
  1219. const offset = (char1 & 0x03);
  1220. // For an offest value 1-3, set the offset for that caption
  1221. // in the non-displayed array.
  1222. this.nonDisplayed_[this.row_].offset = offset;
  1223. this.column_ += offset;
  1224. // Detect PACs (Preamble Address Codes)
  1225. } else if (this.isPAC(char0, char1)) {
  1226. // There's no logic for PAC -> row mapping, so we have to just
  1227. // find the row code in an array and use its index :(
  1228. var row = ROWS.indexOf(data & 0x1f20);
  1229. // Configure the caption window if we're in roll-up mode
  1230. if (this.mode_ === 'rollUp') {
  1231. // This implies that the base row is incorrectly set.
  1232. // As per the recommendation in CEA-608(Base Row Implementation), defer to the number
  1233. // of roll-up rows set.
  1234. if (row - this.rollUpRows_ + 1 < 0) {
  1235. row = this.rollUpRows_ - 1;
  1236. }
  1237. this.setRollUp(packet.pts, row);
  1238. }
  1239. if (row !== this.row_) {
  1240. // formatting is only persistent for current row
  1241. this.clearFormatting(packet.pts);
  1242. this.row_ = row;
  1243. }
  1244. // All PACs can apply underline, so detect and apply
  1245. // (All odd-numbered second bytes set underline)
  1246. if ((char1 & 0x1) && (this.formatting_.indexOf('u') === -1)) {
  1247. this.addFormatting(packet.pts, ['u']);
  1248. }
  1249. if ((data & 0x10) === 0x10) {
  1250. // We've got an indent level code. Each successive even number
  1251. // increments the column cursor by 4, so we can get the desired
  1252. // column position by bit-shifting to the right (to get n/2)
  1253. // and multiplying by 4.
  1254. const indentations = ((data & 0xe) >> 1);
  1255. this.column_ = indentations * 4;
  1256. // add to the number of indentations for positioning
  1257. this.nonDisplayed_[this.row_].indent += indentations;
  1258. }
  1259. if (this.isColorPAC(char1)) {
  1260. // it's a color code, though we only support white, which
  1261. // can be either normal or italicized. white italics can be
  1262. // either 0x4e or 0x6e depending on the row, so we just
  1263. // bitwise-and with 0xe to see if italics should be turned on
  1264. if ((char1 & 0xe) === 0xe) {
  1265. this.addFormatting(packet.pts, ['i']);
  1266. }
  1267. }
  1268. // We have a normal character in char0, and possibly one in char1
  1269. } else if (this.isNormalChar(char0)) {
  1270. if (char1 === 0x00) {
  1271. char1 = null;
  1272. }
  1273. text = getCharFromCode(char0);
  1274. text += getCharFromCode(char1);
  1275. this[this.mode_](packet.pts, text);
  1276. this.column_ += text.length;
  1277. } // finish data processing
  1278. };
  1279. };
  1280. Cea608Stream.prototype = new Stream();
  1281. // Trigger a cue point that captures the current state of the
  1282. // display buffer
  1283. Cea608Stream.prototype.flushDisplayed = function(pts) {
  1284. const logWarning = (index) => {
  1285. this.trigger('log', {
  1286. level: 'warn',
  1287. message: 'Skipping a malformed 608 caption at index ' + index + '.'
  1288. });
  1289. };
  1290. const content = [];
  1291. this.displayed_.forEach((row, i) => {
  1292. if (row && row.text && row.text.length) {
  1293. try {
  1294. // remove spaces from the start and end of the string
  1295. row.text = row.text.trim();
  1296. } catch (e) {
  1297. // Ordinarily, this shouldn't happen. However, caption
  1298. // parsing errors should not throw exceptions and
  1299. // break playback.
  1300. logWarning(i);
  1301. }
  1302. // See the below link for more details on the following fields:
  1303. // https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/608toVTT.html#positioning-in-cea-608
  1304. if (row.text.length) {
  1305. content.push({
  1306. // The text to be displayed in the caption from this specific row, with whitespace removed.
  1307. text: row.text,
  1308. // Value between 1 and 15 representing the PAC row used to calculate line height.
  1309. line: i + 1,
  1310. // A number representing the indent position by percentage (CEA-608 PAC indent code).
  1311. // The value will be a number between 10 and 80. Offset is used to add an aditional
  1312. // value to the position if necessary.
  1313. position: 10 + Math.min(70, row.indent * 10) + (row.offset * 2.5),
  1314. });
  1315. }
  1316. }
  1317. else if (row === undefined || row === null) {
  1318. logWarning(i);
  1319. }
  1320. });
  1321. if (content.length) {
  1322. this.trigger('data', {
  1323. startPts: this.startPts_,
  1324. endPts: pts,
  1325. content,
  1326. stream: this.name_
  1327. });
  1328. }
  1329. };
  1330. /**
  1331. * Zero out the data, used for startup and on seek
  1332. */
  1333. Cea608Stream.prototype.reset = function() {
  1334. this.mode_ = 'popOn';
  1335. // When in roll-up mode, the index of the last row that will
  1336. // actually display captions. If a caption is shifted to a row
  1337. // with a lower index than this, it is cleared from the display
  1338. // buffer
  1339. this.topRow_ = 0;
  1340. this.startPts_ = 0;
  1341. this.displayed_ = createDisplayBuffer();
  1342. this.nonDisplayed_ = createDisplayBuffer();
  1343. this.lastControlCode_ = null;
  1344. // Track row and column for proper line-breaking and spacing
  1345. this.column_ = 0;
  1346. this.row_ = BOTTOM_ROW;
  1347. this.rollUpRows_ = 2;
  1348. // This variable holds currently-applied formatting
  1349. this.formatting_ = [];
  1350. };
  1351. /**
  1352. * Sets up control code and related constants for this instance
  1353. */
  1354. Cea608Stream.prototype.setConstants = function() {
  1355. // The following attributes have these uses:
  1356. // ext_ : char0 for mid-row codes, and the base for extended
  1357. // chars (ext_+0, ext_+1, and ext_+2 are char0s for
  1358. // extended codes)
  1359. // control_: char0 for control codes, except byte-shifted to the
  1360. // left so that we can do this.control_ | CONTROL_CODE
  1361. // offset_: char0 for tab offset codes
  1362. //
  1363. // It's also worth noting that control codes, and _only_ control codes,
  1364. // differ between field 1 and field2. Field 2 control codes are always
  1365. // their field 1 value plus 1. That's why there's the "| field" on the
  1366. // control value.
  1367. if (this.dataChannel_ === 0) {
  1368. this.BASE_ = 0x10;
  1369. this.EXT_ = 0x11;
  1370. this.CONTROL_ = (0x14 | this.field_) << 8;
  1371. this.OFFSET_ = 0x17;
  1372. } else if (this.dataChannel_ === 1) {
  1373. this.BASE_ = 0x18;
  1374. this.EXT_ = 0x19;
  1375. this.CONTROL_ = (0x1c | this.field_) << 8;
  1376. this.OFFSET_ = 0x1f;
  1377. }
  1378. // Constants for the LSByte command codes recognized by Cea608Stream. This
  1379. // list is not exhaustive. For a more comprehensive listing and semantics see
  1380. // http://www.gpo.gov/fdsys/pkg/CFR-2010-title47-vol1/pdf/CFR-2010-title47-vol1-sec15-119.pdf
  1381. // Padding
  1382. this.PADDING_ = 0x0000;
  1383. // Pop-on Mode
  1384. this.RESUME_CAPTION_LOADING_ = this.CONTROL_ | 0x20;
  1385. this.END_OF_CAPTION_ = this.CONTROL_ | 0x2f;
  1386. // Roll-up Mode
  1387. this.ROLL_UP_2_ROWS_ = this.CONTROL_ | 0x25;
  1388. this.ROLL_UP_3_ROWS_ = this.CONTROL_ | 0x26;
  1389. this.ROLL_UP_4_ROWS_ = this.CONTROL_ | 0x27;
  1390. this.CARRIAGE_RETURN_ = this.CONTROL_ | 0x2d;
  1391. // paint-on mode
  1392. this.RESUME_DIRECT_CAPTIONING_ = this.CONTROL_ | 0x29;
  1393. // Erasure
  1394. this.BACKSPACE_ = this.CONTROL_ | 0x21;
  1395. this.ERASE_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2c;
  1396. this.ERASE_NON_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2e;
  1397. };
  1398. /**
  1399. * Detects if the 2-byte packet data is a special character
  1400. *
  1401. * Special characters have a second byte in the range 0x30 to 0x3f,
  1402. * with the first byte being 0x11 (for data channel 1) or 0x19 (for
  1403. * data channel 2).
  1404. *
  1405. * @param {Integer} char0 The first byte
  1406. * @param {Integer} char1 The second byte
  1407. * @return {Boolean} Whether the 2 bytes are an special character
  1408. */
  1409. Cea608Stream.prototype.isSpecialCharacter = function(char0, char1) {
  1410. return (char0 === this.EXT_ && char1 >= 0x30 && char1 <= 0x3f);
  1411. };
  1412. /**
  1413. * Detects if the 2-byte packet data is an extended character
  1414. *
  1415. * Extended characters have a second byte in the range 0x20 to 0x3f,
  1416. * with the first byte being 0x12 or 0x13 (for data channel 1) or
  1417. * 0x1a or 0x1b (for data channel 2).
  1418. *
  1419. * @param {Integer} char0 The first byte
  1420. * @param {Integer} char1 The second byte
  1421. * @return {Boolean} Whether the 2 bytes are an extended character
  1422. */
  1423. Cea608Stream.prototype.isExtCharacter = function(char0, char1) {
  1424. return ((char0 === (this.EXT_ + 1) || char0 === (this.EXT_ + 2)) &&
  1425. (char1 >= 0x20 && char1 <= 0x3f));
  1426. };
  1427. /**
  1428. * Detects if the 2-byte packet is a mid-row code
  1429. *
  1430. * Mid-row codes have a second byte in the range 0x20 to 0x2f, with
  1431. * the first byte being 0x11 (for data channel 1) or 0x19 (for data
  1432. * channel 2).
  1433. *
  1434. * @param {Integer} char0 The first byte
  1435. * @param {Integer} char1 The second byte
  1436. * @return {Boolean} Whether the 2 bytes are a mid-row code
  1437. */
  1438. Cea608Stream.prototype.isMidRowCode = function(char0, char1) {
  1439. return (char0 === this.EXT_ && (char1 >= 0x20 && char1 <= 0x2f));
  1440. };
  1441. /**
  1442. * Detects if the 2-byte packet is an offset control code
  1443. *
  1444. * Offset control codes have a second byte in the range 0x21 to 0x23,
  1445. * with the first byte being 0x17 (for data channel 1) or 0x1f (for
  1446. * data channel 2).
  1447. *
  1448. * @param {Integer} char0 The first byte
  1449. * @param {Integer} char1 The second byte
  1450. * @return {Boolean} Whether the 2 bytes are an offset control code
  1451. */
  1452. Cea608Stream.prototype.isOffsetControlCode = function(char0, char1) {
  1453. return (char0 === this.OFFSET_ && (char1 >= 0x21 && char1 <= 0x23));
  1454. };
  1455. /**
  1456. * Detects if the 2-byte packet is a Preamble Address Code
  1457. *
  1458. * PACs have a first byte in the range 0x10 to 0x17 (for data channel 1)
  1459. * or 0x18 to 0x1f (for data channel 2), with the second byte in the
  1460. * range 0x40 to 0x7f.
  1461. *
  1462. * @param {Integer} char0 The first byte
  1463. * @param {Integer} char1 The second byte
  1464. * @return {Boolean} Whether the 2 bytes are a PAC
  1465. */
  1466. Cea608Stream.prototype.isPAC = function(char0, char1) {
  1467. return (char0 >= this.BASE_ && char0 < (this.BASE_ + 8) &&
  1468. (char1 >= 0x40 && char1 <= 0x7f));
  1469. };
  1470. /**
  1471. * Detects if a packet's second byte is in the range of a PAC color code
  1472. *
  1473. * PAC color codes have the second byte be in the range 0x40 to 0x4f, or
  1474. * 0x60 to 0x6f.
  1475. *
  1476. * @param {Integer} char1 The second byte
  1477. * @return {Boolean} Whether the byte is a color PAC
  1478. */
  1479. Cea608Stream.prototype.isColorPAC = function(char1) {
  1480. return ((char1 >= 0x40 && char1 <= 0x4f) || (char1 >= 0x60 && char1 <= 0x7f));
  1481. };
  1482. /**
  1483. * Detects if a single byte is in the range of a normal character
  1484. *
  1485. * Normal text bytes are in the range 0x20 to 0x7f.
  1486. *
  1487. * @param {Integer} char The byte
  1488. * @return {Boolean} Whether the byte is a normal character
  1489. */
  1490. Cea608Stream.prototype.isNormalChar = function(char) {
  1491. return (char >= 0x20 && char <= 0x7f);
  1492. };
  1493. /**
  1494. * Configures roll-up
  1495. *
  1496. * @param {Integer} pts Current PTS
  1497. * @param {Integer} newBaseRow Used by PACs to slide the current window to
  1498. * a new position
  1499. */
  1500. Cea608Stream.prototype.setRollUp = function(pts, newBaseRow) {
  1501. // Reset the base row to the bottom row when switching modes
  1502. if (this.mode_ !== 'rollUp') {
  1503. this.row_ = BOTTOM_ROW;
  1504. this.mode_ = 'rollUp';
  1505. // Spec says to wipe memories when switching to roll-up
  1506. this.flushDisplayed(pts);
  1507. this.nonDisplayed_ = createDisplayBuffer();
  1508. this.displayed_ = createDisplayBuffer();
  1509. }
  1510. if (newBaseRow !== undefined && newBaseRow !== this.row_) {
  1511. // move currently displayed captions (up or down) to the new base row
  1512. for (var i = 0; i < this.rollUpRows_; i++) {
  1513. this.displayed_[newBaseRow - i] = this.displayed_[this.row_ - i];
  1514. this.displayed_[this.row_ - i] = { text: '', indent: 0, offset: 0 };
  1515. }
  1516. }
  1517. if (newBaseRow === undefined) {
  1518. newBaseRow = this.row_;
  1519. }
  1520. this.topRow_ = newBaseRow - this.rollUpRows_ + 1;
  1521. };
  1522. // Adds the opening HTML tag for the passed character to the caption text,
  1523. // and keeps track of it for later closing
  1524. Cea608Stream.prototype.addFormatting = function(pts, format) {
  1525. this.formatting_ = this.formatting_.concat(format);
  1526. var text = format.reduce(function(text, format) {
  1527. return text + '<' + format + '>';
  1528. }, '');
  1529. this[this.mode_](pts, text);
  1530. };
  1531. // Adds HTML closing tags for current formatting to caption text and
  1532. // clears remembered formatting
  1533. Cea608Stream.prototype.clearFormatting = function(pts) {
  1534. if (!this.formatting_.length) {
  1535. return;
  1536. }
  1537. var text = this.formatting_.reverse().reduce(function(text, format) {
  1538. return text + '</' + format + '>';
  1539. }, '');
  1540. this.formatting_ = [];
  1541. this[this.mode_](pts, text);
  1542. };
  1543. // Mode Implementations
  1544. Cea608Stream.prototype.popOn = function(pts, text) {
  1545. var baseRow = this.nonDisplayed_[this.row_].text;
  1546. // buffer characters
  1547. baseRow += text;
  1548. this.nonDisplayed_[this.row_].text = baseRow;
  1549. };
  1550. Cea608Stream.prototype.rollUp = function(pts, text) {
  1551. var baseRow = this.displayed_[this.row_].text;
  1552. baseRow += text;
  1553. this.displayed_[this.row_].text = baseRow;
  1554. };
  1555. Cea608Stream.prototype.shiftRowsUp_ = function() {
  1556. var i;
  1557. // clear out inactive rows
  1558. for (i = 0; i < this.topRow_; i++) {
  1559. this.displayed_[i] = { text: '', indent: 0, offset: 0 };
  1560. }
  1561. for (i = this.row_ + 1; i < BOTTOM_ROW + 1; i++) {
  1562. this.displayed_[i] = { text: '', indent: 0, offset: 0 };
  1563. }
  1564. // shift displayed rows up
  1565. for (i = this.topRow_; i < this.row_; i++) {
  1566. this.displayed_[i] = this.displayed_[i + 1];
  1567. }
  1568. // clear out the bottom row
  1569. this.displayed_[this.row_] = { text: '', indent: 0, offset: 0 };
  1570. };
  1571. Cea608Stream.prototype.paintOn = function(pts, text) {
  1572. var baseRow = this.displayed_[this.row_].text;
  1573. baseRow += text;
  1574. this.displayed_[this.row_].text = baseRow;
  1575. };
  1576. // exports
  1577. module.exports = {
  1578. CaptionStream: CaptionStream,
  1579. Cea608Stream: Cea608Stream,
  1580. Cea708Stream: Cea708Stream
  1581. };