UNPKG

shaka-player

Version:
675 lines (556 loc) 21.9 kB
/*! @license * Shaka Player * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ describe('Cea708Service', () => { const CeaUtils = shaka.test.CeaUtils; /** @type {!shaka.cea.Cea708Service} */ let service; /** * Hide window (2 bytes), with a bitmap provided to indicate all windows. * @type {!Array<number>} */ const hideWindow = [0x8a, 0xff]; /** * Define window (7 bytes), defines window #0 to be a visible window * with 32 rows and 32 columns. (We specify 31 for each since decoder adds 1). * @type {!Array<number>} */ const defineWindow = [ 0x98, 0x38, 0x00, 0x00, 0x1f, 0x1f, 0x00, ]; /** @type {number} */ const startTime = 1; /** @type {number} */ const endTime = 2; /** * We arbitrarily pick service 1 for all of these tests. * @type {number} */ const serviceNumber = 1; /** @type {string} */ const stream = `svc${serviceNumber}`; /** * Takes in a array of bytes and a presentation timestamp (in seconds), * and converts it into a CEA-708 DTVCC Packet. * @param {!Array<number>} bytes * @param {number} pts */ const createCea708PacketFromBytes = (bytes, pts) => { const cea708Bytes = bytes.map((code, i) => { return { pts, type: shaka.cea.DtvccPacketBuilder.DTVCC_PACKET_DATA, value: code, order: i, }; }); return new shaka.cea.DtvccPacket(cea708Bytes); }; /** * Takes in a CEA-708 service and array of 708 packets with control codes, * and returns all the captions inside of them, using the service to decode. * @param {!shaka.cea.Cea708Service} service * @param {...!shaka.cea.DtvccPacket} packets */ const getCaptionsFromPackets = (service, ...packets) => { const captions = []; for (const packet of packets) { while (packet.hasMoreData()) { const caption = service.handleCea708ControlCode(packet); if (caption) { captions.push(caption); } } } return captions; }; beforeEach(() => { service = new shaka.cea.Cea708Service(serviceNumber); }); it('decodes regular unstyled caption text', () => { const controlCodes = [ ...defineWindow, // Series of G0 control codes that add text. 0x74, 0x65, 0x73, 0x74, // t, e, s, t ]; const packet1 = createCea708PacketFromBytes(controlCodes, startTime); const packet2 = createCea708PacketFromBytes(hideWindow, endTime); const text = 'test'; const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text), ]; const expectedCaptions = [ { stream, cue: topLevelCue, }, ]; const captions = getCaptionsFromPackets(service, packet1, packet2); expect(captions).toEqual(expectedCaptions); }); it('setPenLocation sets the pen location correctly', () => { const controlCodes = [ ...defineWindow, // Series of G0 control codes that add text. 0x74, 0x65, 0x73, 0x74, // t, e, s, t // SetPenLocation command to move the pen to (2, 0) 0x92, 0x02, 0x00, // Series of G0 control codes that add text. 0x74, 0x65, 0x73, 0x74, // t, e, s, t ]; const packet1 = createCea708PacketFromBytes(controlCodes, startTime); const packet2 = createCea708PacketFromBytes(hideWindow, endTime); // After decoding, the buffer should look like this (omitting null cells). // [0]: test // [1]: // [2]: test const text = 'test'; const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text), CeaUtils.createLineBreakCue(startTime, endTime), CeaUtils.createLineBreakCue(startTime, endTime), CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text), ]; const expectedCaptions = [ { stream, cue: topLevelCue, }, ]; const captions = getCaptionsFromPackets(service, packet1, packet2); expect(captions).toEqual(expectedCaptions); }); it('setPenAttributes sets underline and italics correctly', () => { const controlCodes = [ ...defineWindow, // Series of G0 control codes that add text. 0x74, 0x65, 0x73, 0x74, // t, e, s, t // setPenAttributes. First byte is a "don't care", since this // decoder ignores it. First 2 bits of second byte are italics // and underline toggles. Turn on italics + underline. 0x90, 0x00, 0xc0, // Series of G0 control codes that add text. 0x74, 0x65, 0x73, 0x74, // t, e, s, t // setPenAttributes. Turn off italics + underline. 0x90, 0x00, 0x00, // Series of G0 control codes that add text. 0x74, 0x65, 0x73, 0x74, // t, e, s, t ]; const packet1 = createCea708PacketFromBytes(controlCodes, startTime); const packet2 = createCea708PacketFromBytes(hideWindow, endTime); // Three nested cues, where the middle one should be underlined+italicized. const text = 'test'; const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text), CeaUtils.createStyledCue( startTime, endTime, text, /* underline= */ true, /* italics= */ true, /* textColor= */ shaka.cea.CeaUtils.DEFAULT_TXT_COLOR, /* backgroundColor= */ shaka.cea.CeaUtils.DEFAULT_BG_COLOR), CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text), ]; const expectedCaptions = [ { stream, cue: topLevelCue, }, ]; const captions = getCaptionsFromPackets(service, packet1, packet2); expect(captions).toEqual(expectedCaptions); }); it('setPenColor sets foreground and background color correctly', () => { const controlCodes = [ ...defineWindow, // Series of G0 control codes that add text. 0x74, 0x65, 0x73, 0x74, // t, e, s, t // setPenColor (4 bytes). Last 6 bits of byte 2 are R,G,B for foreground. // Last 6 bits of byte 3 are R,G,B for background. This decoder ignores // byte 4 which is edge color, so it's a "don't care". 0x91, 0x30, 0x33, 0x00, // Red foreground, magenta background. // Series of G0 control codes that add text. 0x63, 0x6f, 0x6c, 0x6f, 0x72, // c, o, l, o, r ]; const packet1 = createCea708PacketFromBytes(controlCodes, startTime); const packet2 = createCea708PacketFromBytes(hideWindow, endTime); // Two nested cues, the second one should have colors. const text1 = 'test'; const text2 = 'color'; const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text1), CeaUtils.createStyledCue( startTime, endTime, text2, /* underline= */ false, /* italics= */ false, /* textColor= */ 'red', /* backgroundColor= */ 'magenta'), ]; const expectedCaptions = [ { stream, cue: topLevelCue, }, ]; const captions = getCaptionsFromPackets(service, packet1, packet2); expect(captions).toEqual(expectedCaptions); }); it('handles special characters from the G0, G1, G2, and G3 groups', () => { const controlCodes = [ ...defineWindow, // Series of G0 text control code 0x7f, // A musical note, the only exception the G0 table has to ASCII. // setPenLocation (1, 0) to go to next row. 0x92, 0x01, 0x00, // Series of G1 control codes that add text. 0xa9, 0xb6, 0xf7, // ©, ¶, ÷ // setPenLocation (2, 0) to go to next row. 0x92, 0x02, 0x00, // Series of G2 control codes that add text. 0x1079, 0x107b, 0x1039, // ⅞, ┐, ™ // setPenLocation (3, 0) to go to next row. 0x92, 0x03, 0x00, // G3 control code. 0x10a0, // As of CEA-708-E, there is only 1 char in G3, on 0xa0. ]; const packet1 = createCea708PacketFromBytes(controlCodes, startTime); const packet2 = createCea708PacketFromBytes(hideWindow, endTime); const text1 = '♪'; const text2 = '©¶÷'; const text3 = '⅞┐™'; const text4 = '[CC]'; const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text1), CeaUtils.createLineBreakCue(startTime, endTime), CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text2), CeaUtils.createLineBreakCue(startTime, endTime), CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text3), CeaUtils.createLineBreakCue(startTime, endTime), CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text4), ]; const expectedCaptions = [ { stream, cue: topLevelCue, }, ]; const captions = getCaptionsFromPackets(service, packet1, packet2); expect(captions).toEqual(expectedCaptions); }); it('adds an underline for unsupported chars from the G2/G3 groups', () => { const controlCodes = [ ...defineWindow, // Series of G2 control codes that add text. 0x1036, 0x103c, 0x1070, // unsupported, œ, unsupported // setPenLocation (1, 0) to go to next row. 0x92, 0x01, 0x00, // Series of G3 control codes that add text. 0x10a0, 0x10a1, 0x10db, // [CC], unsupported, unsupported ]; const packet1 = createCea708PacketFromBytes(controlCodes, startTime); const packet2 = createCea708PacketFromBytes(hideWindow, endTime); // Some of the characters are unsupported as of CEA-708-E, so they should // be replaced by an underline. const text1 = '_œ_'; const text2 = '[CC]__'; const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text1), CeaUtils.createLineBreakCue(startTime, endTime), CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text2), ]; const expectedCaptions = [ { stream, cue: topLevelCue, }, ]; const captions = getCaptionsFromPackets(service, packet1, packet2); expect(captions).toEqual(expectedCaptions); }); it('handles the reset command correctly', () => { const controlCodes = [ ...defineWindow, // Series of G0 control codes that add text. 0x74, 0x65, 0x73, 0x74, // t, e, s, t ]; const resetControlCode = [0x8f]; const packet1 = createCea708PacketFromBytes(controlCodes, startTime); const packet2 = createCea708PacketFromBytes(resetControlCode, endTime); // The text in the current window should have been emitted, and then clear // should have been called. const text = 'test'; const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text), ]; const expectedCaptions = [ { stream, cue: topLevelCue, }, ]; spyOn(service, 'clear').and.callThrough(); const captions = getCaptionsFromPackets(service, packet1, packet2); expect(captions).toEqual(expectedCaptions); expect(service.clear).toHaveBeenCalledTimes(1); }); it('handles the setWindowAttributes command correctly', () => { const controlCodes = [ ...defineWindow, // Series of G0 control codes that add text. 0x74, 0x65, 0x73, 0x74, // t, e, s, t // Currently, setWindowAttributes is only used to justify text, // as specified by the last 2 bits of the fourth byte. The // other bytes after the first byte are "don't care". 0x97, 0x00, 0x00, 0x01, 0x00, // Justify right ]; const packet1 = createCea708PacketFromBytes(controlCodes, startTime); const packet2 = createCea708PacketFromBytes(hideWindow, endTime); // Right-justified text is expected. const text = 'test'; const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); topLevelCue.textAlign = shaka.text.Cue.textAlign.RIGHT; topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text), ]; const expectedCaptions = [ { stream, cue: topLevelCue, }, ]; const captions = getCaptionsFromPackets(service, packet1, packet2); expect(captions).toEqual(expectedCaptions); }); it('handles the carriage return command correctly', () => { const controlCodes = [ ...defineWindow, // Series of G0 control codes that add text. 0x74, 0x65, // t, e, // Carriage return. 0x0d, // Series of G0 control codes that add text. 0x73, 0x74, // s, t ]; const packet1 = createCea708PacketFromBytes(controlCodes, startTime); const packet2 = createCea708PacketFromBytes(hideWindow, endTime); const text1 = 'te'; const text2 = 'st'; const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text1), CeaUtils.createLineBreakCue(startTime, endTime), CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text2), ]; const expectedCaptions = [ { stream, cue: topLevelCue, }, ]; const captions = getCaptionsFromPackets(service, packet1, packet2); expect(captions).toEqual(expectedCaptions); }); it('handles the horizontal carriage return command correctly', () => { const controlCodes = [ ...defineWindow, // Series of G0 control codes that add text. 0x74, 0x65, // t, e, // setPenLocation (1, 0) to go to next row. 0x92, 0x01, 0x00, // Series of G0 control codes that add text. 0x6d, 0x70, // m, p // Horizontal Carriage return. 0x0e, // Series of G0 control codes that add text. 0x73, 0x74, // s, t ]; const packet1 = createCea708PacketFromBytes(controlCodes, startTime); const packet2 = createCea708PacketFromBytes(hideWindow, endTime); // HCR wipes the row and moves the pen to the row start. const text1 = 'te'; const text2 = 'st'; const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text1), CeaUtils.createLineBreakCue(startTime, endTime), CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text2), ]; const expectedCaptions = [ { stream, cue: topLevelCue, }, ]; const captions = getCaptionsFromPackets(service, packet1, packet2); expect(captions).toEqual(expectedCaptions); }); it('handles the ASCII backspace command correctly', () => { const controlCodes = [ ...defineWindow, // Series of G0 control codes that add text. 0x74, 0x65, 0x73, 0x74, // t, e, s, t // Backspace. 0x08, ]; const packet1 = createCea708PacketFromBytes(controlCodes, startTime); const packet2 = createCea708PacketFromBytes(hideWindow, endTime); // Backspace should have erased the last 't' in 'test'. const text = 'tes'; const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text), ]; const expectedCaptions = [ { stream, cue: topLevelCue, }, ]; const captions = getCaptionsFromPackets(service, packet1, packet2); expect(captions).toEqual(expectedCaptions); }); it('handles the ASCII form-feed command correctly', () => { const controlCodes = [ ...defineWindow, // Series of G0 control codes that add text. 0x61, 0x62, // a, b, // setPenLocation (1, 0) to go to next row. 0x92, 0x01, 0x00, // Series of G0 control codes that add text. 0x62, 0x61, // b, a // Form-feed. 0x0c, // Series of G0 control codes that add text. 0x74, 0x65, 0x73, 0x74, // t, e, s, t ]; const packet1 = createCea708PacketFromBytes(controlCodes, startTime); const packet2 = createCea708PacketFromBytes(hideWindow, endTime); // The form feed control code would have wiped the entire window // including new lines, and the text after is just 'test'. const text = 'test'; const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text), ]; const expectedCaptions = [ { stream, cue: topLevelCue, }, ]; const captions = getCaptionsFromPackets(service, packet1, packet2); expect(captions).toEqual(expectedCaptions); }); it('handles C2 and C3 no-op control codes correctly', () => { // As of CEA-708, the C2 and C3 control code group has no operations. // However, the bytes are reserved for future modifications to the spec, // and so the correct # of bytes should be skipped if they are seen. const packets = [ // C2 control code data. [0x1008, 0x00], // C2 Packet 1. [0x1010, 0x00, 0x00], // C2 Packet 2. [0x1018, 0x00, 0x00, 0x00], // C2 Packet 3. // C3 control code data. [0x1080, 0x00, 0x00, 0x00, 0x00], // C3 packet 1. [0x1088, 0x00, 0x00, 0x00, 0x00, 0x00], // C3 packet 2. ]; const expectedSkips = [1, 2, 3, 4, 5]; // As per the CEA-708-E spec. for (let i = 0; i < packets.length; i++) { const packet = createCea708PacketFromBytes(packets[i], /* pts= */ 1); spyOn(packet, 'skip'); getCaptionsFromPackets(service, packet); expect(packet.skip).toHaveBeenCalledWith(expectedSkips[i]); } }); describe('handles commands that change the display of windows', () => { const time1 = 1; const time2 = 2; const time3 = 4; const time4 = 5; const textControlCodes = [ // Series of G0 control codes that add text. 0x74, 0x65, 0x73, 0x74, // t, e, s, t ]; // These commands affect ALL windows, per the 0xff bitmap. const toggleWindow = [0x8b, 0xff]; const displayWindow = [0x89, 0xff]; const deleteWindow = [0x8c, 0xff]; const clearWindow = [0x88, 0xff]; it('handles display, toggle, and delete commands on windows', () => { // Define a visible window, add some text, and toggle it off, // which should force the window to emit the caption, 'test'. const packet1 = createCea708PacketFromBytes(defineWindow, time1); const packet2 = createCea708PacketFromBytes(textControlCodes, time1); const packet3 = createCea708PacketFromBytes(toggleWindow, time2); // Window is now hidden. Turn it back on at time 3, and append // more text to it. const packet4 = createCea708PacketFromBytes(displayWindow, time3); const packet5 = createCea708PacketFromBytes(textControlCodes, time3); // Window is now being displayed. Delete all the windows. // This should force the displayed window to emit the caption, 'testtest'. const packet6 = createCea708PacketFromBytes(deleteWindow, time4); const text1 = 'test'; const text2 = 'testtest'; const topLevelCue1 = new shaka.text.Cue( /* startTime= */ time1, /* endTime= */ time2, ''); topLevelCue1.nestedCues = [ CeaUtils.createDefaultCue( /* startTime= */ time1, /* endTime= */ time2, /* payload= */ text1), ]; const topLevelCue2 = new shaka.text.Cue( /* startTime= */ time3, /* endTime= */ time4, ''); topLevelCue2.nestedCues = [ CeaUtils.createDefaultCue( /* startTime= */ time3, /* endTime= */ time4, /* payload= */ text2), ]; const expectedCaptions = [ { stream, cue: topLevelCue1, }, { stream, cue: topLevelCue2, }, ]; const captions = getCaptionsFromPackets( service, packet1, packet2, packet3, packet4, packet5, packet6); expect(captions).toEqual(expectedCaptions); }); it('handles the clear command on a window', () => { // Define a visible window, add text to it, and then clear it. // This should emit a caption, since a visible window is being cleared. const packet1 = createCea708PacketFromBytes(defineWindow, time1); const packet2 = createCea708PacketFromBytes(textControlCodes, time1); const packet3 = createCea708PacketFromBytes(clearWindow, time2); // Display the window again, and then hide it. Although a visible window // that turns off usually emits, this should NOT emit a caption, since // the window contains nothing in it after the clear. const packet4 = createCea708PacketFromBytes(displayWindow, time3); const packet5 = createCea708PacketFromBytes(textControlCodes, time3); const packet6 = createCea708PacketFromBytes(hideWindow, time1); // Only one cue should have been emitted as per the explanation above. const text = 'test'; const topLevelCue = new shaka.text.Cue( /* startTime= */ time1, /* endTime= */ time2, ''); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue( /* startTime= */ time1, /* endTime= */ time2, /* payload= */ text), ]; const expectedCaptions = [ { stream, cue: topLevelCue, }, ]; const captions = getCaptionsFromPackets(service, packet1, packet2, packet3, packet4, packet5, packet6); expect(captions).toEqual(expectedCaptions); }); }); });