Files
shaka-player/test/cea/cea708_window_unit.js
T
Gary Katsevman 2a524bf51f feat: expose CEA708 window position in the cue's region (#5924)
CEA708 captions have positioning data available in their windows.
However, this isn't currently translated and exposed by shaka though it
is parsed from the bitstream.

Translates the windows into WebVTT regions and uses the mappings
outlined
https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/608toVTT.html#positioning-in-cea-708

This is also partially implements #2583.
2023-11-23 17:19:46 +01:00

494 lines
15 KiB
JavaScript

/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
describe('Cea708Window', () => {
const CeaUtils = shaka.test.CeaUtils;
/** @type {!shaka.cea.Cea708Window} */
let window;
/** @type {number} */
const serviceNumber = 1; // We will arbitrarily pick service 1 for all tests.
/** @type {string} */
const stream = `svc${serviceNumber}`;
/** @type {number} */
const rowCount = 10;
/** @type {number} */
const colCount = 32;
/** @type {number} */
const startTime = 1;
/** @type {number} */
const endTime = 2;
beforeEach(() => {
window = new shaka.cea.Cea708Window(/* windowNum= */ 0, serviceNumber);
window.defineWindow(
/* visible= */ true, /* verticalAnchor= */ 0,
/* horAnchor= */ 0, /* anchorId= */ 0, /* relativeToggle= */ false,
rowCount, colCount);
window.setStartTime(startTime);
});
it('adds and emits a series of characters from the buffer', () => {
const text = 'test word';
for (const c of text) {
window.setCharacter(c);
}
const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '',
serviceNumber, 0, rowCount, colCount);
topLevelCue.nestedCues = [
CeaUtils.createDefaultCue(startTime, endTime, text),
];
const caption = window.forceEmit(endTime, serviceNumber);
const expectedCaption = {
stream,
cue: topLevelCue,
};
expect(caption).toEqual(expectedCaption);
});
describe('handles carriage returns', () => {
it('handles a regular carriage return', () => {
const text1 = 'test';
const text2 = 'word';
for (const c of text1) {
window.setCharacter(c);
}
window.carriageReturn();
for (const c of text2) {
window.setCharacter(c);
}
const caption = window.forceEmit(endTime, serviceNumber);
const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '',
serviceNumber, 0, rowCount, colCount);
topLevelCue.nestedCues = [
CeaUtils.createDefaultCue(startTime, endTime, text1),
CeaUtils.createLineBreakCue(startTime, endTime),
CeaUtils.createDefaultCue(startTime, endTime, text2),
];
const expectedCaption = {
stream,
cue: topLevelCue,
};
expect(caption).toEqual(expectedCaption);
});
it('handles a carriage return on the last row of the buffer', () => {
const text1 = 'test';
const text2 = 'word';
// Set the pen lcoation to the very last row in the buffer.
window.setPenLocation(/* row= */ rowCount-1, /* col= */ 0);
for (const c of text1) {
window.setCharacter(c);
}
window.carriageReturn();
for (const c of text2) {
window.setCharacter(c);
}
const caption = window.forceEmit(endTime, serviceNumber);
const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '',
serviceNumber, 0, rowCount, colCount);
topLevelCue.nestedCues = [
CeaUtils.createDefaultCue(startTime, endTime, text1),
CeaUtils.createLineBreakCue(startTime, endTime),
CeaUtils.createDefaultCue(startTime, endTime, text2),
];
const expectedCaption = {
stream,
cue: topLevelCue,
};
expect(caption).toEqual(expectedCaption);
});
it('handles a horizontal carriage return', () => {
const text = 'test';
for (const c of text) {
window.setCharacter(c);
}
window.horizontalCarriageReturn();
const caption = window.forceEmit(endTime, serviceNumber);
// Nothing should have emitted, a horizontal carriage return wipes the row
// and sets the column position to the beginning of the row.
expect(caption).toBe(null);
});
});
it('handles pen styling including colors, underlines, italics', () => {
const text1 = 'style1';
const text2 = 'style2';
const text3 = 'style3';
const textColor1 = 'red';
const textColor2 = 'yellow';
const backgroundColor1 = 'blue';
const backgroundColor2 = 'magenta';
// Set the pen to an underlined, italicized red color.
window.setPenItalics(true);
window.setPenUnderline(true);
window.setPenTextColor('red');
for (const c of text1) {
window.setCharacter(c);
}
// Remove the underline and italics, and set the background color to blue.
window.setPenItalics(false);
window.setPenUnderline(false);
window.setPenBackgroundColor('blue');
for (const c of text2) {
window.setCharacter(c);
}
// Turn underline on again, make the text yellow and background magenta.
window.setPenUnderline(true);
window.setPenTextColor('yellow');
window.setPenBackgroundColor('magenta');
for (const c of text3) {
window.setCharacter(c);
}
// These three stylings should correspond to three nested cues.
const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '',
serviceNumber, 0, rowCount, colCount);
topLevelCue.nestedCues = [
CeaUtils.createStyledCue(startTime, endTime, text1, /* underline= */ true,
/* italics= */ true, textColor1,
shaka.cea.CeaUtils.DEFAULT_BG_COLOR),
CeaUtils.createStyledCue(startTime, endTime, text2,
/* underline= */ false, /* italics= */ false,
textColor1, backgroundColor1),
CeaUtils.createStyledCue(startTime, endTime, text3, /* underline= */ true,
/* italics= */ false, textColor2, backgroundColor2),
];
const caption = window.forceEmit(endTime, serviceNumber);
const expectedCaption = {
stream,
cue: topLevelCue,
};
expect(caption).toEqual(expectedCaption);
});
describe('handles justification of cues', () => {
const text = 'test';
const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '',
serviceNumber, 0, rowCount, colCount);
it('justifies the text left', () => {
for (const c of text) {
window.setCharacter(c);
}
// Left-justified.
window.setJustification(shaka.cea.Cea708Window.TextJustification.LEFT);
topLevelCue.textAlign = shaka.text.Cue.textAlign.LEFT;
topLevelCue.nestedCues = [
CeaUtils.createDefaultCue(startTime, endTime, text),
];
const expectedCaption = {
stream,
cue: topLevelCue,
};
const caption = window.forceEmit(endTime, serviceNumber);
expect(caption).toEqual(expectedCaption);
});
it('justifies the text right', () => {
for (const c of text) {
window.setCharacter(c);
}
// Right-justified.
window.setJustification(shaka.cea.Cea708Window.TextJustification.RIGHT);
topLevelCue.textAlign = shaka.text.Cue.textAlign.RIGHT;
topLevelCue.nestedCues = [
CeaUtils.createDefaultCue(startTime, endTime, text),
];
const expectedCaption = {
stream,
cue: topLevelCue,
};
const caption = window.forceEmit(endTime, serviceNumber);
expect(caption).toEqual(expectedCaption);
});
it('default justification should be centered', () => {
for (const c of text) {
window.setCharacter(c);
}
topLevelCue.textAlign = shaka.text.Cue.textAlign.CENTER;
topLevelCue.nestedCues = [
CeaUtils.createDefaultCue(startTime, endTime, text),
];
const expectedCaption = {
stream,
cue: topLevelCue,
};
const caption = window.forceEmit(endTime, serviceNumber);
expect(caption).toEqual(expectedCaption);
});
});
it('resets the pen correctly', () => {
const text1 = 'abcd';
const text2 = 'efgh';
// Set some styles on the pen and add the first text to window.
window.setPenUnderline(true);
window.setPenBackgroundColor('blue');
for (const c of text1) {
window.setCharacter(c);
}
// Reset the pen and add the second text to window.
window.resetPen();
for (const c of text2) {
window.setCharacter(c);
}
// The second text should have overwritten the first text,
// and all the styles should have been cleared.
const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '',
serviceNumber, 0, rowCount, colCount);
topLevelCue.nestedCues = [
CeaUtils.createDefaultCue(startTime, endTime, text2),
];
const caption = window.forceEmit(endTime, serviceNumber);
const expectedCaption = {
stream,
cue: topLevelCue,
};
expect(caption).toEqual(expectedCaption);
});
it('handles the backspace command to backspace text correctly', () => {
const text = 'testt';
const backspacedText = 'test';
for (const c of text) {
window.setCharacter(c);
}
window.backspace();
const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '',
serviceNumber, 0, rowCount, colCount);
topLevelCue.nestedCues = [
CeaUtils.createDefaultCue(startTime, endTime, backspacedText),
];
const caption = window.forceEmit(endTime, serviceNumber);
const expectedCaption = {
stream,
cue: topLevelCue,
};
expect(caption).toEqual(expectedCaption);
});
it('correctly sets pen location', () => {
const text1 = 'test';
const text2 = 'word';
const text3 = 'on new line';
for (const c of text1) {
window.setCharacter(c);
}
window.setPenLocation(/* row= */ 0, /* col= */ 6);
for (const c of text2) {
window.setCharacter(c);
}
window.setPenLocation(/* row= */ 3, /* col= */ 0);
for (const c of text3) {
window.setCharacter(c);
}
// There should be two spaces between the words on the first row,
// and then the last row with text should appear 3 linebreaks later.
const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '',
serviceNumber, 0, rowCount, colCount);
topLevelCue.nestedCues = [
CeaUtils.createDefaultCue(startTime, endTime, text1+' '+text2),
CeaUtils.createLineBreakCue(startTime, endTime),
CeaUtils.createLineBreakCue(startTime, endTime),
CeaUtils.createLineBreakCue(startTime, endTime),
CeaUtils.createDefaultCue(startTime, endTime, text3),
];
const caption = window.forceEmit(endTime, serviceNumber);
const expectedCaption = {
stream,
cue: topLevelCue,
};
expect(caption).toEqual(expectedCaption);
});
it('cuts off text that exceeds the column size on a given row', () => {
const text = '0123456789012345678901234567890123'; // this text is 34 chars.
const trimmedText = text.substr(0, 32);
for (const c of text) {
window.setCharacter(c);
}
// Since column size is 32, the buffer should have only taken the first
// 32 chars, and omitted the two extra ones at the end.
const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '',
serviceNumber, 0, rowCount, colCount);
topLevelCue.nestedCues = [
CeaUtils.createDefaultCue(startTime, endTime, trimmedText),
];
const caption = window.forceEmit(endTime, serviceNumber);
const expectedCaption = {
stream,
cue: topLevelCue,
};
expect(caption).toEqual(expectedCaption);
});
it('correctly handles display(), hide(), and toggle() commands', () => {
window.display(); // The window should be visible.
expect(window.isVisible()).toBe(true);
window.hide(); // The window should be hidden.
expect(window.isVisible()).toBe(false);
window.toggle(); // The window was hidden, but is now toggled to visible.
expect(window.isVisible()).toBe(true);
});
describe('correctly handles the window anchors', () => {
it('handles bottom of video window anchors', () => {
window = new shaka.cea.Cea708Window(/* windowNum= */ 0,
/* serviceNumber= */ 1);
window.defineWindow(
/* visible= */ true, /* verticalAnchor= */ 99,
/* horAnchor= */ 50, /* anchorId= */ 7, /* relativeToggle= */ true,
rowCount, colCount);
window.setStartTime(startTime);
const text = 'test word';
for (const c of text) {
window.setCharacter(c);
}
const topLevelCue = new shaka.text.Cue(startTime, endTime, '');
topLevelCue.nestedCues = [
CeaUtils.createDefaultCue(startTime, endTime, text),
];
const caption = window.forceEmit(endTime, serviceNumber);
const region = new shaka.text.CueRegion();
region.id = 'svc1win0';
region.height = rowCount;
region.width = colCount;
region.heightUnits = shaka.text.CueRegion.units.LINES;
region.widthUnits = shaka.text.CueRegion.units.LINES;
region.viewportAnchorX = 50;
region.viewportAnchorY = 99;
region.regionAnchorX = 50;
region.regionAnchorY = 100;
region.viewportAnchorUnits = shaka.text.CueRegion.units.PERCENTAGE;
expect(caption.cue.region).toEqual(region);
});
it('handles top of video window anchors', () => {
window = new shaka.cea.Cea708Window(/* windowNum= */ 0,
/* serviceNumber= */ 2);
window.defineWindow(
/* visible= */ true, /* verticalAnchor= */ 0,
/* horAnchor= */ 50, /* anchorId= */ 1, /* relativeToggle= */ true,
rowCount, colCount);
window.setStartTime(startTime);
const text = 'test word';
for (const c of text) {
window.setCharacter(c);
}
const topLevelCue = new shaka.text.Cue(startTime, endTime, '');
topLevelCue.nestedCues = [
CeaUtils.createDefaultCue(startTime, endTime, text),
];
const caption = window.forceEmit(endTime, serviceNumber);
const region = new shaka.text.CueRegion();
region.id = 'svc2win0';
region.height = rowCount;
region.width = colCount;
region.heightUnits = shaka.text.CueRegion.units.LINES;
region.widthUnits = shaka.text.CueRegion.units.LINES;
region.viewportAnchorX = 50;
region.viewportAnchorY = 0;
region.regionAnchorX = 50;
region.regionAnchorY = 0;
region.viewportAnchorUnits = shaka.text.CueRegion.units.PERCENTAGE;
expect(caption.cue.region).toEqual(region);
});
it('handles bottom right of video window anchors using line values', () => {
window = new shaka.cea.Cea708Window(/* windowNum= */ 0,
/* serviceNumbe= */ 3);
window.defineWindow(
/* visible= */ true, /* verticalAnchor= */ 74,
/* horAnchor= */ 209, /* anchorId= */ 8, /* relativeToggle= */ false,
rowCount, colCount);
window.setStartTime(startTime);
const text = 'test word';
for (const c of text) {
window.setCharacter(c);
}
const topLevelCue = new shaka.text.Cue(startTime, endTime, '');
topLevelCue.nestedCues = [
CeaUtils.createDefaultCue(startTime, endTime, text),
];
const caption = window.forceEmit(endTime, serviceNumber);
const region = new shaka.text.CueRegion();
region.id = 'svc3win0';
region.height = rowCount;
region.width = colCount;
region.heightUnits = shaka.text.CueRegion.units.LINES;
region.widthUnits = shaka.text.CueRegion.units.LINES;
region.viewportAnchorX = 209;
region.viewportAnchorY = 74;
region.regionAnchorX = 100;
region.regionAnchorY = 100;
region.viewportAnchorUnits = shaka.text.CueRegion.units.LINES;
expect(caption.cue.region).toEqual(region);
});
});
});