UNPKG

shaka-player

Version:
609 lines (521 loc) 21.3 kB
/*! @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.CeaUtils.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(); describe('decodes CEA-608', () => { 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.line = 81.25; topLevelCue.lineInterpretation = shaka.text.Cue.lineInterpretation.PERCENTAGE; 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.line = 6.25; topLevelCue.lineInterpretation = shaka.text.Cue.lineInterpretation.PERCENTAGE; 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'; // A single nested cue containing yellow, italicized text. const topLevelCue = new shaka.text.Cue(startTimeCaption1, startTimeCaption2, ''); topLevelCue.line = 12.5; topLevelCue.lineInterpretation = shaka.text.Cue.lineInterpretation.PERCENTAGE; topLevelCue.nestedCues = [ CeaUtils.createStyledCue( startTimeCaption1, startTimeCaption2, expectedText, /* underline= */ false, /* italics= */ true, /* 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.line = 6.25; topLevelCue.lineInterpretation = shaka.text.Cue.lineInterpretation.PERCENTAGE; 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.line = 6.25; topLevelCue.lineInterpretation = shaka.text.Cue.lineInterpretation.PERCENTAGE; 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.line = 93.75; topLevelCue1.lineInterpretation = shaka.text.Cue.lineInterpretation.PERCENTAGE; 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.line = 93.75; topLevelCue2.lineInterpretation = shaka.text.Cue.lineInterpretation.PERCENTAGE; 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.line = 93.75; topLevelCue3.lineInterpretation = shaka.text.Cue.lineInterpretation.PERCENTAGE; 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.line = 93.75; topLevelCue4.lineInterpretation = shaka.text.Cue.lineInterpretation.PERCENTAGE; 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.line = 93.75; topLevelCue1.lineInterpretation = shaka.text.Cue.lineInterpretation.PERCENTAGE; 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.line = 25; topLevelCue2.lineInterpretation = shaka.text.Cue.lineInterpretation.PERCENTAGE; 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); }); }); describe('decodes CEA-708', () => { // Hide window (2 bytes), with a bitmap provided to indicate all windows. const hideWindow = new Uint8Array([ ...atscCaptionInitBytes, 0xc2, /* padding= */ 0xff, 0xff, 0x02, 0x22, // Service #1, and 2 bytes will follow. 0xfe, 0x8a, 0xff, ]); it('well-formed caption packet that contains valid control codes', () => { const startTime = 1; const endTime = 2; const bytePairCount = 0x07; const captionData = 0xc0 | bytePairCount; const serviceNumber = 1; const cea708Packet = new Uint8Array([ ...atscCaptionInitBytes, captionData, /* padding= */ 0xff, // Byte 1 (0x07) is a DTVCC_PACKET_START that states 7 * 2 - 1 bytes // will follow. Byte 2 is a service block header that selects service # // and states that there are 12 bytes that will follow in the block. 0xff, 0x07, (serviceNumber << 5) | 12, // Define window (7 bytes). Visible window #0 with 10 rows, 10 columns. 0xfe, 0x98, 0x38, 0xfe, 0x00, 0x00, 0xfe, 0x0a, 0x0a, 0xfe, 0x00, // Series of G0 control codes that add text 0x74, // t 0xfe, 0x65, 0x73, // e, s 0xfe, 0x74, 0x00, // t, padding ]); decoder.extract(cea708Packet, startTime); decoder.extract(hideWindow, endTime); const text = 'test'; const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text), ]; const expectedCaptions = [ { stream: 'svc1', cue: topLevelCue, }, ]; const captions = decoder.decode(); expect(captions).toEqual(expectedCaptions); }); it('service block contains a corrupted header', () => { const startTime = 1; const endTime = 2; const bytePairCount = 0x02; const captionData = 0xc0 | bytePairCount; const serviceNumber = 1; const cea708Packet = new Uint8Array([ ...atscCaptionInitBytes, captionData, /* padding= */ 0xff, // Byte 1 (0x01) is DTVCC_PACKET_START that states 1 * 2 - 1 bytes // will follow. Byte 2 is a service block header that selects service # // and states that there are 12 bytes that will follow in the block. 0xff, 0x01, (serviceNumber << 5) | 12, 0xfe, 0x00, 0x00, ]); // The data corrupted, since the service block header claimed 12 bytes // would follow, but only two bytes followed. decoder.extract(cea708Packet, startTime); decoder.extract(hideWindow, endTime); // Then we should have warned of the invalid data and stopped processing // the block without interrupting playback. spyOn(shaka.log, 'warnOnce').and.callThrough(); const captions = decoder.decode(); expect(shaka.log.warnOnce).toHaveBeenCalledWith('CEA708_INVALID_DATA', 'Buffer read out of bounds / invalid CEA-708 Data.'); expect(shaka.log.warnOnce).toHaveBeenCalledTimes(1); expect(captions).toEqual([]); }); }); });