asd/asd-pc/node_modules/mux.js/test/caption-stream.test.js
愉快的大福 eb8378e551 init
2024-11-21 11:06:22 +08:00

3381 lines
110 KiB
JavaScript

'use strict';
var segments = require('data-files!segments');
var
window = require('global/window'),
captionStream,
m2ts = require('../lib/m2ts'),
mp4 = require('../lib/mp4'),
QUnit = require('qunit'),
seiNalUnitGenerator = require('./utils/sei-nal-unit-generator'),
makeSeiFromCaptionPacket = seiNalUnitGenerator.makeSeiFromCaptionPacket,
makeSeiFromMultipleCaptionPackets = seiNalUnitGenerator.makeSeiFromMultipleCaptionPackets,
characters = seiNalUnitGenerator.characters,
packetHeader708 = seiNalUnitGenerator.packetHeader708,
displayWindows708 = seiNalUnitGenerator.displayWindows708,
cc708PinkUnderscore = require('./utils/cc708-pink-underscore'),
cc708Korean = require('./utils/cc708-korean'),
sintelCaptions = segments['sintel-captions.ts'](),
mixed608708Captions = require('./utils/mixed-608-708-captions.js'),
multiChannel608Captions = segments['multi-channel-608-captions.ts']();
QUnit.module('Caption Stream', {
beforeEach: function() {
captionStream = new m2ts.CaptionStream();
}
});
QUnit.test('parses SEIs messages larger than 255 bytes', function(assert) {
var packets = [], data;
captionStream.ccStreams_[0].push = function(packet) {
packets.push(packet);
};
// set data channel 1 active for field 1
captionStream.activeCea608Channel_[0] = 0;
data = new Uint8Array(268);
data[0] = 0x04; // payload_type === user_data_registered_itu_t_t35
data[1] = 0xff; // payload_size
data[2] = 0x0d; // payload_size
data[3] = 181; // itu_t_t35_country_code
data[4] = 0x00;
data[5] = 0x31; // itu_t_t35_provider_code
data[6] = 0x47;
data[7] = 0x41;
data[8] = 0x39;
data[9] = 0x34; // user_identifier, "GA94"
data[10] = 0x03; // user_data_type_code, 0x03 is cc_data
data[11] = 0xc1; // process_cc_data, cc_count
data[12] = 0xff; // reserved
data[13] = 0xfc; // cc_valid, cc_type (608, field 1)
data[14] = 0xff; // cc_data_1 with parity bit set
data[15] = 0x0e; // cc_data_2 without parity bit set
data[16] = 0xff; // marker_bits
captionStream.push({
nalUnitType: 'sei_rbsp',
escapedRBSP: data
});
captionStream.flush();
assert.equal(packets.length, 1, 'parsed a caption');
});
QUnit.test('parses SEIs containing multiple messages', function(assert) {
var packets = [], data;
captionStream.ccStreams_[0].push = function(packet) {
packets.push(packet);
};
// set data channel 1 active for field 1
captionStream.activeCea608Channel_[0] = 0;
data = new Uint8Array(22);
data[0] = 0x01; // payload_type !== user_data_registered_itu_t_t35
data[1] = 0x04; // payload_size
data[6] = 0x04; // payload_type === user_data_registered_itu_t_t35
data[7] = 0x0d; // payload_size
data[8] = 181; // itu_t_t35_country_code
data[9] = 0x00;
data[10] = 0x31; // itu_t_t35_provider_code
data[11] = 0x47;
data[12] = 0x41;
data[13] = 0x39;
data[14] = 0x34; // user_identifier, "GA94"
data[15] = 0x03; // user_data_type_code, 0x03 is cc_data
data[16] = 0xc1; // process_cc_data, cc_count
data[17] = 0xff; // reserved
data[18] = 0xfc; // cc_valid, cc_type (608, field 1)
data[19] = 0xff; // cc_data_1 with parity bit set
data[20] = 0x0e; // cc_data_2 without parity bit set
data[21] = 0xff; // marker_bits
captionStream.push({
nalUnitType: 'sei_rbsp',
escapedRBSP: data
});
captionStream.flush();
assert.equal(packets.length, 1, 'parsed a caption');
});
QUnit.test('parses SEIs containing multiple messages of type user_data_registered_itu_t_t35', function(assert) {
var packets = [], data;
captionStream.ccStreams_[0].push = function(packet) {
packets.push(packet);
};
// set data channel 1 active for field 1
captionStream.activeCea608Channel_[0] = 0;
data = new Uint8Array(33);
data[0] = 0x01; // payload_type !== user_data_registered_itu_t_t35
data[1] = 0x04; // payload_size
// https://www.etsi.org/deliver/etsi_ts/101100_101199/101154/01.11.01_60/ts_101154v011101p.pdf#page=117
data[6] = 0x04; // payload_type === user_data_registered_itu_t_t35
data[7] = 0x09; // payload_size
data[8] = 181; // itu_t_t35_country_code
data[9] = 0x00;
data[10] = 0x31; // itu_t_t35_provider_code
data[11] = 0x44;
data[12] = 0x54;
data[13] = 0x47;
data[14] = 0x31; // user_identifier, "DTG1"
data[15] = 0x11; // zero_bit (b7), active_format_flag (b6), reserved (b5-b0)
data[16] = 0xF0; // reserved (b7-b4), active_format (b3-b0)
data[17] = 0x04; // payload_type === user_data_registered_itu_t_t35
data[18] = 0x0d; // payload_size
data[19] = 181; // itu_t_t35_country_code
data[20] = 0x00;
data[21] = 0x31; // itu_t_t35_provider_code
data[22] = 0x47;
data[23] = 0x41;
data[24] = 0x39;
data[25] = 0x34; // user_identifier, "GA94"
data[26] = 0x03; // user_data_type_code, 0x03 is cc_data
data[27] = 0xc1; // process_cc_data, cc_count
data[28] = 0xff; // reserved
data[29] = 0xfc; // cc_valid, cc_type (608, field 1)
data[30] = 0xff; // cc_data_1 with parity bit set
data[31] = 0x0e; // cc_data_2 without parity bit set
data[32] = 0xff; // marker_bits
captionStream.push({
nalUnitType: 'sei_rbsp',
escapedRBSP: data
});
captionStream.flush();
assert.equal(packets.length, 1, 'ignored DTG1 payload, parsed a GA94 caption');
});
QUnit.test('does not throw error if only invalid payloads', function(assert) {
var packets = [], data;
captionStream.ccStreams_[0].push = function(packet) {
packets.push(packet);
};
// set data channel 1 active for field 1
captionStream.activeCea608Channel_[0] = 0;
data = new Uint8Array(33);
data[0] = 0x01; // payload_type !== user_data_registered_itu_t_t35
data[1] = 0x04; // payload_size
// https://www.etsi.org/deliver/etsi_ts/101100_101199/101154/01.11.01_60/ts_101154v011101p.pdf#page=117
data[6] = 0x04; // payload_type === user_data_registered_itu_t_t35
data[7] = 0x09; // payload_size
data[8] = 181; // itu_t_t35_country_code
data[9] = 0x00;
data[10] = 0x31; // itu_t_t35_provider_code
data[11] = 0x44;
data[12] = 0x54;
data[13] = 0x47;
data[14] = 0x31; // user_identifier, "DTG1"
data[15] = 0x11; // zero_bit (b7), active_format_flag (b6), reserved (b5-b0)
data[16] = 0xF0; // reserved (b7-b4), active_format (b3-b0)
data[17] = 0x04; // payload_type === user_data_registered_itu_t_t35
data[18] = 0x0d; // payload_size
data[19] = 181; // itu_t_t35_country_code
data[20] = 0x00;
data[21] = 0x31; // itu_t_t35_provider_code
data[22] = 0x47;
data[23] = 0x41;
data[24] = 0x39;
captionStream.push({
nalUnitType: 'sei_rbsp',
escapedRBSP: data
});
captionStream.flush();
assert.equal(packets.length, 0, 'ignored DTG1 payload');
});
QUnit.test('ignores SEIs that do not have type user_data_registered_itu_t_t35', function(assert) {
var captions = [];
captionStream.on('data', function(caption) {
captions.push(caption);
});
captionStream.push({
nalUnitType: 'sei_rbsp',
escapedRBSP: new Uint8Array([
0x05 // payload_type !== user_data_registered_itu_t_t35
])
});
assert.equal(captions.length, 0, 'ignored the unknown payload type');
});
QUnit.test('parses a minimal example of caption data', function(assert) {
var packets = [];
captionStream.ccStreams_[0].push = function(packet) {
packets.push(packet);
};
// set data channel 1 active for field 1
captionStream.activeCea608Channel_[0] = 0;
captionStream.push({
nalUnitType: 'sei_rbsp',
escapedRBSP: new Uint8Array([
0x04, // payload_type === user_data_registered_itu_t_t35
0x0d, // payload_size
181, // itu_t_t35_country_code
0x00, 0x31, // itu_t_t35_provider_code
0x47, 0x41, 0x39, 0x34, // user_identifier, "GA94"
0x03, // user_data_type_code, 0x03 is cc_data
// 110 00001
0xc1, // process_cc_data, cc_count
0xff, // reserved
// 1111 1100
0xfc, // cc_valid, cc_type (608, field 1)
0xff, // cc_data_1 with parity bit set
0x0e, // cc_data_2 without parity bit set
0xff // marker_bits
])
});
captionStream.flush();
assert.equal(packets.length, 1, 'parsed a caption packet');
});
QUnit.test('can be parsed from a segment', function(assert) {
var transmuxer = new mp4.Transmuxer(),
captions = [];
// Setting the BMDT to ensure that captions and id3 tags are not
// time-shifted by this value when they are output and instead are
// zero-based
transmuxer.setBaseMediaDecodeTime(100000);
transmuxer.on('data', function(data) {
if (data.captions) {
captions = captions.concat(data.captions);
}
});
transmuxer.push(sintelCaptions);
transmuxer.flush();
assert.equal(captions.length, 2, 'parsed two captions');
assert.equal(captions[0].content[0].text.indexOf('ASUKA'), 0, 'parsed the start of the first caption');
assert.ok(captions[0].content[0].text.indexOf('Japanese') > 0, 'parsed the end of the first caption');
assert.equal(captions[0].startTime, 1, 'parsed the start time');
assert.equal(captions[0].endTime, 4, 'parsed the end time');
});
QUnit.test('dispatches caption track information', function(assert) {
var transmuxer = new mp4.Transmuxer(),
captions = [],
captionStreams = {};
// Setting the BMDT to ensure that captions and id3 tags are not
// time-shifted by this value when they are output and instead are
// zero-based
transmuxer.setBaseMediaDecodeTime(100000);
transmuxer.on('data', function(data) {
if (data.captions) {
captions = captions.concat(data.captions);
for (var trackId in data.captionStreams) {
captionStreams[trackId] = true;
}
}
});
transmuxer.push(multiChannel608Captions);
transmuxer.flush();
assert.deepEqual(captionStreams, {CC1: true, CC3: true}, 'found captions in CC1 and CC3');
assert.equal(captions.length, 4, 'parsed eight captions');
assert.equal(captions[0].content[0].text, 'être une période de questions', 'parsed the text of the first caption in CC3');
assert.equal(captions[1].content[0].text, 'PERIOD, FOLKS.', 'parsed the text of the first caption in CC1');
});
QUnit.test('sorting is fun', function(assert) {
var packets, captions, seiNals;
packets = [
// Send another command so that the second EOC isn't ignored
{ pts: 10 * 1000, ccData: 0x1420, type: 0 },
// RCL, resume caption loading
{ pts: 1000, ccData: 0x1420, type: 0 },
// 'test string #1'
{ pts: 1000, ccData: characters('te'), type: 0 },
{ pts: 1000, ccData: characters('st'), type: 0 },
{ pts: 1000, ccData: characters(' s'), type: 0 },
// 'test string #2'
{ pts: 10 * 1000, ccData: characters('te'), type: 0 },
{ pts: 10 * 1000, ccData: characters('st'), type: 0 },
{ pts: 10 * 1000, ccData: characters(' s'), type: 0 },
// 'test string #1' continued
{ pts: 1000, ccData: characters('tr'), type: 0 },
{ pts: 1000, ccData: characters('in'), type: 0 },
{ pts: 1000, ccData: characters('g '), type: 0 },
{ pts: 1000, ccData: characters('#1'), type: 0 },
// 'test string #2' continued
{ pts: 10 * 1000, ccData: characters('tr'), type: 0 },
{ pts: 10 * 1000, ccData: characters('in'), type: 0 },
{ pts: 10 * 1000, ccData: characters('g '), type: 0 },
{ pts: 10 * 1000, ccData: characters('#2'), type: 0 },
// EOC, End of Caption. End display
{ pts: 10 * 1000, ccData: 0x142f, type: 0 },
// EOC, End of Caption. Finished transmitting, begin display
{ pts: 1000, ccData: 0x142f, type: 0 },
// Send another command so that the second EOC isn't ignored
{ pts: 20 * 1000, ccData: 0x1420, type: 0 },
// EOC, End of Caption. End display
{ pts: 20 * 1000, ccData: 0x142f, type: 0 }
];
captions = [];
seiNals = packets.map(makeSeiFromCaptionPacket);
captionStream.on('data', function(caption) {
captions.push(caption);
});
seiNals.forEach(captionStream.push, captionStream);
captionStream.flush();
assert.equal(captions.length, 2, 'detected two captions');
assert.equal(captions[0].content[0].text, 'test string #1', 'parsed caption 1');
assert.equal(captions[1].content[0].text, 'test string #2', 'parsed caption 2');
});
QUnit.test('drops duplicate segments', function(assert) {
var packets, captions, seiNals;
packets = [
{
pts: 1000, dts: 1000, captions: [
{ccData: 0x1420, type: 0 }, // RCL (resume caption loading)
{ccData: 0x1420, type: 0 }, // RCL, duplicate as per spec
{ccData: characters('te'), type: 0 },
{ccData: characters('st'), type: 0 }
]
},
{
pts: 2000, dts: 2000, captions: [
{ccData: characters(' s'), type: 0 },
{ccData: characters('tr'), type: 0 },
{ccData: characters('in'), type: 0 }
]
},
{
pts: 3000, dts: 3000, captions: [
{ccData: characters('g '), type: 0 },
{ccData: characters('da'), type: 0 },
{ccData: characters('ta'), type: 0 }
]
},
{
pts: 2000, dts: 2000, captions: [
{ccData: characters(' s'), type: 0 },
{ccData: characters('tr'), type: 0 },
{ccData: characters('in'), type: 0 }
]
},
{
pts: 3000, dts: 3000, captions: [
{ccData: characters('g '), type: 0 },
{ccData: characters('da'), type: 0 },
{ccData: characters('ta'), type: 0 }
]
},
{
pts: 4000, dts: 4000, captions: [
{ccData: 0x142f, type: 0 }, // EOC (end of caption), mark display start
{ccData: 0x142f, type: 0 }, // EOC, duplicate as per spec
{ccData: 0x142f, type: 0 }, // EOC, mark display end and flush
{ccData: 0x142f, type: 0 } // EOC, duplicate as per spec
]
}
];
captions = [];
seiNals = packets.map(makeSeiFromMultipleCaptionPackets);
captionStream.on('data', function(caption) {
captions.push(caption);
});
seiNals.forEach(captionStream.push, captionStream);
captionStream.flush();
assert.equal(captions.length, 1, 'detected one caption');
assert.equal(captions[0].content[0].text, 'test string data', 'parsed caption properly');
});
QUnit.test('drops duplicate segments with multi-segment DTS values', function(assert) {
var packets, captions, seiNals;
packets = [
{
pts: 1000, dts: 1000, captions: [
{ccData: 0x1420, type: 0 }, // RCL (resume caption loading)
{ccData: 0x1420, type: 0 }, // RCL, duplicate as per spec
{ccData: characters('te'), type: 0 }
]
},
{
pts: 2000, dts: 2000, captions: [
{ccData: characters('st'), type: 0 },
{ccData: characters(' s'), type: 0 }
]
},
{
pts: 2000, dts: 2000, captions: [
{ccData: characters('tr'), type: 0 },
{ccData: characters('in'), type: 0 }
]
},
{
pts: 3000, dts: 3000, captions: [
{ccData: characters('g '), type: 0 },
{ccData: characters('da'), type: 0 },
{ccData: characters('ta'), type: 0 }
]
},
{
pts: 2000, dts: 2000, captions: [
{ccData: characters(' s'), type: 0 },
{ccData: characters('tr'), type: 0 },
{ccData: characters('in'), type: 0 }
]
},
{
pts: 3000, dts: 3000, captions: [
{ccData: characters('g '), type: 0 },
{ccData: characters('da'), type: 0 },
{ccData: characters('ta'), type: 0 }
]
},
{
pts: 3000, dts: 3000, captions: [
{ccData: characters(' s'), type: 0 },
{ccData: characters('tu'), type: 0 },
{ccData: characters('ff'), type: 0 }
]
},
{
pts: 4000, dts: 4000, captions: [
{ccData: 0x142f, type: 0 }, // EOC (end of caption)
// EOC not duplicated for robustness testing
{ccData: 0x1420, type: 0 } // RCL (resume caption loading)
]
},
{
pts: 5000, dts: 5000, captions: [
{ccData: 0x1420, type: 0 }, // RCL, duplicated as per spec
{ccData: characters(' a'), type: 0 },
{ccData: characters('nd'), type: 0 }
]
},
{
pts: 6000, dts: 6000, captions: [
{ccData: characters(' e'), type: 0 },
{ccData: characters('ve'), type: 0 }
]
},
{
pts: 6000, dts: 6000, captions: [
{ccData: characters('n '), type: 0 },
{ccData: characters('mo'), type: 0 }
]
},
{
pts: 6000, dts: 6000, captions: [
{ccData: characters('re'), type: 0 },
{ccData: characters(' t'), type: 0 }
]
},
{
pts: 5000, dts: 5000, captions: [
{ccData: 0x1420, type: 0 }, // RCL, duplicated as per spec
{ccData: characters(' a'), type: 0 },
{ccData: characters('nd'), type: 0 }
]
},
{
pts: 6000, dts: 6000, captions: [
{ccData: characters(' e'), type: 0 },
{ccData: characters('ve'), type: 0 }
]
},
{
pts: 6000, dts: 6000, captions: [
{ccData: characters('n '), type: 0 },
{ccData: characters('mo'), type: 0 }
]
},
{
pts: 6000, dts: 6000, captions: [
{ccData: characters('re'), type: 0 },
{ccData: characters(' t'), type: 0 }
]
},
{
pts: 6000, dts: 6000, captions: [
{ccData: characters('ex'), type: 0 },
{ccData: characters('t '), type: 0 }
]
},
{
pts: 6000, dts: 6000, captions: [
{ccData: characters('da'), type: 0 },
{ccData: characters('ta'), type: 0 }
]
},
{
pts: 7000, dts: 7000, captions: [
{ccData: characters(' h'), type: 0 },
{ccData: characters('er'), type: 0 }
]
},
{
pts: 8000, dts: 8000, captions: [
{ccData: characters('e!'), type: 0 },
{ccData: 0x142f, type: 0 }, // EOC (end of caption), mark display start
{ccData: 0x142f, type: 0 }, // EOC, duplicated as per spec
{ccData: 0x142f, type: 0 } // EOC, mark display end and flush
// EOC not duplicated for robustness testing
]
}
];
captions = [];
seiNals = packets.map(makeSeiFromMultipleCaptionPackets);
captionStream.on('data', function(caption) {
captions.push(caption);
});
seiNals.forEach(captionStream.push, captionStream);
captionStream.flush();
assert.equal(captions.length, 2, 'detected two captions');
assert.equal(captions[0].content[0].text, 'test string data stuff', 'parsed caption properly');
assert.equal(captions[1].content[0].text, 'and even more text data here!', 'parsed caption properly');
});
QUnit.test('doesn\'t ignore older segments if reset', function(assert) {
var firstPackets, secondPackets, captions, seiNals1, seiNals2;
firstPackets = [
{
pts: 11000, dts: 11000, captions: [
{ccData: 0x1420, type: 0 }, // RCL (resume caption loading)
{ccData: 0x1420, type: 0 }, // RCL, duplicated as per spec
{ccData: characters('te'), type: 0 }
]
},
{
pts: 12000, dts: 12000, captions: [
{ccData: characters('st'), type: 0 },
{ccData: characters(' s'), type: 0 }
]
},
{
pts: 12000, dts: 12000, captions: [
{ccData: characters('tr'), type: 0 },
{ccData: characters('in'), type: 0 }
]
},
{
pts: 13000, dts: 13000, captions: [
{ccData: characters('g '), type: 0 },
{ccData: characters('da'), type: 0 },
{ccData: characters('ta'), type: 0 }
]
}
];
secondPackets = [
{
pts: 1000, dts: 1000, captions: [
{ccData: 0x1420, type: 0 }, // RCL (resume caption loading)
{ccData: 0x1420, type: 0 }, // RCL, duplicated as per spec
{ccData: characters('af'), type: 0 }
]
},
{
pts: 2000, dts: 2000, captions: [
{ccData: characters('te'), type: 0 },
{ccData: characters('r '), type: 0 },
{ccData: characters('re'), type: 0 }
]
},
{
pts: 3000, dts: 3000, captions: [
{ccData: characters('se'), type: 0 },
{ccData: characters('t '), type: 0 },
{ccData: characters('da'), type: 0 }
]
},
{
pts: 3000, dts: 3000, captions: [
{ccData: characters('ta'), type: 0 },
{ccData: characters('!!'), type: 0 }
]
},
{
pts: 4000, dts: 4000, captions: [
{ccData: 0x142f, type: 0 }, // EOC (end of caption), mark display start
{ccData: 0x142f, type: 0 }, // EOC, duplicated as per spec
{ccData: 0x142f, type: 0 } // EOC, mark display end and flush
// EOC not duplicated for robustness testing
]
}
];
captions = [];
seiNals1 = firstPackets.map(makeSeiFromMultipleCaptionPackets);
seiNals2 = secondPackets.map(makeSeiFromMultipleCaptionPackets);
captionStream.on('data', function(caption) {
captions.push(caption);
});
seiNals1.forEach(captionStream.push, captionStream);
captionStream.flush();
assert.equal(captionStream.latestDts_, 13000, 'DTS is tracked correctly');
captionStream.reset();
assert.equal(captionStream.latestDts_, null, 'DTS tracking was reset');
seiNals2.forEach(captionStream.push, captionStream);
captionStream.flush();
assert.equal(captionStream.latestDts_, 4000, 'DTS is tracked correctly');
assert.equal(captions.length, 1, 'detected one caption');
assert.equal(captions[0].content[0].text, 'after reset data!!', 'parsed caption properly');
});
QUnit.test('extracts all theoretical caption channels', function(assert) {
var captions = [];
captionStream.ccStreams_.forEach(function(cc) {
cc.on('data', function(caption) {
captions.push(caption);
});
});
// RU2 = roll-up, 2 rows
// CR = carriage return
var packets = [
{ pts: 1000, type: 0, ccData: 0x1425 }, // RU2 (sets CC1)
{ pts: 2000, type: 0, ccData: characters('1a') }, // CC1
{ pts: 3000, type: 0, ccData: 0x1c25 }, // RU2 (sets CC2)
{ pts: 4000, type: 1, ccData: 0x1525 }, // RU2 (sets CC3)
{ pts: 5000, type: 1, ccData: characters('3a') }, // CC3
// this next one tests if active channel is tracked per-field
// instead of globally
{ pts: 6000, type: 0, ccData: characters('2a') }, // CC2
{ pts: 7000, type: 1, ccData: 0x1d25 }, // RU2 (sets CC4)
{ pts: 8000, type: 1, ccData: characters('4a') }, // CC4
{ pts: 9000, type: 1, ccData: characters('4b') }, // CC4
{ pts: 10000, type: 0, ccData: 0x142d }, // CR (sets + flushes CC1)
{ pts: 11000, type: 0, ccData: 0x1c2d }, // CR (sets + flushes CC2)
{ pts: 12000, type: 0, ccData: 0x1425 }, // RU2 (sets CC1)
{ pts: 13000, type: 0, ccData: characters('1b') }, // CC1
{ pts: 14000, type: 0, ccData: characters('1c') }, // CC1
{ pts: 15000, type: 0, ccData: 0x142d }, // CR (sets + flushes CC1)
{ pts: 16000, type: 1, ccData: 0x152d }, // CR (sets + flushes CC3)
{ pts: 17000, type: 1, ccData: 0x1d2d }, // CR (sets + flushes CC4)
{ pts: 18000, type: 0, ccData: 0x1c25 }, // RU2 (sets CC2)
{ pts: 19000, type: 0, ccData: characters('2b') }, // CC2
{ pts: 20000, type: 0, ccData: 0x1c2d } // CR (sets + flushes CC2)
];
var seiNals = packets.map(makeSeiFromCaptionPacket);
seiNals.forEach(captionStream.push, captionStream);
captionStream.flush();
assert.equal(captions.length, 6, 'got all captions');
assert.equal(captions[0].content[0].text, '1a', 'cc1 first row');
assert.equal(captions[1].content[0].text, '2a', 'cc2 first row');
assert.equal(captions[2].content[0].text, '1a', 'cc1 first row');
assert.equal(captions[2].content[1].text, '1b1c', 'cc1 second row');
assert.equal(captions[3].content[0].text, '3a', 'cc3 first row');
assert.equal(captions[4].content[0].text, '4a4b', 'cc4 first row');
assert.equal(captions[5].content[0].text, '2a', 'cc2 first row');
assert.equal(captions[5].content[1].text, '2b', 'cc2 second row');
});
QUnit.test('drops data until first command that sets activeChannel for a field', function(assert) {
var captions = [];
captionStream.ccStreams_.forEach(function(cc) {
cc.on('data', function(caption) {
captions.push(caption);
});
});
var packets = [
// test that packets in same field and same data channel are dropped
// before a control code that sets the data channel
{ pts: 0 * 1000, ccData: characters('no'), type: 0 },
{ pts: 0 * 1000, ccData: characters('t '), type: 0 },
{ pts: 0 * 1000, ccData: characters('th'), type: 0 },
{ pts: 0 * 1000, ccData: characters('is'), type: 0 },
// EOC (end of caption), sets CC1
{ pts: 1 * 1000, ccData: 0x142f, type: 0 },
// RCL (resume caption loading)
{ pts: 1 * 1000, ccData: 0x1420, type: 0 },
// EOC, if data wasn't dropped this would dispatch a caption
{ pts: 2 * 1000, ccData: 0x142f, type: 0 },
// RCL
{ pts: 3 * 1000, ccData: 0x1420, type: 0 },
{ pts: 4 * 1000, ccData: characters('fi'), type: 0 },
{ pts: 4 * 1000, ccData: characters('el'), type: 0 },
{ pts: 4 * 1000, ccData: characters('d0'), type: 0 },
// EOC, mark display start
{ pts: 5 * 1000, ccData: 0x142f, type: 0 },
// EOC, duplicated as per spec
{ pts: 5 * 1000, ccData: 0x142f, type: 0 },
// EOC, mark display end and flush
{ pts: 6 * 1000, ccData: 0x142f, type: 0 },
// EOC not duplicated cuz not necessary
// now switch to field 1 and test that packets in the same field
// but DIFFERENT data channel are dropped
{ pts: 7 * 1000, ccData: characters('or'), type: 1 },
{ pts: 7 * 1000, ccData: characters(' t'), type: 1 },
{ pts: 7 * 1000, ccData: characters('hi'), type: 1 },
{ pts: 7 * 1000, ccData: characters('s.'), type: 1 },
// EOC (end of caption, sets CC4)
{ pts: 8 * 1000, ccData: 0x1d2f, type: 1 },
// RCL (resume caption loading)
{ pts: 8 * 1000, ccData: 0x1d20, type: 1 },
// EOC, if data wasn't dropped this would dispatch a caption
{ pts: 9 * 1000, ccData: 0x1d2f, type: 1 },
// RCL
{ pts: 10 * 1000, ccData: 0x1d20, type: 1 },
{ pts: 11 * 1000, ccData: characters('fi'), type: 1 },
{ pts: 11 * 1000, ccData: characters('el'), type: 1 },
{ pts: 11 * 1000, ccData: characters('d1'), type: 1 },
// EOC, mark display start
{ pts: 12 * 1000, ccData: 0x1d2f, type: 1 },
// EOC, duplicated as per spec
{ pts: 12 * 1000, ccData: 0x1d2f, type: 1 },
// EOC, mark display end and flush
{ pts: 13 * 1000, ccData: 0x1d2f, type: 1 }
// EOC not duplicated cuz not necessary
];
var seiNals = packets.map(makeSeiFromCaptionPacket);
seiNals.forEach(captionStream.push, captionStream);
captionStream.flush();
assert.equal(captions.length, 2, 'received 2 captions');
assert.equal(captions[0].content[0].text, 'field0', 'received only confirmed field0 data');
assert.equal(captions[0].stream, 'CC1', 'caption went to right channel');
assert.equal(captions[1].content[0].text, 'field1', 'received only confirmed field1 data');
assert.equal(captions[1].stream, 'CC4', 'caption went to right channel');
});
QUnit.test('clears buffer and drops data until first command that sets activeChannel after reset', function(assert) {
var firstPackets, secondPackets, captions, seiNals1, seiNals2;
captions = [];
firstPackets = [
// RCL (resume caption loading), CC1
{ pts: 1 * 1000, ccData: 0x1420, type: 0 },
{ pts: 2 * 1000, ccData: characters('fi'), type: 0 },
{ pts: 2 * 1000, ccData: characters('el'), type: 0 },
{ pts: 2 * 1000, ccData: characters('d0'), type: 0 },
// EOC (end of caption), swap text to displayed memory
{ pts: 3 * 1000, ccData: 0x142f, type: 0 },
{ pts: 4 * 1000, ccData: characters('fi'), type: 0 },
{ pts: 4 * 1000, ccData: characters('el'), type: 0 },
{ pts: 4 * 1000, ccData: characters('d0'), type: 0 },
// RCL (resume caption loading), CC4
{ pts: 5 * 1000, ccData: 0x1d20, type: 1 },
{ pts: 6 * 1000, ccData: characters('fi'), type: 1 },
{ pts: 6 * 1000, ccData: characters('el'), type: 1 },
{ pts: 6 * 1000, ccData: characters('d1'), type: 1 },
// EOC (end of caption), swap text to displayed memory
{ pts: 7 * 1000, ccData: 0x1d2f, type: 1 },
{ pts: 8 * 1000, ccData: characters('fi'), type: 1 },
{ pts: 8 * 1000, ccData: characters('el'), type: 1 },
{ pts: 8 * 1000, ccData: characters('d1'), type: 1 }
];
secondPackets = [
// following packets are dropped
{ pts: 9 * 1000, ccData: characters('no'), type: 0 },
{ pts: 9 * 1000, ccData: characters('t '), type: 0 },
{ pts: 9 * 1000, ccData: characters('th'), type: 0 },
{ pts: 9 * 1000, ccData: characters('is'), type: 0 },
{ pts: 10 * 1000, ccData: characters('or'), type: 1 },
{ pts: 10 * 1000, ccData: characters(' t'), type: 1 },
{ pts: 10 * 1000, ccData: characters('hi'), type: 1 },
{ pts: 10 * 1000, ccData: characters('s.'), type: 1 },
// EOC (end of caption), sets CC1
{ pts: 11 * 1000, ccData: 0x142f, type: 0 },
// RCL (resume caption loading), CC1
{ pts: 11 * 1000, ccData: 0x1420, type: 0 },
// EOC, sets CC4
{ pts: 12 * 1000, ccData: 0x1d2f, type: 1 },
// RCL, CC4
{ pts: 12 * 1000, ccData: 0x1d20, type: 1 },
// EOC, CC1, would dispatch caption if packets weren't ignored
{ pts: 13 * 1000, ccData: 0x142f, type: 0 },
// RCL, CC1
{ pts: 13 * 1000, ccData: 0x1420, type: 0 },
// EOC, CC4, would dispatch caption if packets weren't ignored
{ pts: 14 * 1000, ccData: 0x1d2f, type: 1 },
// RCL, CC4
{ pts: 14 * 1000, ccData: 0x1d20, type: 1 },
{ pts: 18 * 1000, ccData: characters('bu'), type: 0 },
{ pts: 18 * 1000, ccData: characters('t '), type: 0 },
{ pts: 18 * 1000, ccData: characters('th'), type: 0 },
{ pts: 18 * 1000, ccData: characters('is'), type: 0 },
{ pts: 19 * 1000, ccData: characters('an'), type: 1 },
{ pts: 19 * 1000, ccData: characters('d '), type: 1 },
{ pts: 19 * 1000, ccData: characters('th'), type: 1 },
{ pts: 19 * 1000, ccData: characters('is'), type: 1 },
// EOC (end of caption), CC1, mark caption 1 start
{ pts: 20 * 1000, ccData: 0x142f, type: 0 },
// EOC, CC1, duplicated as per spec
{ pts: 20 * 1000, ccData: 0x142f, type: 0 },
// EOC, CC1, mark caption 1 end and dispatch
{ pts: 21 * 1000, ccData: 0x142f, type: 0 },
// No duplicate EOC cuz not necessary
// EOC, CC4, mark caption 2 start
{ pts: 22 * 1000, ccData: 0x1d2f, type: 1 },
// EOC, CC4, duplicated as per spec
{ pts: 22 * 1000, ccData: 0x1d2f, type: 1 },
// EOC, CC4, mark caption 2 end and dispatch
{ pts: 23 * 1000, ccData: 0x1d2f, type: 1 }
// No duplicate EOC cuz not necessary
];
seiNals1 = firstPackets.map(makeSeiFromCaptionPacket);
seiNals2 = secondPackets.map(makeSeiFromCaptionPacket);
captionStream.on('data', function(caption) {
captions.push(caption);
});
seiNals1.forEach(captionStream.push, captionStream);
captionStream.flush();
assert.equal(captionStream.ccStreams_[0].nonDisplayed_[14].text, 'field0',
'there is data in non-displayed memory for field 0 before reset');
assert.equal(captionStream.ccStreams_[3].nonDisplayed_[14].text, 'field1',
'there is data in non-displayed memory for field 1 before reset');
assert.equal(captionStream.ccStreams_[0].displayed_[14].text, 'field0',
'there is data in displayed memory for field 0 before reset');
assert.equal(captionStream.ccStreams_[3].displayed_[14].text, 'field1',
'there is data in displayed memory for field 1 before reset');
captionStream.reset();
assert.equal(captionStream.ccStreams_[0].nonDisplayed_[14].text, '',
'there is no data in non-displayed memory for field 0 after reset');
assert.equal(captionStream.ccStreams_[3].nonDisplayed_[14].text, '',
'there is no data in non-displayed memory for field 1 after reset');
assert.equal(captionStream.ccStreams_[0].displayed_[14].text, '',
'there is no data in displayed memory for field 0 after reset');
assert.equal(captionStream.ccStreams_[3].displayed_[14].text, '',
'there is no data in displayed memory for field 1 after reset');
seiNals2.forEach(captionStream.push, captionStream);
captionStream.flush();
assert.equal(captions.length, 2, 'detected two captions');
assert.equal(captions[0].content[0].text, 'but this', 'parsed caption properly');
assert.equal(captions[0].stream, 'CC1', 'caption went to right channel');
assert.equal(captions[1].content[0].text, 'and this', 'parsed caption properly');
assert.equal(captions[1].stream, 'CC4', 'caption went to right channel');
});
QUnit.test("don't mess up 608 captions when 708 are present", function(assert) {
var captions = [];
captionStream.ccStreams_.forEach(function(cc) {
cc.on('data', function(caption) {
captions.push(caption);
});
});
var seiNals = mixed608708Captions.map(makeSeiFromCaptionPacket);
seiNals.forEach(captionStream.push, captionStream);
captionStream.flush();
assert.equal(captions.length, 3, 'parsed three captions');
// first caption stream
assert.equal(captions[0].content[0].text, 'BUT IT\'S NOT SUFFERING', 'first stream: parsed first content text correctly');
assert.equal(captions[0].content[1].text, 'RIGHW.', 'first stream: parsed second content text correctly');
// there is also bad data in the captions, but the null ascii character is removed
// second caption stream
assert.equal(captions[1].content[0].text, 'IT\'S NOT A THREAT TO ANYBODY.', 'second stream: parsed content text correctly');
// third stream
assert.equal(captions[2].content[0].text, 'WE TRY NOT TO PUT AN ANIMAL DOWN', 'third stream: parsed first content text correctly');
assert.equal(captions[2].content[1].text, 'IF WE DON\'T HAVE TO.', 'third stream: parsed second content text correctly');
});
QUnit.test("both 608 and 708 captions are available by default", function(assert) {
var cc608 = [];
var cc708 = [];
captionStream.on('data', function(caption) {
if (caption.stream === 'CC1') {
cc608.push(caption);
} else {
cc708.push(caption);
}
});
var seiNals = mixed608708Captions.map(makeSeiFromCaptionPacket);
seiNals.forEach(captionStream.push, captionStream);
captionStream.flush();
assert.equal(cc608.length, 3, 'parsed three 608 cues');
assert.equal(cc708.length, 3, 'parsed three 708 cues');
});
QUnit.test("708 parsing can be turned off", function(assert) {
captionStream.reset();
captionStream = new m2ts.CaptionStream({
parse708captions: false
});
var cc608 = [];
var cc708 = [];
captionStream.on('data', function(caption) {
if (caption.stream === 'CC1') {
cc608.push(caption);
} else {
cc708.push(caption);
}
});
var seiNals = mixed608708Captions.map(makeSeiFromCaptionPacket);
seiNals.forEach(captionStream.push, captionStream);
captionStream.flush();
assert.equal(cc608.length, 3, 'parsed three 608 cues');
assert.equal(cc708.length, 0, 'did not parse any 708 cues');
});
QUnit.test('ignores XDS and Text packets', function(assert) {
var captions = [];
captionStream.on('data', function(caption) {
captions.push(caption);
});
[
// RCL, resume caption loading, CC3
{ pts: 1000, ccData: 0x1520, type: 1 },
{ pts: 2000, ccData: characters('hi'), type: 1 },
// EOC, End of Caption
{ pts: 3000, ccData: 0x152f, type: 1 },
// Send another command so that the second EOC isn't ignored
{ pts: 3000, ccData: 0x152f, type: 1 },
// EOC, End of Caption
{ pts: 4000, ccData: 0x152f, type: 1 },
// ENM, Erase Non-Displayed Memory
{ pts: 4000, ccData: 0x152e, type: 1 },
// RCL, resume caption loading, CC1
{ pts: 5000, ccData: 0x1420, type: 0 }
].map(makeSeiFromCaptionPacket).forEach(captionStream.push, captionStream);
captionStream.flush();
assert.equal(captionStream.activeCea608Channel_[0], 0, 'field 1: CC1 is active');
assert.equal(captionStream.activeCea608Channel_[1], 0, 'field 2: CC3 is active');
[
// TR, text restart, CC1
{ pts: 5000, ccData: 0x142a, type: 0 },
{ pts: 6000, ccData: characters('tx'), type: 0 }
].map(makeSeiFromCaptionPacket).forEach(captionStream.push, captionStream);
captionStream.flush();
assert.equal(captionStream.activeCea608Channel_[0], null, 'field 1: disabled');
assert.equal(captionStream.activeCea608Channel_[1], 0, 'field 2: CC3 is active');
[
// EOC, End of Caption
{ pts: 7000, ccData: 0x142f, type: 0 },
// Send another command so that the second EOC isn't ignored
{ pts: 7000, ccData: 0x142f, type: 0 },
// EOC, End of Caption
{ pts: 8000, ccData: 0x142f, type: 0 },
// RCL, resume caption loading, CC3
{ pts: 9000, ccData: 0x1520, type: 1 },
// XDS start, "current" class, program identification number type
{ pts: 10000, ccData: 0x0101, type: 1 },
{ pts: 11000, ccData: characters('oh'), type: 1 },
// XDS end
{ pts: 12000, ccData: 0x0f00, type: 1 }
].map(makeSeiFromCaptionPacket).forEach(captionStream.push, captionStream);
captionStream.flush();
assert.equal(captionStream.activeCea608Channel_[0], 0, 'field 1: CC1 is active');
assert.equal(captionStream.activeCea608Channel_[1], null, 'field 2: disabled');
[
// EOC, End of Caption
{ pts: 13000, ccData: 0x152f, type: 1 },
// Send another command so that the second EOC isn't ignored
{ pts: 13000, ccData: 0x152f, type: 1 }
].map(makeSeiFromCaptionPacket).forEach(captionStream.push, captionStream);
captionStream.flush();
assert.equal(captions.length, 1, 'only parsed real caption');
assert.equal(captions[0].content[0].text, 'hi', 'caption is correct');
});
// Full character translation tests are below for Cea608Stream, they just only
// test support for CC1. See those tests and the source code for more about the
// mechanics of special and extended characters.
QUnit.test('special and extended character codes work regardless of field and data channel', function(assert) {
var packets, seiNals, captions = [];
packets = [
// RU2 (roll-up, 2 rows), CC2
{ ccData: 0x1c25, type: 0 },
// ®
{ ccData: 0x1930, type: 0 },
// CR (carriage return), CC2, flush caption
{ ccData: 0x1c2d, type: 0 },
// RU2, CC3
{ ccData: 0x1525, type: 1 },
// "
{ ccData: 0x2200, type: 1 },
// «
{ ccData: 0x123e, type: 1 },
// CR, CC3, flush caption
{ ccData: 0x152d, type: 1 },
// RU2, CC4
{ ccData: 0x1d25, type: 1 },
// "
{ ccData: 0x2200, type: 1 },
// »
{ ccData: 0x1a3f, type: 1 },
// CR, CC4, flush caption
{ ccData: 0x1d2d, type: 1 }
];
captionStream.on('data', function(caption) {
captions.push(caption);
});
seiNals = packets.map(makeSeiFromCaptionPacket);
seiNals.forEach(captionStream.push, captionStream);
captionStream.flush();
assert.deepEqual(captions[0].content[0].text, String.fromCharCode(0xae), 'CC2 special character correct');
assert.deepEqual(captions[1].content[0].text, String.fromCharCode(0xab), 'CC3 extended character correct');
assert.deepEqual(captions[2].content[0].text, String.fromCharCode(0xbb), 'CC4 extended character correct');
});
QUnit.test('number of roll up rows takes precedence over base row command', function(assert) {
var captions = [];
var packets = [
// RU2 (roll-up, 2 rows), CC1
{ type: 0, ccData: 0x1425 },
// RU2, CC1
{ type: 0, ccData: 0x1425 },
// PAC: row 1 (sets base row to row 1)
{ type: 0, ccData: 0x1170 },
// PAC: row 1
{ type: 0, ccData: 0x1170 },
// -
{ type: 0, ccData: 0x2d00 },
// CR
{ type: 0, ccData: 0x14ad },
// CR
{ type: 0, ccData: 0x14ad },
// RU3 (roll-up, 3 rows), CC1
{ type: 0, ccData: 0x1426 },
// RU3, CC1
{ type: 0, ccData: 0x1426 },
// PAC, row 11
{ type: 0, ccData: 0x13d0 },
// PAC, row 11
{ type: 0, ccData: 0x13d0 },
// so
{ type: 0, ccData: 0x736f },
// CR
{ type: 0, ccData: 0x14ad },
// CR
{ type: 0, ccData: 0x14ad }
];
var seis;
captionStream.on('data', function(caption) {
captions.push(caption);
});
seis = packets.map(makeSeiFromCaptionPacket);
seis.forEach(captionStream.push, captionStream);
captionStream.flush();
assert.deepEqual(captions[0].content[0].text, '-', 'RU2 caption is correct');
assert.deepEqual(captions[1].content[0].text, '-', 'first RU3 caption is correct');
assert.deepEqual(captions[1].content[1].text, 'so', 'second RU3 caption is correct');
packets = [
// switching from row 11 to 0
// PAC: row 0 (sets base row to row 0)
{ type: 0, ccData: 0x1140 },
// PAC: row 0
{ type: 0, ccData: 0x1140 },
// CR
{ type: 0, ccData: 0x14ad },
// CR
{ type: 0, ccData: 0x14ad }
];
seis = packets.map(makeSeiFromCaptionPacket);
seis.forEach(captionStream.push, captionStream);
captionStream.flush();
assert.deepEqual(captions[2].content[0].text, '-', 'first RU3 caption is correct');
assert.deepEqual(captions[2].content[1].text, 'so', 'second RU3 caption is correct');
});
var cea608Stream;
QUnit.module('CEA 608 Stream', {
beforeEach: function() {
cea608Stream = new m2ts.Cea608Stream();
}
});
QUnit.skip('filters null data', function(assert) {
assert.ok(false, 'not implemented');
});
QUnit.skip('removes parity bits', function(assert) {
assert.ok(false, 'not implemented');
});
QUnit.test('converts non-ASCII character codes to ASCII', function(assert) {
var packets, captions;
packets = [
// RCL, resume caption loading
{ ccData: 0x1420, type: 0 },
// ASCII exceptions
{ ccData: 0x2a5c, type: 0 },
{ ccData: 0x5e5f, type: 0 },
{ ccData: 0x607b, type: 0 },
{ ccData: 0x7c7d, type: 0 },
{ ccData: 0x7e7f, type: 0 },
// EOC, End of Caption
{ pts: 1000, ccData: 0x142f, type: 0 },
// Send another command so that the second EOC isn't ignored
{ ccData: 0x1420, type: 0 },
// EOC, End of Caption, clear the display
{ pts: 10 * 1000, ccData: 0x142f, type: 0 }
];
captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
packets.forEach(cea608Stream.push, cea608Stream);
assert.equal(captions[0].content[0].text,
String.fromCharCode(0xe1, 0xe9, 0xed, 0xf3, 0xfa, 0xe7, 0xf7, 0xd1, 0xf1, 0x2588),
'translated non-standard characters');
});
QUnit.test('properly handles special character codes', function(assert) {
var packets, captions;
packets = [
// RCL, resume caption loading
{ ccData: 0x1420, type: 0 },
// Special characters as defined by CEA-608
// see the CHARACTER_TRANSLATION hash in lib/m2ts/caption-stream for the
// mapping table
{ ccData: 0x1130, type: 0 },
{ ccData: 0x1131, type: 0 },
{ ccData: 0x1132, type: 0 },
{ ccData: 0x1133, type: 0 },
{ ccData: 0x1134, type: 0 },
{ ccData: 0x1135, type: 0 },
{ ccData: 0x1136, type: 0 },
{ ccData: 0x1137, type: 0 },
{ ccData: 0x1138, type: 0 },
{ ccData: 0x1139, type: 0 },
{ ccData: 0x113a, type: 0 },
{ ccData: 0x113b, type: 0 },
{ ccData: 0x113c, type: 0 },
{ ccData: 0x113d, type: 0 },
{ ccData: 0x113e, type: 0 },
{ ccData: 0x113f, type: 0 },
// EOC, End of Caption
{ pts: 1000, ccData: 0x142f, type: 0 },
// Send another command so that the second EOC isn't ignored
{ ccData: 0x1420, type: 0 },
// EOC, End of Caption, CC1, clear the display
{ pts: 10 * 1000, ccData: 0x142f, type: 0 }
];
captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
packets.forEach(cea608Stream.push, cea608Stream);
assert.equal(captions[0].content[0].text,
String.fromCharCode(0xae, 0xb0, 0xbd, 0xbf, 0x2122, 0xa2, 0xa3, 0x266a,
0xe0, 0xa0, 0xe8, 0xe2, 0xea, 0xee, 0xf4, 0xfb),
'translated special characters');
});
QUnit.test('properly handles extended character codes', function(assert) {
var packets, captions;
packets = [
// RCL, resume caption loading
{ ccData: 0x1420, type: 0 },
// Extended characters are defined in CEA-608 as a standard character,
// which is followed by an extended character, and the standard character
// gets deleted.
// see the CHARACTER_TRANSLATION hash in lib/m2ts/caption-stream for the
// mapping table
{ ccData: 0x2200, type: 0 },
{ ccData: 0x123e, type: 0 },
{ ccData: 0x4c41, type: 0 },
{ ccData: 0x1230, type: 0 },
{ ccData: 0x2d4c, type: 0 },
{ ccData: 0x4100, type: 0 },
{ ccData: 0x1338, type: 0 },
{ ccData: 0x204c, type: 0 },
{ ccData: 0x417d, type: 0 },
{ ccData: 0x4400, type: 0 },
{ ccData: 0x1137, type: 0 },
{ ccData: 0x2200, type: 0 },
{ ccData: 0x123f, type: 0 },
// EOC, End of Caption
{ pts: 1000, ccData: 0x142f, type: 0 },
// Send another command so that the second EOC isn't ignored
{ ccData: 0x1420, type: 0 },
// EOC, End of Caption, clear the display
{ pts: 10 * 1000, ccData: 0x142f, type: 0 }
];
captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
packets.forEach(cea608Stream.push, cea608Stream);
assert.equal(captions[0].content[0].text, '«LÀ-LÅ LAÑD♪»',
'translated special characters');
});
QUnit.test('pop-on mode', function(assert) {
var packets, captions;
packets = [
// RCL, resume caption loading
{ ccData: 0x1420, type: 0 },
// 'hi'
{ ccData: characters('hi'), type: 0 },
// EOC, End of Caption. Finished transmitting, begin display
{ pts: 1000, ccData: 0x142f, type: 0 },
// Send another command so that the second EOC isn't ignored
{ ccData: 0x1420, type: 0 },
// EOC, End of Caption. End display
{ pts: 10 * 1000, ccData: 0x142f, type: 0 }
];
captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
packets.forEach(cea608Stream.push, cea608Stream);
assert.equal(captions.length, 1, 'detected a caption');
assert.deepEqual(captions[0], {
startPts: 1000,
endPts: 10 * 1000,
content: [{
line: 15,
position: 10,
text: 'hi'
}],
stream: 'CC1'
}, 'parsed the caption');
});
QUnit.test('ignores null characters', function(assert) {
var packets, captions;
packets = [
// RCL, resume caption loading
{ ccData: 0x1420, type: 0 },
// 'mu'
{ ccData: characters('mu'), type: 0 },
// null characters
{ ccData: 0x0000, type: 0 },
// ' x'
{ ccData: characters(' x'), type: 0 },
// EOC, End of Caption. Finished transmitting, begin display
{ pts: 1000, ccData: 0x142f, type: 0 },
// Send another command so that the second EOC isn't ignored
{ ccData: 0x1420, type: 0 },
// EOC, End of Caption. End display
{ pts: 10 * 1000, ccData: 0x142f, type: 0 }
];
captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
packets.forEach(cea608Stream.push, cea608Stream);
assert.equal(captions.length, 1, 'detected a caption');
assert.deepEqual(captions[0], {
startPts: 1000,
endPts: 10 * 1000,
content: [{
line: 15,
position: 10,
text: 'mu x'
}],
stream: 'CC1'
}, 'ignored null characters');
});
QUnit.test('recognizes the Erase Displayed Memory command', function(assert) {
var packets, captions;
packets = [
// RCL, resume caption loading
{ ccData: 0x1420, type: 0 },
// '01'
{ ccData: characters('01'), type: 0 },
// EOC, End of Caption. Finished transmitting, display '01'
{ pts: 1 * 1000, ccData: 0x142f, type: 0 },
// EDM, Erase Displayed Memory
{ pts: 1.5 * 1000, ccData: 0x142c, type: 0 },
// '23'
{ ccData: characters('23'), type: 0 },
// EOC, End of Caption. Display '23'
{ pts: 2 * 1000, ccData: 0x142f, type: 0 },
// '34'
{ ccData: characters('34'), type: 0 },
// EOC, End of Caption. Display '34'
{ pts: 3 * 1000, ccData: 0x142f, type: 0 },
// Send another command so that the second EOC isn't ignored
{ ccData: 0x1420, type: 0},
// EOC, End of Caption
{ pts: 4 * 1000, ccData: 0x142f, type: 0 }
];
captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
packets.forEach(cea608Stream.push, cea608Stream);
assert.equal(captions.length, 3, 'detected three captions');
assert.deepEqual(captions[0], {
startPts: 1 * 1000,
endPts: 1.5 * 1000,
content: [{
line: 15,
position: 10,
text: '01'
}],
stream: 'CC1'
}, 'parsed the first caption');
assert.deepEqual(captions[1], {
startPts: 2 * 1000,
endPts: 3 * 1000,
content: [{
line: 15,
position: 10,
text: '23'
}],
stream: 'CC1'
}, 'parsed the second caption');
assert.deepEqual(captions[2], {
startPts: 3 * 1000,
endPts: 4 * 1000,
content: [{
line: 15,
position: 10,
text: '34'
}],
stream: 'CC1'
}, 'parsed the third caption');
});
QUnit.test('correct content text is added to non-displayed memory for pop-on mode', function(assert) {
var captions = [], packets;
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
packets = [
// RCL, resume caption loading
{ ccData: 0x1420, type: 0 },
// '01'
{ ccData: characters('01'), type: 0 },
// backspace
{ ccData: 0x1421, type: 0 },
{ ccData: characters('23'), type: 0 },
// PAC: row 13, no indent
{ pts: 1 * 1000, ccData: 0x1370, type: 0 },
{ pts: 1 * 1000, ccData: characters('32'), type: 0 },
// backspace
{ pts: 2 * 1000, ccData: 0x1421, type: 0 },
{ pts: 3 * 1000, ccData: characters('10'), type: 0 },
// EOC, End of Caption
{ pts: 4 * 1000, ccData: 0x142f, type: 0 },
// Send another command so that the second EOC isn't ignored
{ ccData: 0x1420, type: 0 },
// EOC, End of Caption, flush caption
{ pts: 5 * 1000, ccData: 0x142f, type: 0 }
];
packets.forEach(cea608Stream.push, cea608Stream);
assert.equal(captions.length, 1, 'detected a caption');
assert.equal(captions[0].content[0].text, '310', 'first content text');
assert.equal(captions[0].content[1].text, '023', 'second content text');
});
QUnit.test('backspaces on cleared memory are no-ops', function(assert) {
var captions = [], packets;
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
packets = [
// RCL, resume caption loading
{ ccData: 0x1420 },
// backspace
{ ccData: 0x1421 },
// EOC, End of Caption. Finished transmitting, display '01'
{ pts: 1 * 1000, ccData: 0x142f }
];
packets.forEach(cea608Stream.push, cea608Stream);
assert.equal(captions.length, 0, 'no captions detected');
});
QUnit.test('recognizes the Erase Non-Displayed Memory command', function(assert) {
var packets, captions;
packets = [
// RCL, resume caption loading
{ ccData: 0x1420, type: 0 },
// '01'
{ ccData: characters('01'), type: 0 },
// ENM, Erase Non-Displayed Memory
{ ccData: 0x142e, type: 0 },
{ ccData: characters('23'), type: 0 },
// EOC, End of Caption. Finished transmitting, display '23'
{ pts: 1 * 1000, ccData: 0x142f, type: 0 },
// Send another command so that the second EOC isn't ignored
{ ccData: 0x1420, type: 0 },
// EOC, End of Caption
{ pts: 2 * 1000, ccData: 0x142f, type: 0 }
];
captions = [];
packets.forEach(cea608Stream.push, cea608Stream);
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
packets.forEach(cea608Stream.push, cea608Stream);
assert.equal(captions.length, 1, 'detected one caption');
assert.deepEqual(captions[0], {
startPts: 1 * 1000,
endPts: 2 * 1000,
content: [{
line: 15,
position: 10,
text: '23'
}],
stream: 'CC1'
}, 'cleared the non-displayed memory');
});
QUnit.test('ignores unrecognized commands', function(assert) {
var packets, captions;
packets = [
// RCL, resume caption loading
{ ccData: 0x1420, type: 0 },
// a row-9 magenta command, which is not supported
{ ccData: 0x1f4c, type: 0 },
// '01'
{ ccData: characters('01'), type: 0 },
// EOC, End of Caption
{ pts: 1 * 1000, ccData: 0x142f, type: 0 },
// Send another command so that the second EOC isn't ignored
{ ccData: 0x1420, type: 0 },
// EOC, End of Caption
{ pts: 2 * 1000, ccData: 0x142f, type: 0 }
];
captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
packets.forEach(cea608Stream.push, cea608Stream);
assert.equal(captions[0].content[0].text, '01', 'skipped the unrecognized commands');
});
QUnit.skip('applies preamble address codes', function(assert) {
assert.ok(false, 'not implemented');
});
QUnit.skip('applies mid-row colors', function(assert) {
assert.ok(false, 'not implemented');
});
QUnit.test('applies mid-row underline', function(assert) {
var captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
var packets = [
// RU2 (roll-up, 2 rows)
{ ccData: 0x1425, type: 0 },
{ ccData: characters('no'), type: 0 },
// mid-row, white underline
{ ccData: 0x1121, type: 0 },
{ ccData: characters('ye'), type: 0 },
{ ccData: characters('s.'), type: 0 },
// CR (carriage return), dispatches caption
{ ccData: 0x142d, type: 0 }
];
packets.forEach(cea608Stream.push, cea608Stream);
assert.equal(captions[0].content[0].text, 'no <u>yes.</u>', 'properly closed by CR');
assert.deepEqual(cea608Stream.formatting_, [], 'formatting is empty');
});
QUnit.test('applies mid-row italics', function(assert) {
var captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
var packets = [
// RU2 (roll-up, 2 rows)
{ ccData: 0x1425, type: 0 },
{ ccData: characters('no'), type: 0 },
// mid-row, italics
{ ccData: 0x112e, type: 0 },
{ ccData: characters('ye'), type: 0 },
{ ccData: characters('s.'), type: 0 },
// CR (carriage return), dispatches caption
{ ccData: 0x142d, type: 0 }
];
packets.forEach(cea608Stream.push, cea608Stream);
assert.equal(captions[0].content[0].text, 'no <i>yes.</i>', 'properly closed by CR');
assert.deepEqual(cea608Stream.formatting_, [], 'formatting is empty');
});
QUnit.test('applies mid-row italics underline', function(assert) {
var captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
var packets = [
// RU2 (roll-up, 2 rows)
{ ccData: 0x1425, type: 0 },
{ ccData: characters('no'), type: 0 },
// mid-row, italics underline
{ ccData: 0x112f, type: 0 },
{ ccData: characters('ye'), type: 0 },
{ ccData: characters('s.'), type: 0 },
// CR (carriage return), dispatches caption
{ ccData: 0x142d, type: 0 }
];
packets.forEach(cea608Stream.push, cea608Stream);
assert.equal(captions[0].content[0].text, 'no <i><u>yes.</u></i>', 'properly closed by CR');
assert.deepEqual(cea608Stream.formatting_, [], 'formatting is empty');
});
// NOTE: With the exception of white italics PACs (the following two test
// cases), PACs only have their underline attribute extracted and used
QUnit.test('applies PAC underline', function(assert) {
var captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
var packets = [
// RU2 (roll-up, 2 rows)
{ ccData: 0x1425, type: 0 },
// PAC: row 15, white underline
{ ccData: 0x1461, type: 0 },
{ ccData: characters('ye'), type: 0 },
{ ccData: characters('s.'), type: 0 },
// CR (carriage return), dispatches caption
{ ccData: 0x142d, type: 0 }
];
packets.forEach(cea608Stream.push, cea608Stream);
assert.equal(captions[0].content[0].text, '<u>yes.</u>', 'properly closed by CR');
assert.deepEqual(cea608Stream.formatting_, [], 'formatting is empty');
});
QUnit.test('applies PAC white italics', function(assert) {
var captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
var packets = [
// RU2 (roll-up, 2 rows)
{ ccData: 0x1425, type: 0 },
// PAC: row 15, white italics
{ ccData: 0x146e, type: 0 },
{ ccData: characters('ye'), type: 0 },
{ ccData: characters('s.'), type: 0 },
// CR (carriage return), dispatches caption
{ ccData: 0x142d, type: 0 }
];
packets.forEach(cea608Stream.push, cea608Stream);
assert.equal(captions[0].content[0].text, '<i>yes.</i>', 'properly closed by CR');
assert.deepEqual(cea608Stream.formatting_, [], 'formatting is empty');
});
QUnit.test('applies PAC white italics underline', function(assert) {
var captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
var packets = [
// RU2 (roll-up, 2 rows)
{ ccData: 0x1425, type: 0 },
// PAC: row 15, white italics underline
{ ccData: 0x146f, type: 0 },
{ ccData: characters('ye'), type: 0 },
{ ccData: characters('s.'), type: 0 },
// CR (carriage return), dispatches caption
{ ccData: 0x142d, type: 0 }
];
packets.forEach(cea608Stream.push, cea608Stream);
assert.equal(captions[0].content[0].text, '<u><i>yes.</i></u>', 'properly closed by CR');
assert.deepEqual(cea608Stream.formatting_, [], 'formatting is empty');
});
QUnit.test('includes all caption text at PAC row change', function(assert) {
var captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
var packets = [
// RCL, resume caption loading
{ ccData: 0x1420, type: 0 },
// PAC: row 14, white italics underlime
{ ccData: 0x144f, type: 0 },
{ ccData: characters('ye'), type: 0 },
{ ccData: characters('s.'), type: 0 },
// PAC: row 15, indent 0
{ ccData: 0x1470, type: 0 },
{ ccData: characters('no'), type: 0 },
// EOC, End of Caption
{ pts: 1 * 1000, ccData: 0x142f, type: 0 },
// RCL, resume caption loading
{ ccData: 0x1420, type: 0 },
// EOC, End of Caption
{ pts: 2 * 1000, ccData: 0x142f, type: 0 }
];
packets.forEach(cea608Stream.push, cea608Stream);
assert.equal(captions[0].content[0].text, '<u><i>yes.</i></u>', 'first content text');
assert.equal(captions[0].content[1].text, 'no', 'second content text');
assert.deepEqual(cea608Stream.formatting_, [], 'formatting is empty');
});
QUnit.test('closes formatting at EOC', function(assert) {
var captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
var packets = [
// RCL, resume caption loading
{ ccData: 0x1420, type: 0 },
// PAC: row 15, white italics underline
{ ccData: 0x146f, type: 0 },
{ ccData: characters('ye'), type: 0 },
{ ccData: characters('s.'), type: 0 },
// EOC, End of Caption
{ pts: 1 * 1000, ccData: 0x142f, type: 0 },
// Send another command so that the second EOC isn't ignored
{ ccData: 0x1420, type: 0 },
// EOC, End of Caption
{ pts: 2 * 1000, ccData: 0x142f, type: 0 }
];
packets.forEach(cea608Stream.push, cea608Stream);
assert.equal(captions[0].content[0].text, '<u><i>yes.</i></u>', 'properly closed by EOC');
assert.deepEqual(cea608Stream.formatting_, [], 'formatting is empty');
});
QUnit.test('closes formatting at negating mid-row code', function(assert) {
var captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
var packets = [
// RU2 (roll-up, 2 rows)
{ ccData: 0x1425, type: 0 },
{ ccData: characters('no'), type: 0 },
// mid-row: italics underline
{ ccData: 0x112f, type: 0 },
{ ccData: characters('ye'), type: 0 },
{ ccData: characters('s.'), type: 0 },
// mid-row: white
{ ccData: 0x1120, type: 0 },
{ ccData: characters('no'), type: 0 }
];
packets.forEach(cea608Stream.push, cea608Stream);
cea608Stream.flushDisplayed();
assert.equal(captions[0].content[0].text, 'no <i><u>yes.</u></i> no', 'properly closed by negating mid-row code');
assert.deepEqual(cea608Stream.formatting_, [], 'formatting is empty');
});
QUnit.test('roll-up display mode', function(assert) {
var captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
[ // RU2, roll-up captions 2 rows
{ ccData: 0x1425, type: 0},
// '01'
{
pts: 1 * 1000,
ccData: characters('01'),
type: 0
},
// CR, carriage return
{ pts: 3 * 1000, ccData: 0x142d, type: 0 }
].forEach(cea608Stream.push, cea608Stream);
assert.equal(captions.length, 1, 'detected one caption');
assert.deepEqual(captions[0], {
startPts: 0 * 1000,
endPts: 3 * 1000,
content: [{
line: 15,
position: 10,
text: '01'
}],
stream: 'CC1'
}, 'parsed the caption');
captions = [];
[ // RU4, roll-up captions 4 rows
{ ccdata: 0x1427, type: 0 },
// '23'
{
pts: 4 * 1000,
ccData: characters('23'),
type: 0,
stream: 'CC1'
},
// CR
{ pts: 5 * 1000, ccData: 0x142d, type: 0 }
].forEach(cea608Stream.push, cea608Stream);
assert.equal(captions.length, 1, 'detected another caption');
assert.deepEqual(captions[0], {
startPts: 3 * 1000,
endPts: 5 * 1000,
content: [
{
line: 14,
position: 10,
text: '01'
},
{
line: 15,
position: 10,
text: '23'
}
],
stream: 'CC1'
}, 'parsed the new caption and kept the caption up after the new caption');
});
QUnit.test('roll-up displays multiple rows simultaneously', function(assert) {
var captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
[ // RU2, roll-up captions 2 rows
{ ccData: 0x1425, type: 0 },
// '01'
{
pts: 0 * 1000,
ccData: characters('01'),
type: 0,
stream: 'CC1'
},
// CR, carriage return
{ pts: 1 * 1000, ccData: 0x142d, type: 0 }
].forEach(cea608Stream.push, cea608Stream);
assert.equal(captions.length, 1, 'detected a caption');
assert.deepEqual(captions[0], {
startPts: 0 * 1000,
endPts: 1 * 1000,
content: [{
line: 15,
position: 10,
text: '01'
}],
stream: 'CC1'
}, 'created a caption for the first period');
captions = [];
[ // '23'
{
pts: 2 * 1000,
ccData: characters('23'),
type: 0,
stream: 'CC1'
},
// CR, carriage return
{ pts: 3 * 1000, ccData: 0x142d, type: 0 }
].forEach(cea608Stream.push, cea608Stream);
assert.equal(captions.length, 1, 'detected another caption');
assert.deepEqual(captions[0], {
startPts: 1 * 1000,
endPts: 3 * 1000,
content: [
{
line: 14,
position: 10,
text: '01'
},
{
line: 15,
position: 10,
text: '23'
}
],
stream: 'CC1'
}, 'created the top and bottom rows after the shift up');
captions = [];
[ // '45'
{
pts: 4 * 1000,
ccData: characters('45'),
type: 0,
stream: 'CC1'
},
// CR, carriage return
{ pts: 5 * 1000, ccData: 0x142d, type: 0 }
].forEach(cea608Stream.push, cea608Stream);
assert.equal(captions.length, 1, 'detected third caption');
assert.deepEqual(captions[0], {
startPts: 3 * 1000,
endPts: 5 * 1000,
"content": [
{
line: 14,
position: 10,
text: '23'
},
{
line: 15,
position: 10,
text: '45'
}
],
stream: 'CC1'
}, 'created the top and bottom rows after the shift up');
});
QUnit.test('the roll-up count can be changed on-the-fly', function(assert) {
var captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
[ // RU2, roll-up captions 2 rows
{ ccData: 0x1425, type: 0 },
// '01'
{
pts: 0 * 1000,
ccData: characters('01'),
type: 0,
stream: 'CC1'
},
// CR, carriage return
{ pts: 1 * 1000, ccData: 0x142d, type: 0 }
].forEach(cea608Stream.push, cea608Stream);
captions = [];
[ // RU3, roll-up captions 3 rows
{ ccData: 0x1426, type: 0 },
// CR, carriage return
{ pts: 2 * 1000, ccData: 0x142d, type: 0 }
].forEach(cea608Stream.push, cea608Stream);
assert.equal(captions.length, 1, 'still displaying a caption');
captions = [];
[ // RU4, roll-up captions 4 rows
{ ccData: 0x1427, type: 0 },
// CR, carriage return
{ pts: 3 * 1000, ccData: 0x142d, type: 0 }
].forEach(cea608Stream.push, cea608Stream);
assert.equal(captions.length, 1, 'still displaying a caption');
captions = [];
// RU3, roll-up captions 3 rows
cea608Stream.push({ ccdata: 0x1426, type: 0 });
assert.equal(captions.length, 0, 'cleared the caption');
});
QUnit.test('switching to roll-up from pop-on wipes memories and flushes captions', function(assert) {
var captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
[
// RCL (resume caption loading)
{ pts: 0 * 1000, ccData: 0x1420, type: 0 },
{ pts: 0 * 1000, ccData: characters('hi'), type: 0 },
// EOC (end of caption), mark 1st caption start
{ pts: 1 * 1000, ccData: 0x142f, type: 0 },
// RCL, resume caption loading
{ pts: 1 * 1000, ccData: 0x1420, type: 0 },
{ pts: 2 * 1000, ccData: characters('oh'), type: 0 },
// EOC, mark 2nd caption start and flush 1st caption
{ pts: 2 * 1000, ccData: 0x142f, type: 0 },
// RU2 (roll-up, 2 rows), flush 2nd caption
{ pts: 3 * 1000, ccData: 0x1425, type: 0 }
].forEach(cea608Stream.push, cea608Stream);
var displayed = cea608Stream.displayed_.reduce(function(acc, val) {
acc += val.text;
return acc;
}, '');
var nonDisplayed = cea608Stream.nonDisplayed_.reduce(function(acc, val) {
acc += val.text;
return acc;
}, '');
assert.equal(captions.length, 2, 'both captions flushed');
assert.equal(displayed, '', 'displayed memory is wiped');
assert.equal(nonDisplayed, '', 'non-displayed memory is wiped');
assert.deepEqual(captions[0], {
startPts: 1000,
endPts: 2000,
content: [{
line: 15,
position: 10,
text: 'hi',
}],
stream: 'CC1'
}, 'first caption correct');
assert.deepEqual(captions[1], {
startPts: 2000,
endPts: 3000,
content: [{
line: 15,
position: 10,
text: 'oh',
}],
stream: 'CC1'
}, 'second caption correct');
});
QUnit.test('switching to roll-up from paint-on wipes memories and flushes captions', function(assert) {
var captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
[
// RDC (resume direct captioning)
{ pts: 0 * 1000, ccData: 0x1429, type: 0 },
{ pts: 0 * 1000, ccData: characters('hi'), type: 0 },
// RU2 (roll-up, 2 rows), flush displayed caption
{ pts: 1 * 1000, ccData: 0x1425, type: 0 }
].forEach(cea608Stream.push, cea608Stream);
var displayed = cea608Stream.displayed_.reduce(function(acc, val) {
acc += val.text;
return acc;
}, '');
var nonDisplayed = cea608Stream.nonDisplayed_.reduce(function(acc, val) {
acc += val.text;
return acc;
}, '');
assert.equal(captions.length, 1, 'flushed caption');
assert.equal(displayed, '', 'displayed memory is wiped');
assert.equal(nonDisplayed, '', 'non-displayed memory is wiped');
assert.deepEqual(captions[0], {
startPts: 0,
endPts: 1000,
content: [{
line: 15,
position: 10,
text: 'hi',
}],
stream: 'CC1'
}, 'caption correct');
});
// NOTE: This should change to not wiping the display when caption
// positioning is properly implemented
QUnit.test('switching to paint-on from pop-on flushes display', function(assert) {
var captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
[
// RCL (resume caption loading)
{ pts: 0 * 1000, ccData: 0x1420, type: 0 },
// PAC: row 14, indent 0
{ pts: 0 * 1000, ccData: 0x1450, type: 0 },
{ pts: 0 * 1000, ccData: characters('hi'), type: 0 },
// EOC (end of caption), mark caption start
{ pts: 1 * 1000, ccData: 0x142f, type: 0 },
// RCL
{ pts: 1 * 1000, ccData: 0x1420, type: 0 },
// RDC (resume direct captioning), flush caption
{ pts: 2 * 1000, ccData: 0x1429, type: 0 },
// PAC: row 14, indent 0
{ pts: 2 * 1000, ccData: 0x1450, type: 0 },
// TO1 (tab offset 1 column)
{ pts: 2 * 1000, ccData: 0x1721, type: 0 },
{ pts: 3 * 1000, ccData: characters('io'), type: 0 },
// EDM (erase displayed memory), flush paint-on caption
{ pts: 4 * 1000, ccData: 0x142c, type: 0 }
].forEach(cea608Stream.push, cea608Stream);
assert.equal(captions.length, 2, 'detected 2 captions');
assert.equal(captions[0].content[0].text, 'hi', 'pop-on caption received');
assert.equal(captions[0].startPts, 1000, 'proper start pts');
assert.equal(captions[0].endPts, 2000, 'proper end pts');
assert.equal(captions[1].content[0].text, 'io', 'paint-on caption received');
assert.equal(captions[1].startPts, 2000, 'proper start pts');
assert.equal(captions[1].endPts, 4000, 'proper end pts');
});
QUnit.test('backspaces are reflected in the generated captions', function(assert) {
var captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
[ // RU2, roll-up captions 2 rows
{ ccData: 0x1425, type: 0 },
// '01'
{
pts: 0 * 1000,
ccData: characters('01'),
type: 0
},
// backspace
{ ccData: 0x1421, type: 0 },
{
pts: 1 * 1000,
ccData: characters('23'),
type: 0
},
// CR, carriage return
{ pts: 1 * 1000, ccData: 0x142d, type: 0 }
].forEach(cea608Stream.push, cea608Stream);
assert.equal(captions.length, 1, 'detected a caption');
assert.equal(captions[0].content[0].text, '023', 'applied the backspace');
});
QUnit.test('backspaces can remove a caption entirely', function(assert) {
var captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
[ // RU2, roll-up captions 2 rows
{ ccData: 0x1425, type: 0 },
// '01'
{
pts: 0 * 1000,
ccData: characters('01'),
type: 0
},
// backspace
{ ccData: 0x1421, type: 0 },
// Send another command so that the backspace isn't
// ignored as a duplicate command
{ ccData: 0x1425, type: 0 },
// backspace
{ ccData: 0x1421, type: 0 },
// CR, carriage return
{ pts: 1 * 1000, ccData: 0x142d, type: 0 }
].forEach(cea608Stream.push, cea608Stream);
assert.equal(captions.length, 0, 'no caption emitted');
});
QUnit.test('a second identical control code immediately following the first is ignored', function(assert) {
var captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
[ // RU2, roll-up captions 2 rows
{ ccData: 0x1425, type: 0 },
// '01'
{
pts: 0 * 1000,
ccData: characters('01'),
type: 0
},
// '02'
{
pts: 1 * 1000,
ccData: characters('02'),
type: 0
},
// backspace
{ ccData: 0x1421, type: 0 },
// backspace
{ ccData: 0x1421, type: 0 }, // duplicate is ignored
// backspace
{ ccData: 0x1421, type: 0 },
// CR, carriage return
{ pts: 2 * 1000, ccData: 0x142d, type: 0 }
].forEach(cea608Stream.push, cea608Stream);
assert.equal(captions.length, 1, 'caption emitted');
assert.equal(captions[0].content[0].text, '01', 'only two backspaces processed');
});
QUnit.test('a second identical control code separated by only padding from the first is ignored', function(assert) {
var captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
[ // RU2, roll-up captions 2 rows
{ ccData: 0x1425, type: 0 },
// '01'
{
pts: 0 * 1000,
ccData: characters('01'),
type: 0
},
// '02'
{
pts: 1 * 1000,
ccData: characters('02'),
type: 0
},
// backspace
{ ccData: 0x1421, type: 0 },
// padding
{ ccData: 0x0000, type: 0 },
{ ccData: 0x0000, type: 0 },
{ ccData: 0x0000, type: 0 },
// backspace
{ pts: 2 * 1000, ccData: 0x1421, type: 0 }, // duplicate is ignored
// CR, carriage return
{ pts: 3 * 1000, ccData: 0x142d, type: 0 }
].forEach(cea608Stream.push, cea608Stream);
assert.equal(captions.length, 1, 'caption emitted');
assert.equal(captions[0].content[0].text, '010', 'only one backspace processed');
});
QUnit.test('preamble address codes on same row are NOT converted into spaces', function(assert) {
var captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
[ // RU2, roll-up captions 2 rows
{ ccData: 0x1425, type: 0 },
// '01'
{
pts: 0 * 1000,
ccData: characters('01'),
type: 0
},
// PAC: row 15, indent 0
{ ccData: 0x1470, type: 0 },
// '02'
{
pts: 1 * 1000,
ccData: characters('02'),
type: 0
},
// CR, carriage return
{ pts: 2 * 1000, ccData: 0x142d, type: 0 }
].forEach(cea608Stream.push, cea608Stream);
assert.equal(captions.length, 1, 'caption emitted');
assert.equal(captions[0].content[0].text, '0102', 'PACs were NOT converted to space');
});
QUnit.test('generates correct content with PACs in pop-on mode', function(assert) {
var captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
[
// RCL, resume caption loading
{ ccData: 0x1420, type: 0 },
// ENM, erase non-displayed memory
{ ccData: 0x142e, type: 0 },
// PAC: row 12, indent 0
{ ccData: 0x1350, type: 0 },
// text: TEST
{ ccData: 0x5445, type: 0 },
{ ccData: 0x5354, type: 0 },
// PAC: row 14, indent 0
{ ccData: 0x1450, type: 0 },
// text: STRING
{ ccData: 0x5354, type: 0 },
{ ccData: 0x5249, type: 0 },
{ ccData: 0x4e47, type: 0 },
// PAC: row 15, indent 0
{ ccData: 0x1470, type: 0 },
// text: DATA
{ ccData: 0x4441, type: 0 },
{ ccData: 0x5441, type: 0 },
// EOC, end of caption
{ pts: 1 * 1000, ccData: 0x142f, type: 0 },
// EOC, duplicated as per spec
{ pts: 1 * 1000, ccData: 0x142f, type: 0 },
// EOC, dispatch caption
{ pts: 2 * 1000, ccData: 0x142f, type: 0 }
].forEach(cea608Stream.push, cea608Stream);
assert.equal(captions.length, 1, 'caption emitted');
assert.equal(captions[0].content[0].text, 'TEST', 'first content text');
assert.equal(captions[0].content[1].text, 'STRING', 'second content text');
assert.equal(captions[0].content[2].text, 'DATA', 'third content text');
});
QUnit.test('extracts real-world cc1 and cc3 channels', function(assert) {
var cea608Stream1 = cea608Stream;
var cea608Stream3 = new m2ts.Cea608Stream(1, 0);
var captions = [];
cea608Stream1.on('data', function(caption) {
captions.push(caption);
});
cea608Stream3.on('data', function(caption) {
captions.push(caption);
});
var packets = [
{ pts: 425316, type: 0, ccData: 5158 }, // RU3
{ pts: 431322, type: 0, ccData: 5165 }, // CR
{ pts: 440331, type: 0, ccData: 4944 }, // position 11,0
{ pts: 443334, type: 0, ccData: 20549 }, // PE
{ pts: 449340, type: 0, ccData: 21065 }, // RI
{ pts: 449340, type: 0, ccData: 0 }, // padding
{ pts: 452343, type: 0, ccData: 20292 }, // OD
{ pts: 458349, type: 0, ccData: 11264 }, // ,
{ pts: 458349, type: 0, ccData: 0 }, // padding
{ pts: 461352, type: 0, ccData: 0 }, // padding
{ pts: 467358, type: 0, ccData: 8192 }, // (space)
{ pts: 467358, type: 0, ccData: 17920 }, // F
{ pts: 470361, type: 0, ccData: 0 }, // padding
{ pts: 476367, type: 0, ccData: 0 }, // padding
{ pts: 476367, type: 0, ccData: 20300 }, // OL
{ pts: 479370, type: 0, ccData: 19283 }, // KS
{ pts: 485376, type: 0, ccData: 0 }, // padding
{ pts: 485376, type: 0, ccData: 11776 }, // .
{ pts: 674565, type: 0, ccData: 5158 }, // RU3
{ pts: 677568, type: 0, ccData: 5165 }, // CR
{ pts: 371262, type: 1, ccData: 5414 }, // RU3
{ pts: 377268, type: 1, ccData: 0 }, // padding
{ pts: 377268, type: 1, ccData: 4944 }, // position 11,0
{ pts: 380271, type: 1, ccData: 0 }, // padding
{ pts: 386277, type: 1, ccData: 4412 }, // ê
{ pts: 386277, type: 1, ccData: 0 }, // padding
{ pts: 389280, type: 1, ccData: 29810 }, // tr
{ pts: 395286, type: 1, ccData: 25888 }, // e(space)
{ pts: 395286, type: 1, ccData: 30062 }, // un
{ pts: 398289, type: 1, ccData: 25888 }, // e(space)
{ pts: 404295, type: 1, ccData: 28764 }, // pé
{ pts: 404295, type: 1, ccData: 29289 }, // ri
{ pts: 407298, type: 1, ccData: 28516 }, // od
{ pts: 413304, type: 1, ccData: 25856 }, // e
{ pts: 413304, type: 1, ccData: 0 }, // padding
{ pts: 443334, type: 1, ccData: 8292 }, // (space)d
{ pts: 449340, type: 1, ccData: 25888 }, // e(space)
{ pts: 449340, type: 1, ccData: 29045 }, // qu
{ pts: 452343, type: 1, ccData: 25971 }, // es
{ pts: 458349, type: 1, ccData: 29801 }, // ti
{ pts: 458349, type: 1, ccData: 28526 }, // on
{ pts: 461352, type: 1, ccData: 29440 }, // s
{ pts: 467358, type: 1, ccData: 5421 }, // CR
{ pts: 467358, type: 1, ccData: 0 }, // padding
{ pts: 470361, type: 1, ccData: 5414 }, // RU3
{ pts: 476367, type: 1, ccData: 0 } // padding
];
packets.forEach(function(packet) {
cea608Stream1.push(packet);
cea608Stream3.push(packet);
});
var cc1 = {stream: 'CC1', content: [{ text: 'PERIOD, FOLKS.'}] };
var cc3 = {stream: 'CC3', content: [{ text: 'être une période de questions' }] };
assert.equal(captions.length, 2, 'caption emitted');
assert.equal(captions[0].stream, cc1.stream, 'cc1 stream detected');
assert.equal(captions[0].content[0].text, cc1.content[0].text, 'cc1 stream extracted successfully');
assert.equal(captions[1].stream, cc3.stream, 'cc3 stream detected');
assert.equal(captions[1].content[0].text, cc3.content[0].text, 'cc3 stream extracted successfully');
});
QUnit.test('backspaces stop at the beginning of the line', function(assert) {
var captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
[ // RU2, roll-up captions 2 rows
{ ccData: 0x1425, type: 0 },
// '01'
{
pts: 0 * 1000,
ccData: characters('01'),
type: 0
},
// backspace
{ ccData: 0x1421, type: 0 },
// Send another command so that the backspace isn't
// ignored as a duplicate command
{ ccData: 0x1425, type: 0 },
// backspace
{ ccData: 0x1421, type: 0 },
// Send another command so that the backspace isn't
// ignored as a duplicate command
{ ccData: 0x1425, type: 0 },
// backspace
{ ccData: 0x1421, type: 0 },
// CR, carriage return
{ pts: 1 * 1000, ccData: 0x142d, type: 0 }
].forEach(cea608Stream.push, cea608Stream);
assert.equal(captions.length, 0, 'no caption emitted');
});
QUnit.test('reset works', function(assert) {
var captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
[ // RU2, roll-up captions 2 rows
{ pts: 0, ccData: 0x1425, type: 0 },
// mid-row: white underline
{ pts: 0, ccData: 0x1121, type: 0 },
{ pts: 0, ccData: characters('01'), type: 0 }
].forEach(cea608Stream.push, cea608Stream);
var buffer = cea608Stream.displayed_.map(function(row) {
return row.text.trim();
}).join('\n')
.replace(/^\n+|\n+$/g, '');
assert.equal(buffer, '<u>01', 'buffer is as expected');
cea608Stream.reset();
buffer = cea608Stream.displayed_
.map(function(row) {
return row.text.trim();
})
.join('\n')
.replace(/^\n+|\n+$/g, '');
assert.equal(buffer, '', 'displayed buffer reset successfully');
assert.equal(cea608Stream.lastControlCode_, null, 'last control code reset successfully');
assert.deepEqual(cea608Stream.formatting_, [], 'formatting was reset');
});
QUnit.test('paint-on mode', function(assert) {
var packets, captions;
packets = [
// RDC, resume direct captioning, begin display
{ pts: 1000, ccData: 0x1429, type: 0 },
{ pts: 2000, ccData: characters('hi'), type: 0 },
// EDM, erase displayed memory. Finish display, flush caption
{ pts: 3000, ccData: 0x142c, type: 0 }
];
captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
packets.forEach(cea608Stream.push, cea608Stream);
assert.equal(captions.length, 1, 'detected a caption');
assert.deepEqual(captions[0], {
startPts: 1000,
endPts: 3000,
content: [{
line: 15,
position: 10,
text: 'hi',
}],
stream: 'CC1'
}, 'parsed the caption');
});
QUnit.test('generates correct text from PACs in paint-on mode', function(assert) {
var captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
[
// RDC, resume direct captioning
{ pts: 1000, ccData: 0x1429, type: 0 },
// PAC: row 12, indent 0
{ pts: 1000, ccData: 0x1350, type: 0 },
// text: TEST
{ pts: 2000, ccData: 0x5445, type: 0 },
{ pts: 2000, ccData: 0x5354, type: 0 },
// PAC: row 14, indent 0
{ pts: 3000, ccData: 0x1450, type: 0 },
// text: STRING
{ pts: 3000, ccData: 0x5354, type: 0 },
{ pts: 4000, ccData: 0x5249, type: 0 },
{ pts: 4000, ccData: 0x4e47, type: 0 },
// PAC: row 15, indent 0
{ pts: 5000, ccData: 0x1470, type: 0 },
// text: DATA
{ pts: 5000, ccData: 0x4441, type: 0 },
{ pts: 6000, ccData: 0x5441, type: 0 },
// EDM, erase displayed memory. Finish display, flush caption
{ pts: 6000, ccData: 0x142c, type: 0 }
].forEach(cea608Stream.push, cea608Stream);
assert.equal(captions.length, 1, 'caption emitted');
assert.equal(captions[0].content[0].text, 'TEST', 'first content text');
assert.equal(captions[0].content[1].text, 'STRING', 'second content text');
assert.equal(captions[0].content[2].text, 'DATA', 'third content text');
});
QUnit.test('multiple caption texts are generated (paint-on)', function(assert) {
var captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
[ // RDC, resume direct captioning
{ ccData: 0x1429, type: 0 },
// '01', default row 15
{ pts: 0 * 1000, ccData: characters('01'), type: 0 },
// backspace
{ pts: 0 * 1000, ccData: 0x1421, type: 0 },
{ pts: 1 * 1000, ccData: characters('23'), type: 0 },
// PAC: row 13, indent 0
{ pts: 2 * 1000, ccData: 0x1370, type: 0 },
{ pts: 2 * 1000, ccData: characters('32'), type: 0 },
// backspace
{ pts: 3 * 1000, ccData: 0x1421, type: 0 },
{ pts: 4 * 1000, ccData: characters('10'), type: 0 },
// EDM, erase displayed memory, flush caption
{ pts: 5 * 1000, ccData: 0x142c, type: 0 }
].forEach(cea608Stream.push, cea608Stream);
assert.equal(captions.length, 1, 'detected a caption');
assert.equal(captions[0].content[0].text, '310', 'first caption text');
assert.equal(captions[0].content[1].text, '023', 'second caption text');
});
QUnit.test('PAC indent code increases the position', function(assert) {
var captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
var packets = [
// RCL, resume caption loading
{ ccData: 0x1420, type: 0 },
// PAC indent code representing 4 indentations.
{ ccData: 5240, type: 0 },
{ ccData: characters('te'), type: 0 },
{ ccData: characters('st'), type: 0 },
// EOC, End of Caption
{ pts: 1 * 1000, ccData: 0x142f, type: 0 },
// RCL, resume caption loading
{ ccData: 0x1420, type: 0 },
// EOC, End of Caption
{ pts: 2 * 1000, ccData: 0x142f, type: 0 }
];
packets.forEach(cea608Stream.push, cea608Stream);
assert.equal(captions[0].content[0].text, 'test', 'content text');
assert.equal(captions[0].content[0].line, 15, 'positions the caption to the bottom of the screen');
assert.equal(captions[0].content[0].position, 50, 'positions the caption to the right of the screen');
});
QUnit.test('PAC offset code increases the position', function(assert) {
var captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
var packets = [
// RCL, resume caption loading
{ ccData: 0x1420, type: 0 },
// PAC: row 1, indent 0
{ pts: 6750, ccData: 0x1150, type: 0 },
// TO2 (tab offset 2 columns)
{ pts: 6755, ccData: 0x1722, type: 0 },
{ ccData: characters('te'), type: 0 },
{ ccData: characters('st'), type: 0 },
// EOC, End of Caption
{ pts: 1 * 1000, ccData: 0x142f, type: 0 },
// RCL, resume caption loading
{ ccData: 0x1420, type: 0 },
// EOC, End of Caption
{ pts: 2 * 1000, ccData: 0x142f, type: 0 }
];
packets.forEach(cea608Stream.push, cea608Stream);
assert.equal(captions[0].content[0].text, 'test', 'content text');
assert.equal(captions[0].content[0].line, 1, 'positions the caption to the bottom of the screen');
// Two tab offset columns adds 5 to the position (2 * 2.5)
assert.equal(captions[0].content[0].position, 15, 'positions the caption to the right');
});
QUnit.test('PAC row command ensures we have the correct line property for captions', function(assert) {
var captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
var packets = [
// RU2 (roll-up, 2 rows)
{ pts: 6675, ccData: 0x1425, type: 0 },
// CR (carriange return), flush nothing
{ pts: 6675, ccData: 0x142d, type: 0 },
// PAC: row 2, indent 0
// This should ensure the captions are at the top of the screen.
{ pts: 6675, ccData: 0x1170, type: 0 },
// text: YEAR.
{ pts: 6676, ccData: 0x5945, type: 0 },
{ pts: 6676, ccData: 0x4152, type: 0 },
{ pts: 6676, ccData: 0x2e00, type: 0 },
// RU2 (roll-up, 2 rows)
{ pts: 6677, ccData: 0x1425, type: 0 },
// CR (carriange return), flush 1 row
{ pts: 6677, ccData: 0x142d, type: 0 },
// EDM (erase displayed memory), flush 2 displayed roll-up rows
{ pts: 6697, ccData: 0x142c, type: 0 },
// RDC (resume direct captioning), wipes memories, flushes nothing
{ pts: 6749, ccData: 0x1429, type: 0 },
// PAC: row 1, indent 0
{ pts: 6750, ccData: 0x1150, type: 0 },
// EOC, End of Caption
{ pts: 1 * 1000, ccData: 0x142f, type: 0 },
// RCL, resume caption loading
{ ccData: 0x1420, type: 0 },
// EOC, End of Caption
{ pts: 2 * 1000, ccData: 0x142f, type: 0 }
];
// First caption stream is at the second most bottom row.
packets.forEach(cea608Stream.push, cea608Stream);
assert.equal(captions[0].content[0].text, 'YEAR.', 'content text');
assert.equal(captions[0].content[0].line, 2, 'positions the caption in the second most bottom row');
assert.equal(captions[0].content[0].position, 10, 'position of the caption');
// Second caption stream is at the most bottom row.
assert.equal(captions[1].content[0].text, 'YEAR.', 'content text');
assert.equal(captions[1].content[0].line, 1, 'positions the caption in the most bottom row');
assert.equal(captions[1].content[0].position, 10, 'position of the caption');
});
QUnit.test('mix of all modes (extract from CNN)', function(assert) {
var captions = [];
cea608Stream.on('data', function(caption) {
captions.push(caption);
});
[
// RU2 (roll-up, 2 rows)
{ pts: 6675, ccData: 0x1425, type: 0 },
// CR (carriange return), flush nothing
{ pts: 6675, ccData: 0x142d, type: 0 },
// PAC: row 2, indent 0
{ pts: 6675, ccData: 0x1170, type: 0 },
// text: YEAR.
{ pts: 6676, ccData: 0x5945, type: 0 },
{ pts: 6676, ccData: 0x4152, type: 0 },
{ pts: 6676, ccData: 0x2e00, type: 0 },
// RU2 (roll-up, 2 rows)
{ pts: 6677, ccData: 0x1425, type: 0 },
// CR (carriange return), flush 1 row
{ pts: 6677, ccData: 0x142d, type: 0 },
// PAC: row 2, indent 0
{ pts: 6677, ccData: 0x1170, type: 0 },
// text: GO TO CNNHEROS.COM.
{ pts: 6677, ccData: 0x474f, type: 0 },
{ pts: 6678, ccData: 0x2054, type: 0 },
{ pts: 6678, ccData: 0x4f00, type: 0 },
{ pts: 6678, ccData: 0x2043, type: 0 },
{ pts: 6679, ccData: 0x4e4e, type: 0 },
{ pts: 6679, ccData: 0x4845, type: 0 },
{ pts: 6679, ccData: 0x524f, type: 0 },
{ pts: 6680, ccData: 0x532e, type: 0 },
{ pts: 6680, ccData: 0x434f, type: 0 },
{ pts: 6680, ccData: 0x4d2e, type: 0 },
// EDM (erase displayed memory), flush 2 displayed roll-up rows
{ pts: 6697, ccData: 0x142c, type: 0 },
// RDC (resume direct captioning), wipes memories, flushes nothing
{ pts: 6749, ccData: 0x1429, type: 0 },
// PAC: row 1, indent 0
{ pts: 6750, ccData: 0x1150, type: 0 },
// text: Did your Senator or Congressman
{ pts: 6750, ccData: 0x4469, type: 0 },
{ pts: 6750, ccData: 0x6420, type: 0 },
{ pts: 6750, ccData: 0x796f, type: 0 },
{ pts: 6751, ccData: 0x7572, type: 0 },
{ pts: 6751, ccData: 0x2053, type: 0 },
{ pts: 6751, ccData: 0x656e, type: 0 },
{ pts: 6752, ccData: 0x6174, type: 0 },
{ pts: 6752, ccData: 0x6f72, type: 0 },
{ pts: 6752, ccData: 0x206f, type: 0 },
{ pts: 6753, ccData: 0x7220, type: 0 },
{ pts: 6753, ccData: 0x436f, type: 0 },
{ pts: 6753, ccData: 0x6e67, type: 0 },
{ pts: 6753, ccData: 0x7265, type: 0 },
{ pts: 6754, ccData: 0x7373, type: 0 },
{ pts: 6754, ccData: 0x6d61, type: 0 },
{ pts: 6754, ccData: 0x6e00, type: 0 },
// PAC: row 2, indent 0
{ pts: 6755, ccData: 0x1170, type: 0 },
// TO2 (tab offset 2 columns)
{ pts: 6755, ccData: 0x1722, type: 0 },
// text: get elected by talking tough
{ pts: 6755, ccData: 0x6765, type: 0 },
{ pts: 6756, ccData: 0x7420, type: 0 },
{ pts: 6756, ccData: 0x656c, type: 0 },
{ pts: 6756, ccData: 0x6563, type: 0 },
{ pts: 6756, ccData: 0x7465, type: 0 },
{ pts: 6757, ccData: 0x6420, type: 0 },
{ pts: 6757, ccData: 0x6279, type: 0 },
{ pts: 6757, ccData: 0x2074, type: 0 },
{ pts: 6758, ccData: 0x616c, type: 0 },
{ pts: 6758, ccData: 0x6b69, type: 0 },
{ pts: 6758, ccData: 0x6e67, type: 0 },
{ pts: 6759, ccData: 0x2074, type: 0 },
{ pts: 6759, ccData: 0x6f75, type: 0 },
{ pts: 6759, ccData: 0x6768, type: 0 },
// RCL (resume caption loading)
{ pts: 6759, ccData: 0x1420, type: 0 },
// PAC: row 1, indent 4
{ pts: 6760, ccData: 0x1152, type: 0 },
// TO1 (tab offset 1 column)
{ pts: 6760, ccData: 0x1721, type: 0 },
// text: on the national debt?
{ pts: 6760, ccData: 0x6f6e, type: 0 },
{ pts: 6761, ccData: 0x2074, type: 0 },
{ pts: 6761, ccData: 0x6865, type: 0 },
{ pts: 6761, ccData: 0x206e, type: 0 },
{ pts: 6762, ccData: 0x6174, type: 0 },
{ pts: 6762, ccData: 0x696f, type: 0 },
{ pts: 6762, ccData: 0x6e61, type: 0 },
{ pts: 6762, ccData: 0x6c20, type: 0 },
{ pts: 6763, ccData: 0x6465, type: 0 },
{ pts: 6763, ccData: 0x6274, type: 0 },
{ pts: 6763, ccData: 0x3f00, type: 0 },
// RCL (resume caption loading)
{ pts: 6781, ccData: 0x1420, type: 0 },
// EDM (erase displayed memory), flush paint-on caption
{ pts: 6781, ccData: 0x142c, type: 0 },
// EOC (end of caption), mark pop-on caption 1 start
{ pts: 6782, ccData: 0x142f, type: 0 },
// RCL (resume caption loading)
{ pts: 6782, ccData: 0x1420, type: 0 },
// PAC: row 1, indent 4
{ pts: 6782, ccData: 0x1152, type: 0 },
// TO2 (tab offset 2 columns)
{ pts: 6783, ccData: 0x1722, type: 0 },
// text: Will they stay true
{ pts: 6783, ccData: 0x5769, type: 0 },
{ pts: 6783, ccData: 0x6c6c, type: 0 },
{ pts: 6783, ccData: 0x2074, type: 0 },
{ pts: 6784, ccData: 0x6865, type: 0 },
{ pts: 6784, ccData: 0x7920, type: 0 },
{ pts: 6784, ccData: 0x7374, type: 0 },
{ pts: 6785, ccData: 0x6179, type: 0 },
{ pts: 6785, ccData: 0x2074, type: 0 },
{ pts: 6785, ccData: 0x7275, type: 0 },
{ pts: 6786, ccData: 0x6500, type: 0 },
// PAC: row 2, indent 8
{ pts: 6786, ccData: 0x1174, type: 0 },
// text: to their words?
{ pts: 6786, ccData: 0x746f, type: 0 },
{ pts: 6786, ccData: 0x2074, type: 0 },
{ pts: 6787, ccData: 0x6865, type: 0 },
{ pts: 6787, ccData: 0x6972, type: 0 },
{ pts: 6787, ccData: 0x2077, type: 0 },
{ pts: 6788, ccData: 0x6f72, type: 0 },
{ pts: 6788, ccData: 0x6473, type: 0 },
{ pts: 6788, ccData: 0x3f00, type: 0 },
// RCL (resume caption loading)
{ pts: 6797, ccData: 0x1420, type: 0 },
// EDM (erase displayed memory), mark pop-on caption 1 end and flush
{ pts: 6797, ccData: 0x142c, type: 0 },
// EOC (end of caption), mark pop-on caption 2 start, flush nothing
{ pts: 6798, ccData: 0x142f, type: 0 },
// RCL
{ pts: 6799, ccData: 0x1420, type: 0 },
// EOC, mark pop-on caption 2 end and flush
{ pts: 6838, ccData: 0x142f, type: 0 },
// RU2 (roll-up, 2 rows), wipes memories
{ pts: 6841, ccData: 0x1425, type: 0 },
// CR (carriage return), flush nothing
{ pts: 6841, ccData: 0x142d, type: 0 },
// PAC: row 2, indent 0
{ pts: 6841, ccData: 0x1170, type: 0 },
// text: NO MORE SPECULATION, NO MORE
{ pts: 6841, ccData: 0x3e3e, type: 0 },
{ pts: 6841, ccData: 0x3e00, type: 0 },
{ pts: 6842, ccData: 0x204e, type: 0 },
{ pts: 6842, ccData: 0x4f00, type: 0 },
{ pts: 6842, ccData: 0x204d, type: 0 },
{ pts: 6842, ccData: 0x4f52, type: 0 },
{ pts: 6842, ccData: 0x4500, type: 0 },
{ pts: 6842, ccData: 0x2000, type: 0 },
{ pts: 6842, ccData: 0x5350, type: 0 },
{ pts: 6843, ccData: 0x4543, type: 0 },
{ pts: 6843, ccData: 0x554c, type: 0 },
{ pts: 6843, ccData: 0x4154, type: 0 },
{ pts: 6843, ccData: 0x494f, type: 0 },
{ pts: 6843, ccData: 0x4e2c, type: 0 },
{ pts: 6843, ccData: 0x204e, type: 0 },
{ pts: 6843, ccData: 0x4f00, type: 0 },
{ pts: 6843, ccData: 0x204d, type: 0 },
{ pts: 6844, ccData: 0x4f52, type: 0 },
{ pts: 6844, ccData: 0x4500, type: 0 },
// RU2 (roll-up, two rows)
{ pts: 6844, ccData: 0x1425, type: 0 },
// CR (carriage return), flush 1 roll-up row
{ pts: 6844, ccData: 0x142d, type: 0 },
// PAC: row 2, indent 0
{ pts: 6844, ccData: 0x1170, type: 0 },
// text: RUMORS OR GUESSING GAMES.
{ pts: 6844, ccData: 0x5255, type: 0 },
{ pts: 6844, ccData: 0x4d4f, type: 0 },
{ pts: 6844, ccData: 0x5253, type: 0 },
{ pts: 6844, ccData: 0x204f, type: 0 },
{ pts: 6845, ccData: 0x5200, type: 0 },
{ pts: 6845, ccData: 0x2047, type: 0 },
{ pts: 6845, ccData: 0x5545, type: 0 },
{ pts: 6845, ccData: 0x5353, type: 0 },
{ pts: 6845, ccData: 0x494e, type: 0 },
{ pts: 6845, ccData: 0x4700, type: 0 },
{ pts: 6845, ccData: 0x2047, type: 0 },
{ pts: 6845, ccData: 0x414d, type: 0 },
{ pts: 6845, ccData: 0x4553, type: 0 },
{ pts: 6845, ccData: 0x2e00, type: 0 },
// RU2 (roll-up, 2 rows)
{ pts: 6846, ccData: 0x1425, type: 0 },
// CR (carriage return), flush 2 roll-up rows
{ pts: 6846, ccData: 0x142d, type: 0 }
].forEach(cea608Stream.push, cea608Stream);
assert.equal(captions.length, 7, 'detected 7 captions of varying types');
assert.deepEqual(captions[0], {
content: [{
line: 2,
position: 10,
text: 'YEAR.',
}],
startPts: 6675,
endPts: 6677,
stream: 'CC1'
}, 'parsed the 1st roll-up caption');
assert.deepEqual(captions[1], {
content: [
{
line: 1,
position: 10,
text: 'YEAR.',
},
{
line: 2,
position: 10,
text: 'GO TO CNNHEROS.COM.',
}
],
startPts: 6677,
endPts: 6697,
stream: 'CC1'
}, 'parsed the 2nd roll-up caption');
assert.deepEqual(captions[2], {
content: [
{
line: 1,
position: 10,
text: 'Did your Senator or Congressman',
},
{
line: 2,
position: 10,
text: 'get elected by talking tough',
}
],
startPts: 6749,
endPts: 6781,
stream: 'CC1'
}, 'parsed the paint-on caption');
assert.deepEqual(captions[3], {
content: [{
line: 1,
position: 22.5,
text: 'on the national debt?',
}],
startPts: 6782,
endPts: 6797,
stream: 'CC1'
}, 'parsed the 1st pop-on caption');
assert.deepEqual(captions[4], {
content: [
{
line: 1,
position: 25,
text: 'Will they stay true',
},
{
line: 2,
position: 30,
text: 'to their words?',
}
],
startPts: 6798,
endPts: 6838,
stream: 'CC1'
}, 'parsed the 2nd pop-on caption');
assert.deepEqual(captions[5], {
content: [{
line: 2,
position: 10,
text: '>>> NO MORE SPECULATION, NO MORE',
}],
startPts: 6841,
endPts: 6844,
stream: 'CC1'
}, 'parsed the 3rd roll-up caption');
assert.deepEqual(captions[6], {
content: [
{
line: 1,
position: 10,
text: '>>> NO MORE SPECULATION, NO MORE',
},
{
line: 2,
position: 10,
text: 'RUMORS OR GUESSING GAMES.',
}
],
startPts: 6844,
endPts: 6846,
stream: 'CC1'
}, 'parsed the 4th roll-up caption');
});
QUnit.test('Cea608Stream will trigger log on malformed captions', function(assert) {
var result;
var logs = [];
cea608Stream.on('log', function(log) {
logs.push(log);
})
// this will force an exception to happen in flushDisplayed
cea608Stream.displayed_[0] = undefined;
try {
cea608Stream.flushDisplayed();
result = true;
} catch (e) {
result = false;
}
assert.ok(
result,
'the function does not throw an exception'
);
assert.deepEqual(
logs,
[
{level: 'warn', message: 'Skipping a malformed 608 caption at index 0.'}
],
'logs were triggered'
);
});
var cea708Stream;
QUnit.module('CEA 708 Stream', {
beforeEach: function() {
cea708Stream = new m2ts.Cea708Stream();
}
});
QUnit.test('Filters encoding values out of captionServices option block', function(assert) {
var expectedServiceEncodings = {
SERVICE1: 'euc-kr',
SERVICE2: 'utf-8',
};
cea708Stream = new m2ts.Cea708Stream({
captionServices: {
SERVICE1: {
language: 'kr',
label: 'Korean',
encoding: 'euc-kr'
},
SERVICE2: {
language: 'en',
label: 'English',
encoding: 'utf-8'
}
}
});
assert.deepEqual(cea708Stream.serviceEncodings, expectedServiceEncodings, 'filtered encodings correctly');
});
QUnit.test('parses 708 captions', function(assert) {
var captions = [];
cea708Stream.on('data', function(caption) {
captions.push(caption);
});
cc708PinkUnderscore.forEach(cea708Stream.push, cea708Stream);
assert.equal(captions.length, 235, 'parsed 235 captions');
assert.deepEqual(captions[0], {
startPts: 6723335478,
endPts: 6723626769,
text: '\"Pinkalicious_and_Peterrific\"\nis_made_possible_in_part_by:',
stream: 'cc708_1'
}, 'parsed first caption correctly');
assert.deepEqual(captions[1], {
startPts: 6723740883,
endPts: 6723945087,
text: 'GIRL:\nRead_me_the_tale\nof_a_faraway_land.',
stream: 'cc708_1'
}, 'parsed second caption correctly');
assert.deepEqual(captions[2], {
startPts: 6723948090,
endPts: 6724200342,
text: 'Tell_me_of_planets\nwith_oceans_of_sand.',
stream: 'cc708_1'
}, 'parsed third caption correctly');
assert.deepEqual(captions[33], {
startPts: 6732617751,
endPts: 6732876009,
text: '♪_It\'s_a_Pinkalicious_feeling_♪',
stream: 'cc708_1'
}, 'parsed caption 33 correctly with music note');
assert.deepEqual(captions[38], {
startPts: 6734218350,
endPts: 6734425557,
text: 'PINKALICIOUS:\n\"Dream_Salon.\"',
stream: 'cc708_1'
}, 'parsed caption 38 correctly');
assert.deepEqual(captions[234], {
startPts: 6778809897,
endPts: 6779104191,
text: 'I_guess_I\'ll_just_have\nto_duck_a_little_bit.',
stream: 'cc708_1'
}, 'parsed caption 234 correctly');
});
QUnit.test('Decodes multibyte characters if valid encoding option is provided and TextDecoder is supported', function(assert) {
var captions = [];
cea708Stream = new m2ts.Cea708Stream({
captionServices: {
SERVICE1: {
encoding: 'euc-kr'
}
}
});
cea708Stream.on('data', function(caption) {
captions.push(caption);
});
cc708Korean.forEach(cea708Stream.push, cea708Stream);
cea708Stream.flushDisplayed(4721138662, cea708Stream.services[1]);
assert.equal(captions.length, 1, 'parsed single caption correctly');
if (window.TextDecoder) {
assert.ok(cea708Stream.services[1].textDecoder_, 'TextDecoder created when supported');
assert.equal(
captions[0].text,
'니가 ',
'parsed multibyte characters correctly'
);
} else {
assert.notOk(cea708Stream.services[1].textDecoder_, 'TextDecoder not created when unsupported');
}
});
QUnit.test('Decodes multi-byte characters as unicode if no valid encoding option is provided', function(assert) {
var captions = [];
cea708Stream = new m2ts.Cea708Stream({
captionServices: {
SERVICE1: {}
}
});
cea708Stream.on('data', function(caption) {
captions.push(caption);
});
cc708Korean.forEach(cea708Stream.push, cea708Stream);
cea708Stream.flushDisplayed(4721138662, cea708Stream.services[1]);
assert.equal(captions.length, 1, 'parsed single caption correctly');
assert.notOk(cea708Stream.services[1].textDecoder_, 'TextDecoder was not created');
assert.equal(
captions[0].text,
'듏낡 ',
'parsed multibyte characters correctly'
);
});
QUnit.test('Creates TextDecoder only if valid encoding value is provided', function(assert) {
var secondCea708Stream;
cea708Stream = new m2ts.Cea708Stream({
captionServices: {
SERVICE1: {
encoding: 'euc-kr'
}
}
});
cc708Korean.forEach(cea708Stream.push, cea708Stream);
cea708Stream.flushDisplayed(4721138662, cea708Stream.services[1]);
if (window.TextDecoder) {
assert.ok(cea708Stream.services[1].textDecoder_, 'TextDecoder created successfully when encoding is valid');
}
secondCea708Stream = new m2ts.Cea708Stream({
captionServices: {
SERVICE1: {
encoding: 'invalid'
}
}
});
cc708Korean.forEach(secondCea708Stream.push, secondCea708Stream);
secondCea708Stream.flushDisplayed(4721138662, secondCea708Stream.services[1]);
assert.notOk(secondCea708Stream.services[1].textDecoder_, 'TextDecoder not created when encoding is invalid');
});
QUnit.test('reset command', function(assert) {
var captions = [];
cea708Stream.on('data', function(caption) {
captions.push(caption);
});
[
{ type: 3, pts: 153315036, ccData: 0x8322 },
{ type: 2, pts: 153315036, ccData: 0x4820 },
{ type: 2, pts: 153315036, ccData: 0x0000 },
{ type: 3, pts: 153318039, ccData: 0xc322 },
{ type: 2, pts: 153318039, ccData: 0x4953 },
{ type: 2, pts: 153318039, ccData: 0x0000 },
{ type: 3, pts: 153387108, ccData: 0x0628 },
{ type: 2, pts: 153387108, ccData: 0x0d90 },
{ type: 2, pts: 153387108, ccData: 0x0503 },
{ type: 2, pts: 153387108, ccData: 0x912a },
{ type: 2, pts: 153387108, ccData: 0x002a },
{ type: 2, pts: 153387108, ccData: 0x0000 },
{ type: 3, pts: 153405126, ccData: 0x4da2 },
{ type: 2, pts: 153405126, ccData: 0x8c0f },
{ type: 2, pts: 153405126, ccData: 0x628c },
{ type: 2, pts: 153405126, ccData: 0x0f31 },
{ type: 2, pts: 153405126, ccData: 0x983b },
{ type: 2, pts: 153405126, ccData: 0x912a },
{ type: 2, pts: 153405126, ccData: 0x8f00 },
{ type: 2, pts: 153405126, ccData: 0x611f },
{ type: 2, pts: 153405126, ccData: 0x002a },
{ type: 2, pts: 153405126, ccData: 0x1090 },
{ type: 2, pts: 153405126, ccData: 0x0503 },
{ type: 3, pts: 153408129, ccData: 0x8a31 },
{ type: 2, pts: 153408129, ccData: 0x9201 },
{ type: 2, pts: 153408129, ccData: 0x983b },
// RST (Reset command)
{ type: 2, pts: 153408129, ccData: 0x8f00 },
{ type: 2, pts: 153408129, ccData: 0x0000 },
{ type: 2, pts: 153408129, ccData: 0x611f },
{ type: 2, pts: 153408129, ccData: 0x1090 },
{ type: 2, pts: 153408129, ccData: 0x0000 },
{ type: 2, pts: 153408129, ccData: 0x0503 },
{ type: 2, pts: 153408129, ccData: 0x912a },
{ type: 2, pts: 153408129, ccData: 0x0000 },
{ type: 2, pts: 153408129, ccData: 0x9201 },
{ type: 3, pts: 153414135, ccData: 0xc322 },
{ type: 2, pts: 153414135, ccData: 0x434f },
{ type: 2, pts: 153414135, ccData: 0x0000 }
].forEach(cea708Stream.push, cea708Stream);
assert.equal(captions.length, 1, 'parsed 1 caption');
assert.deepEqual(captions[0], {
startPts: 153315036,
endPts: 153408129,
text: '*\n;',
stream: 'cc708_1'
}, 'parsed the caption correctly');
});
QUnit.test('windowing', function(assert) {
var captions = [];
cea708Stream.on('data', function(caption) {
captions.push(caption);
});
[
{ type: 3, pts: 1000, ccData: packetHeader708(0, 3, 1, 4) },
{ type: 2, pts: 1000, ccData: 0x8000 }, // CW0
{ type: 2, pts: 1000, ccData: characters('w0') },
{ type: 3, pts: 1000, ccData: packetHeader708(1, 3, 1, 4) },
{ type: 2, pts: 1000, ccData: 0x8100 }, // CW1
{ type: 2, pts: 1000, ccData: characters('w1') },
{ type: 3, pts: 1000, ccData: packetHeader708(2, 3, 1, 4) },
{ type: 2, pts: 1000, ccData: 0x8200 }, // CW2
{ type: 2, pts: 1000, ccData: characters('w2') },
{ type: 3, pts: 1000, ccData: packetHeader708(0, 3, 1, 4) },
{ type: 2, pts: 1000, ccData: 0x8300 }, // CW3
{ type: 2, pts: 1000, ccData: characters('w3') },
{ type: 3, pts: 1000, ccData: packetHeader708(1, 3, 1, 4) },
{ type: 2, pts: 1000, ccData: 0x8400 }, // CW4
{ type: 2, pts: 1000, ccData: characters('w4') },
{ type: 3, pts: 1000, ccData: packetHeader708(2, 3, 1, 4) },
{ type: 2, pts: 1000, ccData: 0x8500 }, // CW5
{ type: 2, pts: 1000, ccData: characters('w5') },
{ type: 3, pts: 1000, ccData: packetHeader708(0, 3, 1, 4) },
{ type: 2, pts: 1000, ccData: 0x8600 }, // CW6
{ type: 2, pts: 1000, ccData: characters('w6') },
{ type: 3, pts: 1000, ccData: packetHeader708(1, 3, 1, 4) },
{ type: 2, pts: 1000, ccData: 0x8700 }, // CW7
{ type: 2, pts: 1000, ccData: characters('w7') },
{ type: 3, pts: 2000, ccData: packetHeader708(2, 3, 1, 4) },
{ type: 2, pts: 2000, ccData: 0x8aff }, // HDW (Hide all)
{ type: 2, pts: 2000, ccData: displayWindows708([0]) },
{ type: 3, pts: 3000, ccData: packetHeader708(0, 3, 1, 4) },
{ type: 2, pts: 3000, ccData: 0x8aff }, // HDW (Hide all)
{ type: 2, pts: 3000, ccData: displayWindows708([1]) },
{ type: 3, pts: 4000, ccData: packetHeader708(1, 3, 1, 4) },
{ type: 2, pts: 4000, ccData: 0x8aff }, // HDW (Hide all)
{ type: 2, pts: 4000, ccData: displayWindows708([2, 3]) },
{ type: 3, pts: 5000, ccData: packetHeader708(2, 3, 1, 4) },
{ type: 2, pts: 5000, ccData: 0x8aff }, // HDW (Hide all)
{ type: 2, pts: 5000, ccData: displayWindows708([3, 4]) },
{ type: 3, pts: 6000, ccData: packetHeader708(0, 3, 1, 4) },
{ type: 2, pts: 6000, ccData: 0x8aff }, // HDW (Hide all)
{ type: 2, pts: 6000, ccData: displayWindows708([5, 6, 7]) },
{ type: 3, pts: 7000, ccData: packetHeader708(1, 2, 1, 2) },
{ type: 2, pts: 7000, ccData: 0x8aff }, // HDW (Hide all)
// Indicate end of last packet
{ type: 3, pts: 8000, ccData: packetHeader708(2, 1, 1, 0) }
].forEach(cea708Stream.push, cea708Stream);
assert.equal(captions.length, 5, 'parsed 5 captions');
assert.deepEqual(captions[0], {
startPts: 2000,
endPts: 3000,
text: 'w0',
stream: 'cc708_1'
}, 'parsed caption 0 correctly');
assert.deepEqual(captions[1], {
startPts: 3000,
endPts: 4000,
text: 'w1',
stream: 'cc708_1'
}, 'parsed caption 1 correctly');
assert.deepEqual(captions[2], {
startPts: 4000,
endPts: 5000,
text: 'w2\n\nw3',
stream: 'cc708_1'
}, 'parsed caption 2 correctly');
assert.deepEqual(captions[3], {
startPts: 5000,
endPts: 6000,
text: 'w3\n\nw4',
stream: 'cc708_1'
}, 'parsed caption 3 correctly');
assert.deepEqual(captions[4], {
startPts: 6000,
endPts: 7000,
text: 'w5\n\nw6\n\nw7',
stream: 'cc708_1'
}, 'parsed caption 4 correctly');
});
QUnit.test('backspace', function(assert) {
var captions = [];
cea708Stream.on('data', function(caption) {
captions.push(caption);
});
[
{ type: 3, pts: 1000, ccData: packetHeader708(0, 7, 1, 12) },
{ type: 2, pts: 1000, ccData: 0x8000 }, // CW0
{ type: 2, pts: 1000, ccData: characters('ty') },
{ type: 2, pts: 1000, ccData: characters('op') },
{ type: 2, pts: 1000, ccData: 0x0808 }, // BS BS: Backspace twice
{ type: 2, pts: 1000, ccData: characters('po') },
{ type: 2, pts: 1000, ccData: displayWindows708([0]) },
{ type: 3, pts: 2000, ccData: packetHeader708(1, 2, 1, 2) },
{ type: 2, pts: 2000, ccData: 0x8aff },
// Indicate end of last packet
{ type: 3, pts: 3000, ccData: packetHeader708(2, 1, 1, 0) }
].forEach(cea708Stream.push, cea708Stream);
assert.equal(captions.length, 1, 'parsed 1 caption');
assert.equal(captions[0].text, 'typo', 'parsed caption with backspaces correctly');
});
QUnit.test('extended character set', function(assert) {
var captions = [];
cea708Stream.on('data', function(caption) {
captions.push(caption);
});
[
{ type: 3, pts: 1000, ccData: packetHeader708(0, 7, 1, 12) },
{ type: 2, pts: 1000, ccData: displayWindows708([0]) },
{ type: 2, pts: 1000, ccData: 0x8000 }, // CW0
{ type: 2, pts: 1000, ccData: 0x103f }, // Ÿ
{ type: 2, pts: 1000, ccData: 0x1035 }, // •
{ type: 2, pts: 1000, ccData: 0x103f }, // Ÿ
{ type: 2, pts: 1000, ccData: 0x0020 },
{ type: 3, pts: 1000, ccData: packetHeader708(1, 7, 1, 12) },
{ type: 2, pts: 1000, ccData: 0x103c }, // œ
{ type: 2, pts: 1000, ccData: 0x102a }, // Š
{ type: 2, pts: 1000, ccData: 0x1025 }, // …
{ type: 2, pts: 1000, ccData: 0x102a }, // Š
{ type: 2, pts: 1000, ccData: 0x103c }, // œ
{ type: 2, pts: 1000, ccData: 0x0020 },
{ type: 3, pts: 1000, ccData: packetHeader708(2, 5, 1, 8) },
{ type: 2, pts: 1000, ccData: 0x1033 }, // “
{ type: 2, pts: 1000, ccData: 0x103d }, // ℠
{ type: 2, pts: 1000, ccData: 0x1034 }, // ”
{ type: 2, pts: 1000, ccData: 0x1039 }, // ™
{ type: 3, pts: 2000, ccData: packetHeader708(0, 2, 1, 2) },
{ type: 2, pts: 2000, ccData: 0x8aff },
// Indicate end of last packet
{ type: 3, pts: 3000, ccData: packetHeader708(1, 1, 1, 0) }
].forEach(cea708Stream.push, cea708Stream);
assert.equal(captions.length, 1, 'parsed 1 caption');
assert.equal(captions[0].text, 'Ÿ•Ÿ œŠ…Šœ “℠”™', 'parsed extended characters correctly');
});
QUnit.test('roll up', function(assert) {
var captions = [];
cea708Stream.on('data', function(caption) {
captions.push(caption);
});
[
// Define window with two virtual rows (rowCount = 1)
{ type: 3, pts: 1000, ccData: packetHeader708(0, 4, 1, 6) },
{ type: 2, pts: 1000, ccData: 0x983b },
{ type: 2, pts: 1000, ccData: 0x8f00 },
{ type: 2, pts: 1000, ccData: 0x611f },
{ type: 3, pts: 1000, ccData: packetHeader708(1, 3, 1, 4) },
{ type: 2, pts: 1000, ccData: characters('L1') },
{ type: 2, pts: 1000, ccData: 0x0d00 }, // CR
{ type: 3, pts: 2000, ccData: packetHeader708(2, 3, 1, 4) },
{ type: 2, pts: 2000, ccData: characters('L2') },
{ type: 2, pts: 2000, ccData: 0x0d00 }, // CR
{ type: 3, pts: 3000, ccData: packetHeader708(0, 3, 1, 4) },
{ type: 2, pts: 3000, ccData: characters('L3') },
{ type: 2, pts: 3000, ccData: 0x0d00 }, // CR
{ type: 3, pts: 4000, ccData: packetHeader708(1, 3, 1, 4) },
{ type: 2, pts: 4000, ccData: characters('L4') },
{ type: 2, pts: 4000, ccData: 0x0d00 }, // CR
{ type: 3, pts: 5000, ccData: packetHeader708(2, 2, 1, 2) },
{ type: 2, pts: 5000, ccData: 0x8aff },
// Indicate end of last packet
{ type: 3, pts: 6000, ccData: packetHeader708(0, 1, 1, 0) }
].forEach(cea708Stream.push, cea708Stream);
assert.equal(captions.length, 3, 'parsed 3 captions');
assert.equal(captions[0].text, 'L1\nL2', 'parsed caption 1 correctly');
assert.equal(captions[1].text, 'L2\nL3', 'parsed caption 2 correctly');
assert.equal(captions[2].text, 'L3\nL4', 'parsed caption 3 correctly');
});