123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856 |
- /**
- * mux.js
- *
- * Copyright (c) 2015 Brightcove
- * All rights reserved.
- *
- * Reads in-band caption information from a video elementary
- * stream. Captions must follow the CEA-708 standard for injection
- * into an MPEG-2 transport streams.
- * @see https://en.wikipedia.org/wiki/CEA-708
- * @see https://www.gpo.gov/fdsys/pkg/CFR-2007-title47-vol1/pdf/CFR-2007-title47-vol1-sec15-119.pdf
- */
- 'use strict';
- // -----------------
- // Link To Transport
- // -----------------
- // Supplemental enhancement information (SEI) NAL units have a
- // payload type field to indicate how they are to be
- // interpreted. CEAS-708 caption content is always transmitted with
- // payload type 0x04.
- var USER_DATA_REGISTERED_ITU_T_T35 = 4,
- RBSP_TRAILING_BITS = 128,
- Stream = require('../utils/stream');
- /**
- * Parse a supplemental enhancement information (SEI) NAL unit.
- * Stops parsing once a message of type ITU T T35 has been found.
- *
- * @param bytes {Uint8Array} the bytes of a SEI NAL unit
- * @return {object} the parsed SEI payload
- * @see Rec. ITU-T H.264, 7.3.2.3.1
- */
- var parseSei = function(bytes) {
- var
- i = 0,
- result = {
- payloadType: -1,
- payloadSize: 0
- },
- payloadType = 0,
- payloadSize = 0;
- // go through the sei_rbsp parsing each each individual sei_message
- while (i < bytes.byteLength) {
- // stop once we have hit the end of the sei_rbsp
- if (bytes[i] === RBSP_TRAILING_BITS) {
- break;
- }
- // Parse payload type
- while (bytes[i] === 0xFF) {
- payloadType += 255;
- i++;
- }
- payloadType += bytes[i++];
- // Parse payload size
- while (bytes[i] === 0xFF) {
- payloadSize += 255;
- i++;
- }
- payloadSize += bytes[i++];
- // this sei_message is a 608/708 caption so save it and break
- // there can only ever be one caption message in a frame's sei
- if (!result.payload && payloadType === USER_DATA_REGISTERED_ITU_T_T35) {
- result.payloadType = payloadType;
- result.payloadSize = payloadSize;
- result.payload = bytes.subarray(i, i + payloadSize);
- break;
- }
- // skip the payload and parse the next message
- i += payloadSize;
- payloadType = 0;
- payloadSize = 0;
- }
- return result;
- };
- // see ANSI/SCTE 128-1 (2013), section 8.1
- var parseUserData = function(sei) {
- // itu_t_t35_contry_code must be 181 (United States) for
- // captions
- if (sei.payload[0] !== 181) {
- return null;
- }
- // itu_t_t35_provider_code should be 49 (ATSC) for captions
- if (((sei.payload[1] << 8) | sei.payload[2]) !== 49) {
- return null;
- }
- // the user_identifier should be "GA94" to indicate ATSC1 data
- if (String.fromCharCode(sei.payload[3],
- sei.payload[4],
- sei.payload[5],
- sei.payload[6]) !== 'GA94') {
- return null;
- }
- // finally, user_data_type_code should be 0x03 for caption data
- if (sei.payload[7] !== 0x03) {
- return null;
- }
- // return the user_data_type_structure and strip the trailing
- // marker bits
- return sei.payload.subarray(8, sei.payload.length - 1);
- };
- // see CEA-708-D, section 4.4
- var parseCaptionPackets = function(pts, userData) {
- var results = [], i, count, offset, data;
- // if this is just filler, return immediately
- if (!(userData[0] & 0x40)) {
- return results;
- }
- // parse out the cc_data_1 and cc_data_2 fields
- count = userData[0] & 0x1f;
- for (i = 0; i < count; i++) {
- offset = i * 3;
- data = {
- type: userData[offset + 2] & 0x03,
- pts: pts
- };
- // capture cc data when cc_valid is 1
- if (userData[offset + 2] & 0x04) {
- data.ccData = (userData[offset + 3] << 8) | userData[offset + 4];
- results.push(data);
- }
- }
- return results;
- };
- var CaptionStream = function() {
- CaptionStream.prototype.init.call(this);
- this.captionPackets_ = [];
- this.ccStreams_ = [
- new Cea608Stream(0, 0), // eslint-disable-line no-use-before-define
- new Cea608Stream(0, 1), // eslint-disable-line no-use-before-define
- new Cea608Stream(1, 0), // eslint-disable-line no-use-before-define
- new Cea608Stream(1, 1) // eslint-disable-line no-use-before-define
- ];
- this.reset();
- // forward data and done events from CCs to this CaptionStream
- this.ccStreams_.forEach(function(cc) {
- cc.on('data', this.trigger.bind(this, 'data'));
- cc.on('done', this.trigger.bind(this, 'done'));
- }, this);
- };
- CaptionStream.prototype = new Stream();
- CaptionStream.prototype.push = function(event) {
- var sei, userData;
- // only examine SEI NALs
- if (event.nalUnitType !== 'sei_rbsp') {
- return;
- }
- // parse the sei
- sei = parseSei(event.escapedRBSP);
- // ignore everything but user_data_registered_itu_t_t35
- if (sei.payloadType !== USER_DATA_REGISTERED_ITU_T_T35) {
- return;
- }
- // parse out the user data payload
- userData = parseUserData(sei);
- // ignore unrecognized userData
- if (!userData) {
- return;
- }
- // Sometimes, the same segment # will be downloaded twice. To stop the
- // caption data from being processed twice, we track the latest dts we've
- // received and ignore everything with a dts before that. However, since
- // data for a specific dts can be split across 2 packets on either side of
- // a segment boundary, we need to make sure we *don't* ignore the second
- // dts packet we receive that has dts === this.latestDts_. And thus, the
- // ignoreNextEqualDts_ flag was born.
- if (event.dts < this.latestDts_) {
- // We've started getting older data, so set the flag.
- this.ignoreNextEqualDts_ = true;
- return;
- } else if ((event.dts === this.latestDts_) && (this.ignoreNextEqualDts_)) {
- // We've received the last duplicate packet, time to start processing again
- this.ignoreNextEqualDts_ = false;
- return;
- }
- // parse out CC data packets and save them for later
- this.captionPackets_ = this.captionPackets_.concat(parseCaptionPackets(event.pts, userData));
- this.latestDts_ = event.dts;
- };
- CaptionStream.prototype.flush = function() {
- // make sure we actually parsed captions before proceeding
- if (!this.captionPackets_.length) {
- this.ccStreams_.forEach(function(cc) {
- cc.flush();
- }, this);
- return;
- }
- // In Chrome, the Array#sort function is not stable so add a
- // presortIndex that we can use to ensure we get a stable-sort
- this.captionPackets_.forEach(function(elem, idx) {
- elem.presortIndex = idx;
- });
- // sort caption byte-pairs based on their PTS values
- this.captionPackets_.sort(function(a, b) {
- if (a.pts === b.pts) {
- return a.presortIndex - b.presortIndex;
- }
- return a.pts - b.pts;
- });
- this.captionPackets_.forEach(function(packet) {
- if (packet.type < 2) {
- // Dispatch packet to the right Cea608Stream
- this.dispatchCea608Packet(packet);
- }
- // this is where an 'else' would go for a dispatching packets
- // to a theoretical Cea708Stream that handles SERVICEn data
- }, this);
- this.captionPackets_.length = 0;
- this.ccStreams_.forEach(function(cc) {
- cc.flush();
- }, this);
- return;
- };
- CaptionStream.prototype.reset = function() {
- this.latestDts_ = null;
- this.ignoreNextEqualDts_ = false;
- this.activeCea608Channel_ = [null, null];
- this.ccStreams_.forEach(function(ccStream) {
- ccStream.reset();
- });
- };
- CaptionStream.prototype.dispatchCea608Packet = function(packet) {
- // NOTE: packet.type is the CEA608 field
- if (this.setsChannel1Active(packet)) {
- this.activeCea608Channel_[packet.type] = 0;
- } else if (this.setsChannel2Active(packet)) {
- this.activeCea608Channel_[packet.type] = 1;
- }
- if (this.activeCea608Channel_[packet.type] === null) {
- // If we haven't received anything to set the active channel, discard the
- // data; we don't want jumbled captions
- return;
- }
- this.ccStreams_[(packet.type << 1) + this.activeCea608Channel_[packet.type]].push(packet);
- };
- CaptionStream.prototype.setsChannel1Active = function(packet) {
- return ((packet.ccData & 0x7800) === 0x1000);
- };
- CaptionStream.prototype.setsChannel2Active = function(packet) {
- return ((packet.ccData & 0x7800) === 0x1800);
- };
- // ----------------------
- // Session to Application
- // ----------------------
- var CHARACTER_TRANSLATION = {
- 0x2a: 0xe1, // á
- 0x5c: 0xe9, // é
- 0x5e: 0xed, // í
- 0x5f: 0xf3, // ó
- 0x60: 0xfa, // ú
- 0x7b: 0xe7, // ç
- 0x7c: 0xf7, // ÷
- 0x7d: 0xd1, // Ñ
- 0x7e: 0xf1, // ñ
- 0x7f: 0x2588, // █
- 0x0130: 0xae, // ®
- 0x0131: 0xb0, // °
- 0x0132: 0xbd, // ½
- 0x0133: 0xbf, // ¿
- 0x0134: 0x2122, // ™
- 0x0135: 0xa2, // ¢
- 0x0136: 0xa3, // £
- 0x0137: 0x266a, // ♪
- 0x0138: 0xe0, // à
- 0x0139: 0xa0, //
- 0x013a: 0xe8, // è
- 0x013b: 0xe2, // â
- 0x013c: 0xea, // ê
- 0x013d: 0xee, // î
- 0x013e: 0xf4, // ô
- 0x013f: 0xfb, // û
- 0x0220: 0xc1, // Á
- 0x0221: 0xc9, // É
- 0x0222: 0xd3, // Ó
- 0x0223: 0xda, // Ú
- 0x0224: 0xdc, // Ü
- 0x0225: 0xfc, // ü
- 0x0226: 0x2018, // ‘
- 0x0227: 0xa1, // ¡
- 0x0228: 0x2a, // *
- 0x0229: 0x27, // '
- 0x022a: 0x2014, // —
- 0x022b: 0xa9, // ©
- 0x022c: 0x2120, // ℠
- 0x022d: 0x2022, // •
- 0x022e: 0x201c, // “
- 0x022f: 0x201d, // ”
- 0x0230: 0xc0, // À
- 0x0231: 0xc2, // Â
- 0x0232: 0xc7, // Ç
- 0x0233: 0xc8, // È
- 0x0234: 0xca, // Ê
- 0x0235: 0xcb, // Ë
- 0x0236: 0xeb, // ë
- 0x0237: 0xce, // Î
- 0x0238: 0xcf, // Ï
- 0x0239: 0xef, // ï
- 0x023a: 0xd4, // Ô
- 0x023b: 0xd9, // Ù
- 0x023c: 0xf9, // ù
- 0x023d: 0xdb, // Û
- 0x023e: 0xab, // «
- 0x023f: 0xbb, // »
- 0x0320: 0xc3, // Ã
- 0x0321: 0xe3, // ã
- 0x0322: 0xcd, // Í
- 0x0323: 0xcc, // Ì
- 0x0324: 0xec, // ì
- 0x0325: 0xd2, // Ò
- 0x0326: 0xf2, // ò
- 0x0327: 0xd5, // Õ
- 0x0328: 0xf5, // õ
- 0x0329: 0x7b, // {
- 0x032a: 0x7d, // }
- 0x032b: 0x5c, // \
- 0x032c: 0x5e, // ^
- 0x032d: 0x5f, // _
- 0x032e: 0x7c, // |
- 0x032f: 0x7e, // ~
- 0x0330: 0xc4, // Ä
- 0x0331: 0xe4, // ä
- 0x0332: 0xd6, // Ö
- 0x0333: 0xf6, // ö
- 0x0334: 0xdf, // ß
- 0x0335: 0xa5, // ¥
- 0x0336: 0xa4, // ¤
- 0x0337: 0x2502, // │
- 0x0338: 0xc5, // Å
- 0x0339: 0xe5, // å
- 0x033a: 0xd8, // Ø
- 0x033b: 0xf8, // ø
- 0x033c: 0x250c, // ┌
- 0x033d: 0x2510, // ┐
- 0x033e: 0x2514, // └
- 0x033f: 0x2518 // ┘
- };
- var getCharFromCode = function(code) {
- if (code === null) {
- return '';
- }
- code = CHARACTER_TRANSLATION[code] || code;
- return String.fromCharCode(code);
- };
- // the index of the last row in a CEA-608 display buffer
- var BOTTOM_ROW = 14;
- // This array is used for mapping PACs -> row #, since there's no way of
- // getting it through bit logic.
- 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
- // cells. The "bottom" row is the last element in the outer array.
- var createDisplayBuffer = function() {
- var result = [], i = BOTTOM_ROW + 1;
- while (i--) {
- result.push('');
- }
- return result;
- };
- var Cea608Stream = function(field, dataChannel) {
- Cea608Stream.prototype.init.call(this);
- this.field_ = field || 0;
- this.dataChannel_ = dataChannel || 0;
- this.name_ = 'CC' + (((this.field_ << 1) | this.dataChannel_) + 1);
- this.setConstants();
- this.reset();
- this.push = function(packet) {
- var data, swap, char0, char1, text;
- // remove the parity bits
- data = packet.ccData & 0x7f7f;
- // ignore duplicate control codes; the spec demands they're sent twice
- if (data === this.lastControlCode_) {
- this.lastControlCode_ = null;
- return;
- }
- // Store control codes
- if ((data & 0xf000) === 0x1000) {
- this.lastControlCode_ = data;
- } else if (data !== this.PADDING_) {
- this.lastControlCode_ = null;
- }
- char0 = data >>> 8;
- char1 = data & 0xff;
- if (data === this.PADDING_) {
- return;
- } else if (data === this.RESUME_CAPTION_LOADING_) {
- this.mode_ = 'popOn';
- } else if (data === this.END_OF_CAPTION_) {
- this.clearFormatting(packet.pts);
- // if a caption was being displayed, it's gone now
- this.flushDisplayed(packet.pts);
- // flip memory
- swap = this.displayed_;
- this.displayed_ = this.nonDisplayed_;
- this.nonDisplayed_ = swap;
- // start measuring the time to display the caption
- this.startPts_ = packet.pts;
- } else if (data === this.ROLL_UP_2_ROWS_) {
- this.topRow_ = BOTTOM_ROW - 1;
- this.mode_ = 'rollUp';
- } else if (data === this.ROLL_UP_3_ROWS_) {
- this.topRow_ = BOTTOM_ROW - 2;
- this.mode_ = 'rollUp';
- } else if (data === this.ROLL_UP_4_ROWS_) {
- this.topRow_ = BOTTOM_ROW - 3;
- this.mode_ = 'rollUp';
- } else if (data === this.CARRIAGE_RETURN_) {
- this.clearFormatting(packet.pts);
- this.flushDisplayed(packet.pts);
- this.shiftRowsUp_();
- this.startPts_ = packet.pts;
- } else if (data === this.BACKSPACE_) {
- if (this.mode_ === 'popOn') {
- this.nonDisplayed_[BOTTOM_ROW] = this.nonDisplayed_[BOTTOM_ROW].slice(0, -1);
- } else {
- this.displayed_[BOTTOM_ROW] = this.displayed_[BOTTOM_ROW].slice(0, -1);
- }
- } else if (data === this.ERASE_DISPLAYED_MEMORY_) {
- this.flushDisplayed(packet.pts);
- this.displayed_ = createDisplayBuffer();
- } else if (data === this.ERASE_NON_DISPLAYED_MEMORY_) {
- this.nonDisplayed_ = createDisplayBuffer();
- } else if (data === this.RESUME_DIRECT_CAPTIONING_) {
- this.mode_ = 'paintOn';
- // Append special characters to caption text
- } else if (this.isSpecialCharacter(char0, char1)) {
- // Bitmask char0 so that we can apply character transformations
- // regardless of field and data channel.
- // Then byte-shift to the left and OR with char1 so we can pass the
- // entire character code to `getCharFromCode`.
- char0 = (char0 & 0x03) << 8;
- text = getCharFromCode(char0 | char1);
- this[this.mode_](packet.pts, text);
- this.column_++;
- // Append extended characters to caption text
- } else if (this.isExtCharacter(char0, char1)) {
- // Extended characters always follow their "non-extended" equivalents.
- // IE if a "è" is desired, you'll always receive "eè"; non-compliant
- // decoders are supposed to drop the "è", while compliant decoders
- // backspace the "e" and insert "è".
- // Delete the previous character
- if (this.mode_ === 'popOn') {
- this.nonDisplayed_[this.row_] = this.nonDisplayed_[this.row_].slice(0, -1);
- } else {
- this.displayed_[BOTTOM_ROW] = this.displayed_[BOTTOM_ROW].slice(0, -1);
- }
- // Bitmask char0 so that we can apply character transformations
- // regardless of field and data channel.
- // Then byte-shift to the left and OR with char1 so we can pass the
- // entire character code to `getCharFromCode`.
- char0 = (char0 & 0x03) << 8;
- text = getCharFromCode(char0 | char1);
- this[this.mode_](packet.pts, text);
- this.column_++;
- // Process mid-row codes
- } else if (this.isMidRowCode(char0, char1)) {
- // Attributes are not additive, so clear all formatting
- this.clearFormatting(packet.pts);
- // According to the standard, mid-row codes
- // should be replaced with spaces, so add one now
- this[this.mode_](packet.pts, ' ');
- this.column_++;
- if ((char1 & 0xe) === 0xe) {
- this.addFormatting(packet.pts, ['i']);
- }
- if ((char1 & 0x1) === 0x1) {
- this.addFormatting(packet.pts, ['u']);
- }
- // Detect offset control codes and adjust cursor
- } else if (this.isOffsetControlCode(char0, char1)) {
- // Cursor position is set by indent PAC (see below) in 4-column
- // increments, with an additional offset code of 1-3 to reach any
- // of the 32 columns specified by CEA-608. So all we need to do
- // here is increment the column cursor by the given offset.
- this.column_ += (char1 & 0x03);
- // Detect PACs (Preamble Address Codes)
- } else if (this.isPAC(char0, char1)) {
- // There's no logic for PAC -> row mapping, so we have to just
- // find the row code in an array and use its index :(
- var row = ROWS.indexOf(data & 0x1f20);
- if (row !== this.row_) {
- // formatting is only persistent for current row
- this.clearFormatting(packet.pts);
- this.row_ = row;
- }
- // All PACs can apply underline, so detect and apply
- // (All odd-numbered second bytes set underline)
- if ((char1 & 0x1) && (this.formatting_.indexOf('u') === -1)) {
- this.addFormatting(packet.pts, ['u']);
- }
- if ((data & 0x10) === 0x10) {
- // We've got an indent level code. Each successive even number
- // increments the column cursor by 4, so we can get the desired
- // column position by bit-shifting to the right (to get n/2)
- // and multiplying by 4.
- this.column_ = ((data & 0xe) >> 1) * 4;
- }
- if (this.isColorPAC(char1)) {
- // it's a color code, though we only support white, which
- // can be either normal or italicized. white italics can be
- // either 0x4e or 0x6e depending on the row, so we just
- // bitwise-and with 0xe to see if italics should be turned on
- if ((char1 & 0xe) === 0xe) {
- this.addFormatting(packet.pts, ['i']);
- }
- }
- // We have a normal character in char0, and possibly one in char1
- } else if (this.isNormalChar(char0)) {
- if (char1 === 0x00) {
- char1 = null;
- }
- text = getCharFromCode(char0);
- text += getCharFromCode(char1);
- this[this.mode_](packet.pts, text);
- this.column_ += text.length;
- } // finish data processing
- };
- };
- Cea608Stream.prototype = new Stream();
- // Trigger a cue point that captures the current state of the
- // display buffer
- Cea608Stream.prototype.flushDisplayed = function(pts) {
- var content = this.displayed_
- // remove spaces from the start and end of the string
- .map(function(row) {
- return row.trim();
- })
- // combine all text rows to display in one cue
- .join('\n')
- // and remove blank rows from the start and end, but not the middle
- .replace(/^\n+|\n+$/g, '');
- if (content.length) {
- this.trigger('data', {
- startPts: this.startPts_,
- endPts: pts,
- text: content,
- stream: this.name_
- });
- }
- };
- /**
- * Zero out the data, used for startup and on seek
- */
- Cea608Stream.prototype.reset = function() {
- this.mode_ = 'popOn';
- // When in roll-up mode, the index of the last row that will
- // actually display captions. If a caption is shifted to a row
- // with a lower index than this, it is cleared from the display
- // buffer
- this.topRow_ = 0;
- this.startPts_ = 0;
- this.displayed_ = createDisplayBuffer();
- this.nonDisplayed_ = createDisplayBuffer();
- this.lastControlCode_ = null;
- // Track row and column for proper line-breaking and spacing
- this.column_ = 0;
- this.row_ = BOTTOM_ROW;
- // This variable holds currently-applied formatting
- this.formatting_ = [];
- };
- /**
- * Sets up control code and related constants for this instance
- */
- Cea608Stream.prototype.setConstants = function() {
- // The following attributes have these uses:
- // ext_ : char0 for mid-row codes, and the base for extended
- // chars (ext_+0, ext_+1, and ext_+2 are char0s for
- // extended codes)
- // control_: char0 for control codes, except byte-shifted to the
- // left so that we can do this.control_ | CONTROL_CODE
- // offset_: char0 for tab offset codes
- //
- // It's also worth noting that control codes, and _only_ control codes,
- // differ between field 1 and field2. Field 2 control codes are always
- // their field 1 value plus 1. That's why there's the "| field" on the
- // control value.
- if (this.dataChannel_ === 0) {
- this.BASE_ = 0x10;
- this.EXT_ = 0x11;
- this.CONTROL_ = (0x14 | this.field_) << 8;
- this.OFFSET_ = 0x17;
- } else if (this.dataChannel_ === 1) {
- this.BASE_ = 0x18;
- this.EXT_ = 0x19;
- this.CONTROL_ = (0x1c | this.field_) << 8;
- this.OFFSET_ = 0x1f;
- }
- // Constants for the LSByte command codes recognized by Cea608Stream. This
- // list is not exhaustive. For a more comprehensive listing and semantics see
- // http://www.gpo.gov/fdsys/pkg/CFR-2010-title47-vol1/pdf/CFR-2010-title47-vol1-sec15-119.pdf
- // Padding
- this.PADDING_ = 0x0000;
- // Pop-on Mode
- this.RESUME_CAPTION_LOADING_ = this.CONTROL_ | 0x20;
- this.END_OF_CAPTION_ = this.CONTROL_ | 0x2f;
- // Roll-up Mode
- this.ROLL_UP_2_ROWS_ = this.CONTROL_ | 0x25;
- this.ROLL_UP_3_ROWS_ = this.CONTROL_ | 0x26;
- this.ROLL_UP_4_ROWS_ = this.CONTROL_ | 0x27;
- this.CARRIAGE_RETURN_ = this.CONTROL_ | 0x2d;
- // paint-on mode (not supported)
- this.RESUME_DIRECT_CAPTIONING_ = this.CONTROL_ | 0x29;
- // Erasure
- this.BACKSPACE_ = this.CONTROL_ | 0x21;
- this.ERASE_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2c;
- this.ERASE_NON_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2e;
- };
- /**
- * Detects if the 2-byte packet data is a special character
- *
- * Special characters have a second byte in the range 0x30 to 0x3f,
- * with the first byte being 0x11 (for data channel 1) or 0x19 (for
- * data channel 2).
- *
- * @param {Integer} char0 The first byte
- * @param {Integer} char1 The second byte
- * @return {Boolean} Whether the 2 bytes are an special character
- */
- Cea608Stream.prototype.isSpecialCharacter = function(char0, char1) {
- return (char0 === this.EXT_ && char1 >= 0x30 && char1 <= 0x3f);
- };
- /**
- * Detects if the 2-byte packet data is an extended character
- *
- * Extended characters have a second byte in the range 0x20 to 0x3f,
- * with the first byte being 0x12 or 0x13 (for data channel 1) or
- * 0x1a or 0x1b (for data channel 2).
- *
- * @param {Integer} char0 The first byte
- * @param {Integer} char1 The second byte
- * @return {Boolean} Whether the 2 bytes are an extended character
- */
- Cea608Stream.prototype.isExtCharacter = function(char0, char1) {
- return ((char0 === (this.EXT_ + 1) || char0 === (this.EXT_ + 2)) &&
- (char1 >= 0x20 && char1 <= 0x3f));
- };
- /**
- * Detects if the 2-byte packet is a mid-row code
- *
- * Mid-row codes have a second byte in the range 0x20 to 0x2f, with
- * the first byte being 0x11 (for data channel 1) or 0x19 (for data
- * channel 2).
- *
- * @param {Integer} char0 The first byte
- * @param {Integer} char1 The second byte
- * @return {Boolean} Whether the 2 bytes are a mid-row code
- */
- Cea608Stream.prototype.isMidRowCode = function(char0, char1) {
- return (char0 === this.EXT_ && (char1 >= 0x20 && char1 <= 0x2f));
- };
- /**
- * Detects if the 2-byte packet is an offset control code
- *
- * Offset control codes have a second byte in the range 0x21 to 0x23,
- * with the first byte being 0x17 (for data channel 1) or 0x1f (for
- * data channel 2).
- *
- * @param {Integer} char0 The first byte
- * @param {Integer} char1 The second byte
- * @return {Boolean} Whether the 2 bytes are an offset control code
- */
- Cea608Stream.prototype.isOffsetControlCode = function(char0, char1) {
- return (char0 === this.OFFSET_ && (char1 >= 0x21 && char1 <= 0x23));
- };
- /**
- * Detects if the 2-byte packet is a Preamble Address Code
- *
- * PACs have a first byte in the range 0x10 to 0x17 (for data channel 1)
- * or 0x18 to 0x1f (for data channel 2), with the second byte in the
- * range 0x40 to 0x7f.
- *
- * @param {Integer} char0 The first byte
- * @param {Integer} char1 The second byte
- * @return {Boolean} Whether the 2 bytes are a PAC
- */
- Cea608Stream.prototype.isPAC = function(char0, char1) {
- return (char0 >= this.BASE_ && char0 < (this.BASE_ + 8) &&
- (char1 >= 0x40 && char1 <= 0x7f));
- };
- /**
- * Detects if a packet's second byte is in the range of a PAC color code
- *
- * PAC color codes have the second byte be in the range 0x40 to 0x4f, or
- * 0x60 to 0x6f.
- *
- * @param {Integer} char1 The second byte
- * @return {Boolean} Whether the byte is a color PAC
- */
- Cea608Stream.prototype.isColorPAC = function(char1) {
- return ((char1 >= 0x40 && char1 <= 0x4f) || (char1 >= 0x60 && char1 <= 0x7f));
- };
- /**
- * Detects if a single byte is in the range of a normal character
- *
- * Normal text bytes are in the range 0x20 to 0x7f.
- *
- * @param {Integer} char The byte
- * @return {Boolean} Whether the byte is a normal character
- */
- Cea608Stream.prototype.isNormalChar = function(char) {
- return (char >= 0x20 && char <= 0x7f);
- };
- // Adds the opening HTML tag for the passed character to the caption text,
- // and keeps track of it for later closing
- Cea608Stream.prototype.addFormatting = function(pts, format) {
- this.formatting_ = this.formatting_.concat(format);
- var text = format.reduce(function(text, format) {
- return text + '<' + format + '>';
- }, '');
- this[this.mode_](pts, text);
- };
- // Adds HTML closing tags for current formatting to caption text and
- // clears remembered formatting
- Cea608Stream.prototype.clearFormatting = function(pts) {
- if (!this.formatting_.length) {
- return;
- }
- var text = this.formatting_.reverse().reduce(function(text, format) {
- return text + '</' + format + '>';
- }, '');
- this.formatting_ = [];
- this[this.mode_](pts, text);
- };
- // Mode Implementations
- Cea608Stream.prototype.popOn = function(pts, text) {
- var baseRow = this.nonDisplayed_[this.row_];
- // buffer characters
- baseRow += text;
- this.nonDisplayed_[this.row_] = baseRow;
- };
- Cea608Stream.prototype.rollUp = function(pts, text) {
- var baseRow = this.displayed_[BOTTOM_ROW];
- baseRow += text;
- this.displayed_[BOTTOM_ROW] = baseRow;
- };
- Cea608Stream.prototype.shiftRowsUp_ = function() {
- var i;
- // clear out inactive rows
- for (i = 0; i < this.topRow_; i++) {
- this.displayed_[i] = '';
- }
- // shift displayed rows up
- for (i = this.topRow_; i < BOTTOM_ROW; i++) {
- this.displayed_[i] = this.displayed_[i + 1];
- }
- // clear out the bottom row
- this.displayed_[BOTTOM_ROW] = '';
- };
- // paintOn mode is not implemented
- Cea608Stream.prototype.paintOn = function() {};
- // exports
- module.exports = {
- CaptionStream: CaptionStream,
- Cea608Stream: Cea608Stream
- };
|