Files
shaka-player/test/text/simple_text_displayer_unit.js
T
Joey Parrish 6c85c8cbfc fix: Fix rendering of TTML nested cues and spacers
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
2020-07-30 11:29:55 -07:00

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)));
}
});