UNPKG

shaka-player

Version:
469 lines (405 loc) 15.1 kB
/*! @license * Shaka Player * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ describe('SimpleTextDisplayer', () => { const originalVTTCue = window.VTTCue; const Cue = shaka.text.Cue; const SimpleTextDisplayer = shaka.text.SimpleTextDisplayer; /** @type {!shaka.test.FakeVideo} */ let video; /** @type {!shaka.test.FakeTextTrack} */ let mockTrack; /** @type {!shaka.text.SimpleTextDisplayer} */ let displayer; beforeEach(() => { video = new shaka.test.FakeVideo(); displayer = new SimpleTextDisplayer(video); expect(video.textTracks.length).toBe(1); mockTrack = /** @type {!shaka.test.FakeTextTrack} */ (video.textTracks[0]); expect(mockTrack).toBeTruthy(); /** * @constructor * @param {number} start * @param {number} end * @param {string} text */ function FakeVTTCue(start, end, text) { this.startTime = start; this.endTime = end; this.text = text; this.snapToLines = true; this.vertical = undefined; this.line = 'auto'; this.position = 'auto'; } window.VTTCue = /** @type {?} */(FakeVTTCue); }); afterEach(async () => { await displayer.destroy(); }); afterAll(() => { window.VTTCue = originalVTTCue; }); describe('append', () => { it('sorts cues before inserting', () => { // See: https://bit.ly/2K9VX3s verifyHelper( [ {startTime: 10, endTime: 20, text: 'Test1'}, {startTime: 20, endTime: 30, text: 'Test2'}, {startTime: 30, endTime: 40, text: 'Test3'}, ], [ new shaka.text.Cue(20, 30, 'Test2'), new shaka.text.Cue(30, 40, 'Test3'), new shaka.text.Cue(10, 20, 'Test1'), ]); }); it('appends equal time cues in reverse order', () => { // Regression test for https://github.com/shaka-project/shaka-player/issues/848 // When VTTCue is seen as the real thing (because of the presence of // VTTCue.prototype.line), then the reverse-order behavior comes into // play. The reverse order is only needed because of VTTCue spec // behavior. // First we test the behavior with a real-looking VTTCue (in which // prototype.line merely exists). This simulates Chrome, Firefox, and // Safari. // eslint-disable-next-line no-restricted-syntax window.VTTCue.prototype['line'] = 'auto'; verifyHelper( [ {startTime: 20, endTime: 40, text: 'Test1'}, {startTime: 20, endTime: 40, text: 'Test2'}, {startTime: 20, endTime: 40, text: 'Test3'}, ], [ // Reverse order to compensate for the way line='auto' is // implemented in browsers. new shaka.text.Cue(20, 40, 'Test3'), new shaka.text.Cue(20, 40, 'Test2'), new shaka.text.Cue(20, 40, 'Test1'), ]); // Next we test the behavior with a VTTCue which is seen as a cheap // polyfill (in which prototype.line does not exist). This simulates // legacy Edge. // eslint-disable-next-line no-restricted-syntax delete window.VTTCue.prototype['line']; displayer.remove(0, Infinity); // Clear the cues from above. verifyHelper( [ {startTime: 20, endTime: 40, text: 'Test1'}, {startTime: 20, endTime: 40, text: 'Test2'}, {startTime: 20, endTime: 40, text: 'Test3'}, ], [ // Input order, since the displayer sees this as a fake VTTCue // implementation. new shaka.text.Cue(20, 40, 'Test1'), new shaka.text.Cue(20, 40, 'Test2'), new shaka.text.Cue(20, 40, 'Test3'), ]); }); it('appends nested cues', () => { const shakaCue = new shaka.text.Cue(10, 20, ''); const nestedCue1 = new shaka.text.Cue(10, 20, 'Test1 '); const nestedCue2 = new shaka.text.Cue(10, 20, 'Test2'); shakaCue.nestedCues = [nestedCue1, nestedCue2]; verifyHelper( [ {startTime: 10, endTime: 20, text: 'Test1 Test2'}, ], [shakaCue]); }); it('flattens nested cue payloads correctly', () => { const level0ContainerCue = new shaka.text.Cue(10, 30, ''); level0ContainerCue.isContainer = true; const level1NonContainerCueA = new shaka.text.Cue(10, 20, ''); const level1NonContainerCueB = new shaka.text.Cue(20, 30, ''); // Add a trailing whitespace character to get a space-delimited expected // result. const cueANestedCue0 = new shaka.text.Cue(10, 20, 'Cue A Test0 '); const cueANestedCue1 = new shaka.text.Cue(10, 20, 'Cue A Test1'); const cueBNestedCue0 = new shaka.text.Cue(20, 30, 'Cue B Test0 '); const cueBNestedCue1 = new shaka.text.Cue(20, 30, 'Cue B Test1'); level1NonContainerCueA.nestedCues = [cueANestedCue0, cueANestedCue1]; level1NonContainerCueB.nestedCues = [cueBNestedCue0, cueBNestedCue1]; level0ContainerCue.nestedCues = [level1NonContainerCueA, level1NonContainerCueB]; verifyHelper( [ {startTime: 10, endTime: 20, text: 'Cue A Test0 Cue A Test1'}, {startTime: 20, endTime: 30, text: 'Cue B Test0 Cue B Test1'}, ], [level0ContainerCue]); }); // Regression test for b/159050711 it('maintains the styles of the parent cue', () => { const shakaCue = new shaka.text.Cue(10, 20, ''); const nestedCue1 = new shaka.text.Cue(10, 20, 'Test1 '); const nestedCue2 = new shaka.text.Cue(10, 20, 'Test2'); shakaCue.nestedCues = [nestedCue1, nestedCue2]; shakaCue.lineAlign = Cue.lineAlign.CENTER; nestedCue1.lineAlign = Cue.lineAlign.START; nestedCue2.lineAlign = Cue.lineAlign.START; verifyHelper( [ { startTime: 10, endTime: 20, text: 'Test1 Test2', lineAlign: 'center', }, ], [shakaCue]); }); it('creates style tags for cues with underline/italics/bold', () => { const shakaCue = new shaka.text.Cue(10, 20, ''); // First cue is underlined and italicized. const nestedCue1 = new shaka.text.Cue(10, 20, 'Test1'); nestedCue1.fontStyle = shaka.text.Cue.fontStyle.ITALIC; nestedCue1.textDecoration.push(shaka.text.Cue.textDecoration.UNDERLINE); // Second cue is italicized and bolded. const nestedCue2 = new shaka.text.Cue(10, 20, 'Test2'); nestedCue2.fontStyle = shaka.text.Cue.fontStyle.ITALIC; nestedCue2.fontWeight = shaka.text.Cue.fontWeight.BOLD; // Third cue has no bold, italics, or underline. const nestedCue3 = new shaka.text.Cue(10, 20, 'Test3'); // Fourth cue is only underlined. const nestedCue4 = new shaka.text.Cue(10, 20, 'Test4'); nestedCue4.textDecoration.push(shaka.text.Cue.textDecoration.UNDERLINE); const expectedText = '<i><u>Test1</u></i><b><i>Test2</i></b>Test3<u>Test4</u>'; shakaCue.nestedCues = [nestedCue1, nestedCue2, nestedCue3, nestedCue4]; verifyHelper( [ {startTime: 10, endTime: 20, text: expectedText}, ], [shakaCue]); }); it('adds linebreaks when a linebreak cue is seen', () => { const shakaCue = new shaka.text.Cue(10, 20, ''); const nestedCue1 = new shaka.text.Cue(10, 20, 'Test1'); // Second cue is a linebreak cue. const nestedCue2 = new shaka.text.Cue(10, 20, ''); nestedCue2.lineBreak = true; const nestedCue3 = new shaka.text.Cue(10, 20, 'Test2'); shakaCue.nestedCues = [nestedCue1, nestedCue2, nestedCue3]; verifyHelper( [ {startTime: 10, endTime: 20, text: 'Test1\nTest2'}, ], [shakaCue]); }); it('skips duplicate cues', () => { const cue1 = new shaka.text.Cue(10, 20, 'Test'); displayer.append([cue1]); expect(mockTrack.addCue).toHaveBeenCalledTimes(1); mockTrack.addCue.calls.reset(); const cue2 = new shaka.text.Cue(10, 20, 'Test'); displayer.append([cue2]); expect(mockTrack.addCue).not.toHaveBeenCalled(); }); }); describe('remove', () => { it('removes cues which overlap the range', () => { const cue1 = new shaka.text.Cue(0, 1, 'Test'); const cue2 = new shaka.text.Cue(1, 2, 'Test'); const cue3 = new shaka.text.Cue(2, 3, 'Test'); displayer.append([cue1, cue2, cue3]); displayer.remove(0, 1); expect(mockTrack.removeCue).toHaveBeenCalledTimes(1); expect(mockTrack.removeCue).toHaveBeenCalledWith( jasmine.objectContaining({startTime: 0, endTime: 1})); mockTrack.removeCue.calls.reset(); displayer.remove(0.5, 1.001); expect(mockTrack.removeCue).toHaveBeenCalledTimes(1); expect(mockTrack.removeCue).toHaveBeenCalledWith( jasmine.objectContaining({startTime: 1, endTime: 2})); mockTrack.removeCue.calls.reset(); displayer.remove(3, 5); expect(mockTrack.removeCue).not.toHaveBeenCalled(); mockTrack.removeCue.calls.reset(); displayer.remove(2.9999, Infinity); expect(mockTrack.removeCue).toHaveBeenCalledTimes(1); expect(mockTrack.removeCue).toHaveBeenCalledWith( jasmine.objectContaining({startTime: 2, endTime: 3})); mockTrack.removeCue.calls.reset(); }); it('does nothing when nothing is buffered', () => { displayer.remove(0, 1); expect(mockTrack.removeCue).not.toHaveBeenCalled(); }); }); describe('convertToTextTrackCue', () => { it('converts shaka.text.Cues to VttCues', () => { verifyHelper( [ {startTime: 20, endTime: 40, text: 'Test4'}, ], [ new shaka.text.Cue(20, 40, 'Test4'), ]); const cue1 = new shaka.text.Cue(20, 40, 'Test5'); cue1.positionAlign = Cue.positionAlign.LEFT; cue1.lineAlign = Cue.lineAlign.START; cue1.size = 80; cue1.textAlign = Cue.textAlign.LEFT; cue1.writingMode = Cue.writingMode.VERTICAL_LEFT_TO_RIGHT; cue1.lineInterpretation = Cue.lineInterpretation.LINE_NUMBER; cue1.line = 5; cue1.position = 10; verifyHelper( [ { startTime: 20, endTime: 40, text: 'Test5', lineAlign: 'start', positionAlign: 'line-left', size: 80, align: 'left', vertical: 'lr', snapToLines: true, line: 5, position: 10, }, ], [cue1]); const cue2 = new shaka.text.Cue(30, 50, 'Test'); cue2.positionAlign = Cue.positionAlign.RIGHT; cue2.lineAlign = Cue.lineAlign.END; cue2.textAlign = Cue.textAlign.RIGHT; cue2.writingMode = Cue.writingMode.VERTICAL_RIGHT_TO_LEFT; cue2.lineInterpretation = Cue.lineInterpretation.PERCENTAGE; cue2.line = 5; verifyHelper( [ { startTime: 30, endTime: 50, text: 'Test', lineAlign: 'end', positionAlign: 'line-right', align: 'right', vertical: 'rl', snapToLines: false, line: 5, }, ], [cue2]); const cue3 = new shaka.text.Cue(40, 60, 'Test1'); cue3.positionAlign = Cue.positionAlign.CENTER; cue3.lineAlign = Cue.lineAlign.CENTER; cue3.textAlign = Cue.textAlign.START; cue3.direction = Cue.direction.HORIZONTAL_LEFT_TO_RIGHT; verifyHelper( [ { startTime: 40, endTime: 60, text: 'Test1', lineAlign: 'center', positionAlign: 'center', align: 'start', vertical: undefined, }, ], [cue3]); const cue4 = new shaka.text.Cue(40, 60, 'Test2'); cue4.line = null; cue4.position = null; verifyHelper( [ { startTime: 40, endTime: 60, text: 'Test2', line: 'auto', position: 'auto', }, ], [cue4]); const cue5 = new shaka.text.Cue(40, 60, 'Test3'); cue5.line = 0; cue5.position = 0; verifyHelper( [ { startTime: 40, endTime: 60, text: 'Test3', line: 0, position: 0, }, ], [cue5]); }); it('works around browsers not supporting align=center', () => { /** * @constructor * @param {number} start * @param {number} end * @param {string} text */ function FakeVTTCueWithoutAlignCenter(start, end, text) { let align = 'middle'; Object.defineProperty(this, 'align', { get: () => align, set: (newValue) => { if (newValue != 'center') { align = newValue; } }, }); this.startTime = start; this.endTime = end; this.text = text; } window.VTTCue = /** @type {?} */(FakeVTTCueWithoutAlignCenter); const cue1 = new shaka.text.Cue(20, 40, 'Test'); cue1.textAlign = Cue.textAlign.CENTER; verifyHelper( [ { startTime: 20, endTime: 40, text: 'Test', align: 'middle', }, ], [cue1]); }); it('ignores cues with startTime >= endTime', () => { mockTrack.addCue.calls.reset(); const cue1 = new shaka.text.Cue(60, 40, 'Test'); const cue2 = new shaka.text.Cue(40, 40, 'Test'); displayer.append([cue1, cue2]); expect(mockTrack.addCue).not.toHaveBeenCalled(); }); }); describe('destroy', () => { it('disables the TextTrack it created', async () => { // There should only be the one track created by this displayer. expect(video.textTracks.length).toBe(1); /** @type {!TextTrack} */ const textTrack = video.textTracks[0]; // It should not be disabled before we destroy it. expect(textTrack.mode).not.toBe('disabled'); await displayer.destroy(); // It should be disabled after we destroy it. expect(textTrack.mode).toBe('disabled'); }); }); function createFakeCue(startTime, endTime) { return {startTime: startTime, endTime: endTime}; } /** * Verifies that vttCues are converted to shakaCues and appended. * @param {!Array} vttCues * @param {!Array.<!shaka.text.Cue>} shakaCues */ function verifyHelper(vttCues, shakaCues) { mockTrack.addCue.calls.reset(); displayer.append(shakaCues); const result = mockTrack.addCue.calls.allArgs().reduce( shaka.util.Functional.collapseArrays, []); expect(result).toEqual(vttCues.map((c) => jasmine.objectContaining(c))); } });