Files
shaka-player/test/cea/cea_decoder_unit.js
T
Muhammad Haris 1c00b4c1fb feat(text): CEA-608 Decoder (#2731)
This replaces mux.js for CEA608 decoding.  Applications will no longer need to include mux.js for CEA support, and mux.js will only be necessary for TS transmuxing.

Closes #2648
2020-08-07 09:07:26 -07:00

494 lines
16 KiB
JavaScript

/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
describe('CeaDecoder', () => {
const CeaUtils = shaka.test.CeaUtils;
/** @type {!string} */
const DEFAULT_BG_COLOR = shaka.cea.Cea608Memory.DEFAULT_BG_COLOR;
/**
* Initialization bytes for CC packet.
* Includes padding bytes, USA country code, and ATSC provider code.
* @type {!Uint8Array}
*/
const atscCaptionInitBytes = new Uint8Array([
0xb5, // USA country code.
0x00, 0x31, // ATSC provider code.
0x47, 0x41, 0x39, 0x34, // ATSC user identifier.
0x03, // User data type for cc_data.
]);
/** @type {!shaka.cea.CeaDecoder} */
const decoder = new shaka.cea.CeaDecoder();
const edmCodeByte2 = 0x2c; // Erase displayed memory byte 2.
// Blank padding control code between two control codes that are the same.
const blankPaddingControlCode = new Uint8Array([0x97, 0x23]);
// Erases displayed memory on every captioning mode.
const eraseDisplayedMemory = new Uint8Array([
...atscCaptionInitBytes, 0xc4, /* padding= */ 0xff,
0xfc, 0x94, edmCodeByte2, // EDM on CC1
0xfc, 0x1c, edmCodeByte2, // EDM on CC2
0xfd, 0x15, edmCodeByte2, // EDM on CC3
0xfd, 0x9d, edmCodeByte2, // EDM on CC4
]);
beforeEach(() => {
decoder.clear();
});
it('green and underlined popon caption data on CC3', () => {
const controlCount = 0x08;
const captionData = 0xc0 | controlCount;
const greenTextCC3Packet = new Uint8Array([
...atscCaptionInitBytes, captionData, /* padding= */ 0xff,
0xfd, 0x15, 0x20, // Pop-on mode (RCL control code)
0xfd, 0x13, 0xe3, // PAC to underline and color text green on last row.
0xfd, 0x67, 0xf2, // g, r
0xfd, 0xe5, 0xe5, // e, e
0xfd, 0x6e, 0x20, // n, space
0xfd, 0xf4, 0xe5, // t, e
0xfd, 0xf8, 0xf4, // x, t
0xfd, 0x15, 0x2f, // EOC
]);
const startTimeCaption1 = 1;
const startTimeCaption2 = 2;
const expectedText = 'green text';
const topLevelCue = new shaka.text.Cue(
startTimeCaption1, startTimeCaption2, '');
topLevelCue.nestedCues = [
CeaUtils.createStyledCue(
startTimeCaption1, startTimeCaption2, expectedText,
/* underline= */ true, /* italics= */ false,
/* textColor= */ 'green', /* backgroundColor= */ 'black'),
];
const expectedCaptions = [
{
stream: 'CC3',
cue: topLevelCue,
},
];
decoder.extract(greenTextCC3Packet, startTimeCaption1);
decoder.extract(eraseDisplayedMemory, startTimeCaption2);
const captions = decoder.decode();
expect(captions).toEqual(expectedCaptions);
});
it('popon captions that change color and underline midrow on CC2', () => {
const controlCount = 0x08;
const captionData = 0xc0 | controlCount;
const midrowStyleChangeCC2Packet = new Uint8Array([
...atscCaptionInitBytes, captionData, /* padding= */ 0xff,
0xfc, 0x1c, 0x20, // Pop-on mode (RCL control code).
0xfc, 0xad, 0xad, // -, -
0xfc, 0x19, 0x29, // Red + underline midrow style control code.
0xfc, 0xf2, 0xe5, // r, e
0xfc, 0x64, 0x80, // d, invalid
0xfc, 0x19, 0x20, // Midrow style control code to clear styles.
0xfc, 0xad, 0xad, // -, -
0xfc, 0x1c, 0x2f, // EOC
]);
const startTimeCaption1 = 1;
const startTimeCaption2 = 2;
const expectedText1 = '-- ';
const expectedText2 = 'red';
const expectedText3 = ' --';
// Since there are three style changes, there should be three nested cues.
const topLevelCue = new shaka.text.Cue(
startTimeCaption1, startTimeCaption2, '');
topLevelCue.nestedCues = [
CeaUtils.createDefaultCue(
startTimeCaption1, startTimeCaption2, expectedText1),
CeaUtils.createStyledCue(
startTimeCaption1, startTimeCaption2, expectedText2,
/* underline= */ true, /* italics= */ false,
/* textColor= */ 'red', /* backgroundColor= */ DEFAULT_BG_COLOR),
CeaUtils.createDefaultCue(
startTimeCaption1, startTimeCaption2, expectedText3),
];
const expectedCaptions = [
{
stream: 'CC2',
cue: topLevelCue,
},
];
decoder.extract(midrowStyleChangeCC2Packet, startTimeCaption1);
decoder.extract(eraseDisplayedMemory, startTimeCaption2);
const captions = decoder.decode();
expect(captions).toEqual(expectedCaptions);
});
it('italicized popon captions on a yellow background on CC2', () => {
const controlCount = 0x08;
const captionData = 0xc0 | controlCount;
const midrowStyleChangeCC2Packet = new Uint8Array([
...atscCaptionInitBytes, captionData, /* padding= */ 0xff,
0xfc, 0x1c, 0x20, // Pop-on mode (RCL control code).
0xfc, 0x19, 0x6e, // White Italics PAC.
0xfc, 0x98, 0x2a, // Background attribute yellow.
0xfc, 0xf4, 0xe5, // t, e
0xfc, 0x73, 0xf4, // s, t
0xfc, 0x19, 0x20, // Midrow style control code to clear styles.
0xfc, 0x98, 0x20, // Background attribute to clear background.
0xfc, 0x1c, 0x2f, // EOC
]);
const startTimeCaption1 = 1;
const startTimeCaption2 = 2;
const expectedText = 'test';
// 2 nested cues, one contains styled text, one contains a midrow space.
const topLevelCue = new shaka.text.Cue(startTimeCaption1,
startTimeCaption2, '');
topLevelCue.nestedCues = [
CeaUtils.createStyledCue(
startTimeCaption1, startTimeCaption2, expectedText,
/* underline= */ false, /* italics= */ true,
/* textColor= */ 'white', /* backgroundColor= */ 'yellow'),
CeaUtils.createStyledCue(
startTimeCaption1, startTimeCaption2, /* payload= */ ' ',
/* underline= */ false, /* italics= */ false,
/* textColor= */ 'white', /* backgroundColor= */ 'yellow'),
];
const expectedCaptions = [{
stream: 'CC2',
cue: topLevelCue,
}];
decoder.extract(midrowStyleChangeCC2Packet, startTimeCaption1);
decoder.extract(eraseDisplayedMemory, startTimeCaption2);
const captions = decoder.decode();
expect(captions).toEqual(expectedCaptions);
});
it('popon captions with special characters on CC2', () => {
const controlCount = 0x07;
const captionData = 0xc0 | controlCount;
const midrowStyleChangeCC2Packet = new Uint8Array([
...atscCaptionInitBytes, captionData, /* padding= */ 0xff,
0xfc, 0x1c, 0x20, // Pop-on mode (RCL control code).
0xfc, 0x19, 0x37, // Special North American character (♪)
0xfc, 0x20, 0x80, // SP, invalid. SP will be replaced by extended char.
0xfc, 0x1a, 0x25, // Extended Spanish/Misc character (ü)
0xfc, 0x20, 0x80, // SP, invalid.
0xfc, 0x9b, 0xb9, // Extended German/Danish character (å)
0xfc, 0x1c, 0x2f, // EOC
]);
const startTimeCaption1 = 1;
const startTimeCaption2 = 2;
const expectedText = '♪üå';
const topLevelCue = new shaka.text.Cue(startTimeCaption1,
startTimeCaption2, '');
topLevelCue.nestedCues = [
CeaUtils.createDefaultCue(
startTimeCaption1, startTimeCaption2, expectedText),
];
const expectedCaptions = [{
stream: 'CC2',
cue: topLevelCue,
}];
decoder.extract(midrowStyleChangeCC2Packet, startTimeCaption1);
decoder.extract(eraseDisplayedMemory, startTimeCaption2);
const captions = decoder.decode();
expect(captions).toEqual(expectedCaptions);
});
it('painton captions on CC1', () => {
const controlCount = 0x03;
const captionData = 0xc0 | controlCount;
const paintonCaptionCC1Packet = new Uint8Array([
...atscCaptionInitBytes, captionData, /* padding= */ 0xff,
0xfc, 0x94, 0x29, // Paint-on mode (RDC control code).
0xfc, 0xf4, 0xe5, // t, e
0xfc, 0x73, 0xf4, // s, t
]);
const startTimeCaption1 = 1;
const startTimeCaption2 = 2;
const expectedText = 'test';
const topLevelCue = new shaka.text.Cue(startTimeCaption1,
startTimeCaption2, '');
topLevelCue.nestedCues = [
CeaUtils.createDefaultCue(
startTimeCaption1, startTimeCaption2, expectedText),
];
const expectedCaptions = [{
stream: 'CC1',
cue: topLevelCue,
}];
decoder.extract(paintonCaptionCC1Packet, startTimeCaption1);
decoder.extract(eraseDisplayedMemory, startTimeCaption2);
const captions = decoder.decode();
expect(captions).toEqual(expectedCaptions);
});
it('rollup captions (2 lines) on CC1', () => {
const controlCount1 = 0x03;
const controlCount2 = 0x02;
const stream = 'CC1';
const time1 = 1;
const time2 = 2;
const time3 = 3;
const time4 = 4;
const time5 = 5;
// Carriage return on CC1
const carriageReturnControlCode = new Uint8Array([0x94, 0xad]);
const packets = [
new Uint8Array([
...atscCaptionInitBytes, 0xc0 | controlCount1, /* padding= */ 0xff,
0xfc, 0x94, 0x25, // Roll-up 2 rows control code.
0xfc, ...carriageReturnControlCode,
0xfc, ...blankPaddingControlCode,
]),
new Uint8Array([
...atscCaptionInitBytes, 0xc0 | controlCount1, /* padding= */ 0xff,
0xfc, 0x31, 0xae, // 1, .
0xfc, ...carriageReturnControlCode,
0xfc, ...blankPaddingControlCode,
]),
new Uint8Array([
...atscCaptionInitBytes, 0xc0 | controlCount1, /* padding= */ 0xff,
0xfc, 0x32, 0xae, // 2, .
0xfc, ...carriageReturnControlCode,
0xfc, ...blankPaddingControlCode,
]),
new Uint8Array([
...atscCaptionInitBytes, 0xc0 | controlCount1, /* padding= */ 0xff,
0xfc, 0xb3, 0xae, // 3, .
0xfc, ...carriageReturnControlCode,
0xfc, ...blankPaddingControlCode,
]),
new Uint8Array([
...atscCaptionInitBytes, 0xc0 | controlCount2, /* padding= */ 0xff,
0xfc, 0x34, 0xae, // 4, .
0xfc, 0x94, 0x2f, // EOC
]),
];
for (let i = 0; i < packets.length; i++) {
decoder.extract(packets[i], i+1);
}
decoder.extract(eraseDisplayedMemory, 6);
// Top level cue corresponding to the first closed caption.
const topLevelCue1 = new shaka.text.Cue(
/* startTime= */ time1, /* endTime= */ time2, '');
topLevelCue1.nestedCues = [
CeaUtils.createDefaultCue(
/* startTime= */ time1, /* endTime= */ time2, /* payload= */ '1.'),
];
// Top level cue corresponding to the second closed caption.
const topLevelCue2 = new shaka.text.Cue(
/* startTime= */ time2, /* endTime= */ time3, '');
topLevelCue2.nestedCues = [
CeaUtils.createDefaultCue(
/* startTime= */ time2, /* endTime= */ time3, /* payload= */ '1.'),
CeaUtils.createLineBreakCue(
/* startTime= */ time2, /* endTime= */ time3),
CeaUtils.createDefaultCue(
/* startTime= */ time2, /* endTime= */ time3, /* payload= */ '2.'),
];
// Top level cue corresponding to the third closed caption.
const topLevelCue3 = new shaka.text.Cue(
/* startTime= */ time3, /* endTime= */ time4, '');
topLevelCue3.nestedCues = [
CeaUtils.createDefaultCue(
/* startTime= */ time3, /* endTime= */ time4, /* payload= */ '2.'),
CeaUtils.createLineBreakCue(
/* startTime= */ time3, /* endTime= */ time4),
CeaUtils.createDefaultCue(
/* startTime= */ time3, /* endTime= */ time4, /* payload= */ '3.'),
];
// Top level cue corresponding to the fourth closed caption.
const topLevelCue4 = new shaka.text.Cue(
/* startTime= */ time4, /* endTime= */ time5, '');
topLevelCue4.nestedCues = [
CeaUtils.createDefaultCue(
/* startTime= */ time4, /* endTime= */ time5, /* payload= */ '3.'),
CeaUtils.createLineBreakCue(
/* startTime= */ time4, /* endTime= */ time5),
CeaUtils.createDefaultCue(
/* startTime= */ time4, /* endTime= */ time5, /* payload= */ '4.'),
];
const expectedCaptions = [
{
stream,
cue: topLevelCue1,
},
{
stream,
cue: topLevelCue2,
},
{
stream,
cue: topLevelCue3,
},
{
stream,
cue: topLevelCue4,
},
];
const captions = decoder.decode();
expect(captions).toEqual(expectedCaptions);
});
it('PAC shifts entire 2-line rollup window to a new row on CC1', () => {
const controlCount1 = 0x03;
const controlCount2 = 0x02;
const stream = 'CC1';
// Carriage return on CC1
const carriageReturnControlCode = new Uint8Array([0x94, 0xad]);
const packets = [
new Uint8Array([
...atscCaptionInitBytes, 0xc0 | controlCount1, /* padding= */ 0xff,
0xfc, 0x94, 0x25, // Roll-up 2 rows control code.
0xfc, ...carriageReturnControlCode,
0xfc, 0x97, 0x23, // Blank padding control code
]),
new Uint8Array([
...atscCaptionInitBytes, 0xc0 | controlCount1, /* padding= */ 0xff,
0xfc, 0x31, 0xae, // 1, .
0xfc, ...carriageReturnControlCode,
0xfc, 0x97, 0x23, // Blank padding control code
]),
new Uint8Array([
...atscCaptionInitBytes, 0xc0 | controlCount2, /* padding= */ 0xff,
0xfc, 0x32, 0xae, // 2, .
0xfc, 0x92, 0xe0, // PAC control code to move to row 4.
]),
];
for (let i = 0; i < packets.length; i++) {
decoder.extract(packets[i], i+1);
}
decoder.extract(eraseDisplayedMemory, 3);
// Top level cue corresponding to the first closed caption.
const topLevelCue1 = new shaka.text.Cue(/* startTime= */ 1,
/* endTime= */ 2, '');
topLevelCue1.nestedCues = [
CeaUtils.createDefaultCue(
/* startTime= */ 1, /* endTime= */ 2, /* payload= */ '1.'),
];
// Top level cue corresponding to the second closed caption.
const topLevelCue2 = new shaka.text.Cue(/* startTime= */ 2,
/* endTime= */ 3, '');
topLevelCue2.nestedCues = [
CeaUtils.createDefaultCue(
/* startTime= */ 2, /* endTime= */ 3, /* payload= */ '1.'),
CeaUtils.createLineBreakCue(/* startTime= */ 2, /* endTime= */ 3),
CeaUtils.createDefaultCue(
/* startTime= */ 2, /* endTime= */ 3, /* payload= */ '2.'),
];
const expectedCaptions = [
{
stream,
cue: topLevelCue1,
},
{
stream,
cue: topLevelCue2,
},
];
const captions = decoder.decode();
expect(captions).toEqual(expectedCaptions);
});
it('does not emit text sent while in CEA-608 Text Mode', () => {
const controlCount = 0x03;
const captionData = 0xc0 | controlCount;
const textModePacket = new Uint8Array([
...atscCaptionInitBytes, captionData, /* padding= */ 0xff,
0xfc, 0x94, 0x2a, // Text mode (Text restart control code).
0xfc, 0xf4, 0xe5, // t, e
0xfc, 0x73, 0xf4, // s, t
]);
const startTimeCaption1 = 1;
const startTimeCaption2 = 2;
decoder.extract(textModePacket, startTimeCaption1);
decoder.extract(eraseDisplayedMemory, startTimeCaption2);
const captions = decoder.decode();
expect(captions).toEqual([]);
});
it('resets the decoder on >=45 consecutive bad frames', () => {
// CEA-608-B C.21 says to reset the decoder after 45 invalid frames.
const controlCount = 0x0f;
const captionData = 0xc0 | controlCount;
const badFrames = [];
const badFrameCount = 15;
for (let i = 0; i<badFrameCount; i++) {
// Without loss of generality, the bad frames will be sent on CC1.
badFrames.push(0xfc, 0x0, 0x0);
}
const badFramesBuffer = new Uint8Array([
...atscCaptionInitBytes, captionData, /* padding= */ 0xff,
...new Uint8Array(badFrames),
]);
// 3*15 = 45 total bad frames extracted.
for (let i = 0; i < 3; i++) {
decoder.extract(badFramesBuffer, i+1);
}
spyOn(decoder, 'reset').and.callThrough();
decoder.decode();
expect(decoder.reset).toHaveBeenCalledTimes(1);
});
});