caption-stream.js 54 KB

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