mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-17 16:26:39 +03:00
6c85c8cbfc
TTML nested cues are meant to be displayed horizontally as inline elements. This fixes the rendering of these nested cues in both SimpleTextDisplayer and UITextDisplayer. In UITextDisplayer, the styles have been adjusted to lay out the nested cues horizontally rather than vertically. In SimpleTextDisplayer, the nested cues were being displayed as if they were top-level cues. This change concatenates the nested cues into a single cue displayed in the browser. This also improves comments on the poorly-named "spacer" property, which represents a line break in TTML. This fixes the rendering of "spacer" in SimpleTextDisplayer by inserting an actual newline character into the collapsed nested cues. Finally, this fixes and clarifies names used internally in UITextDisplayer. For example, there is a difference between a nested cue and leaf cue. A nested cue and a top-level cue without nested cues are both "leaf" cues, but a top-level cue is never a "nested" cue, since it is at the top level. The conflation of these names before this fix made it difficult to understand and fix the code in the first place. Closes #2760 Change-Id: I89633761d12704e253371d17e2e786c5b2ed67a7
338 lines
9.7 KiB
JavaScript
338 lines
9.7 KiB
JavaScript
/*! @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/google/shaka-player/issues/848
|
|
verifyHelper(
|
|
[
|
|
{startTime: 20, endTime: 40, text: 'Test1'},
|
|
{startTime: 20, endTime: 40, text: 'Test2'},
|
|
{startTime: 20, endTime: 40, text: 'Test3'},
|
|
],
|
|
[
|
|
new shaka.text.Cue(20, 40, 'Test3'),
|
|
new shaka.text.Cue(20, 40, 'Test2'),
|
|
new shaka.text.Cue(20, 40, 'Test1'),
|
|
]);
|
|
});
|
|
|
|
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('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)));
|
|
}
|
|
});
|