mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-25 17:45:03 +03:00
3b9af2efa6
Subtitle timing and offsets were not calculated correctly for HLS live streams. This issue appears to have affected all v4.x releases up to v4.0.5, v4.1.5, and v4.2.2.
1132 lines
34 KiB
JavaScript
1132 lines
34 KiB
JavaScript
/*! @license
|
|
* Shaka Player
|
|
* Copyright 2016 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
describe('VttTextParser', () => {
|
|
const Cue = shaka.text.Cue;
|
|
const CueRegion = shaka.text.CueRegion;
|
|
const originalLogWarning = shaka.log.warning;
|
|
const anyString = jasmine.any(String);
|
|
|
|
/** @type {!jasmine.Spy} */
|
|
let logWarningSpy;
|
|
|
|
beforeEach(() => {
|
|
logWarningSpy = jasmine.createSpy('shaka.log.warning');
|
|
shaka.log.warning = shaka.test.Util.spyFunc(logWarningSpy);
|
|
});
|
|
|
|
afterEach(() => {
|
|
shaka.log.warning = originalLogWarning;
|
|
});
|
|
|
|
it('supports no cues', () => {
|
|
verifyHelper([],
|
|
'WEBVTT',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('supports initial comments', () => {
|
|
verifyHelper([],
|
|
'WEBVTT - Comments',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('supports comment blocks', () => {
|
|
verifyHelper([],
|
|
'WEBVTT\n\n' +
|
|
'NOTE\n' +
|
|
'This is a comment block',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('supports comment blocks with inital comment', () => {
|
|
verifyHelper([],
|
|
'WEBVTT\n\n' +
|
|
'NOTE - A header comment\n' +
|
|
'This is a comment block',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('handles a blank line at the end of the file', () => {
|
|
verifyHelper(
|
|
[
|
|
{startTime: 20, endTime: 40, payload: 'Test'},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'00:00:20.000 --> 00:00:40.000\n' +
|
|
'Test\n\n',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('handles no blank line at the end of the file', () => {
|
|
verifyHelper(
|
|
[
|
|
{startTime: 20, endTime: 40, payload: 'Test'},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'00:00:20.000 --> 00:00:40.000\n' +
|
|
'Test\n',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('handles no newline after the final text payload', () => {
|
|
verifyHelper(
|
|
[
|
|
{startTime: 20, endTime: 40, payload: 'Test'},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'00:00:20.000 --> 00:00:40.000\n' +
|
|
'Test',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('supports cues with no settings', () => {
|
|
verifyHelper(
|
|
[
|
|
{startTime: 20, endTime: 40, payload: 'Test', id: '1'},
|
|
{startTime: 40, endTime: 50, payload: 'Test2', id: '2'},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'1\n' +
|
|
'00:00:20.000 --> 00:00:40.000\n' +
|
|
'Test\n\n' +
|
|
'2\n' +
|
|
'00:00:40.000 --> 00:00:50.000\n' +
|
|
'Test2',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('supports cues with no ID', () => {
|
|
verifyHelper(
|
|
[
|
|
{startTime: 20, endTime: 40, payload: 'Test'},
|
|
{startTime: 40, endTime: 50, payload: 'Test2'},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'00:00:20.000 --> 00:00:40.000\n' +
|
|
'Test\n\n' +
|
|
'00:00:40.000 --> 00:00:50.000\n' +
|
|
'Test2',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('supports comments within cues', () => {
|
|
verifyHelper(
|
|
[
|
|
{startTime: 20, endTime: 40, payload: 'Test'},
|
|
{startTime: 40, endTime: 50, payload: 'Test2'},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'00:00:20.000 --> 00:00:40.000\n' +
|
|
'Test\n\n' +
|
|
'NOTE\n' +
|
|
'This is a note\n\n' +
|
|
'00:00:40.000 --> 00:00:50.000\n' +
|
|
'Test2',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('supports non-integer timecodes', () => {
|
|
verifyHelper(
|
|
[
|
|
{startTime: 20.1, endTime: 40.505, payload: 'Test'},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'00:00:20.100 --> 00:00:40.505\n' +
|
|
'Test',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('supports large timecodes', () => {
|
|
verifyHelper(
|
|
[
|
|
{startTime: 20, endTime: 108000, payload: 'Test'},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'00:00:20.000 --> 30:00:00.000\n' +
|
|
'Test',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('requires header', () => {
|
|
errorHelper(shaka.util.Error.Code.INVALID_TEXT_HEADER,
|
|
'',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
errorHelper(shaka.util.Error.Code.INVALID_TEXT_HEADER,
|
|
'00:00:00.000 --> 00:00:00.020\nTest',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('rejects invalid time values', () => {
|
|
errorHelper(shaka.util.Error.Code.INVALID_TEXT_CUE,
|
|
'WEBVTT\n\n00.020 --> 0:00.040\nTest',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0},
|
|
anyString);
|
|
errorHelper(shaka.util.Error.Code.INVALID_TEXT_CUE,
|
|
'WEBVTT\n\n0:00.020 --> 0:00.040\nTest',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0},
|
|
anyString);
|
|
errorHelper(shaka.util.Error.Code.INVALID_TEXT_CUE,
|
|
'WEBVTT\n\n00:00.20 --> 0:00.040\nTest',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0},
|
|
anyString);
|
|
errorHelper(shaka.util.Error.Code.INVALID_TEXT_CUE,
|
|
'WEBVTT\n\n00:100.20 --> 0:00.040\nTest',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0},
|
|
anyString);
|
|
errorHelper(shaka.util.Error.Code.INVALID_TEXT_CUE,
|
|
'WEBVTT\n\n00:00.020 --> 0:00.040\nTest',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0},
|
|
anyString);
|
|
errorHelper(shaka.util.Error.Code.INVALID_TEXT_CUE,
|
|
'WEBVTT\n\n00:00:00:00.020 --> 0:00.040\nTest',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0},
|
|
anyString);
|
|
errorHelper(shaka.util.Error.Code.INVALID_TEXT_CUE,
|
|
'WEBVTT\n\n00:61.020 --> 0:00.040\nTest',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0},
|
|
anyString);
|
|
errorHelper(shaka.util.Error.Code.INVALID_TEXT_CUE,
|
|
'WEBVTT\n\n61:00.020 --> 0:00.040\nTest',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0},
|
|
anyString);
|
|
});
|
|
|
|
it('supports vertical setting', () => {
|
|
verifyHelper(
|
|
[
|
|
{
|
|
startTime: 20,
|
|
endTime: 40,
|
|
payload: 'Test',
|
|
writingMode: Cue.writingMode.VERTICAL_RIGHT_TO_LEFT,
|
|
},
|
|
{
|
|
startTime: 40,
|
|
endTime: 50,
|
|
payload: 'Test2',
|
|
writingMode: Cue.writingMode.VERTICAL_LEFT_TO_RIGHT,
|
|
},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'00:00:20.000 --> 00:00:40.000 vertical:rl\n' +
|
|
'Test\n\n' +
|
|
'00:00:40.000 --> 00:00:50.000 vertical:lr\n' +
|
|
'Test2',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('supports line setting', () => {
|
|
verifyHelper(
|
|
[
|
|
{
|
|
startTime: 20, endTime: 40, payload: 'Test', line: 0,
|
|
lineInterpretation: Cue.lineInterpretation.LINE_NUMBER,
|
|
},
|
|
{
|
|
startTime: 40, endTime: 50, payload: 'Test2', line: -1,
|
|
lineInterpretation: Cue.lineInterpretation.LINE_NUMBER,
|
|
},
|
|
{
|
|
startTime: 50, endTime: 60, payload: 'Test3', line: 45,
|
|
lineInterpretation: Cue.lineInterpretation.PERCENTAGE,
|
|
},
|
|
{
|
|
startTime: 55, endTime: 65, payload: 'Test4', line: 12.3,
|
|
lineInterpretation: Cue.lineInterpretation.PERCENTAGE,
|
|
},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'00:00:20.000 --> 00:00:40.000 line:0\n' +
|
|
'Test\n\n' +
|
|
'00:00:40.000 --> 00:00:50.000 line:-1\n' +
|
|
'Test2\n\n' +
|
|
'00:00:50.000 --> 00:01:00.000 line:45%\n' +
|
|
'Test3\n\n' +
|
|
'00:00:55.000 --> 00:01:05.000 line:12.3%\n' +
|
|
'Test4\n\n',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('supports line setting with optional part', () => {
|
|
verifyHelper(
|
|
[
|
|
{
|
|
startTime: 20, endTime: 40, payload: 'Test', line: 10,
|
|
lineInterpretation: Cue.lineInterpretation.PERCENTAGE,
|
|
lineAlign: Cue.lineAlign.START,
|
|
},
|
|
{
|
|
startTime: 40, endTime: 50, payload: 'Test2', line: -1,
|
|
lineInterpretation: Cue.lineInterpretation.LINE_NUMBER,
|
|
lineAlign: Cue.lineAlign.CENTER,
|
|
},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'00:00:20.000 --> 00:00:40.000 line:10%,start\n' +
|
|
'Test\n\n' +
|
|
'00:00:40.000 --> 00:00:50.000 line:-1,center\n' +
|
|
'Test2',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('supports position setting', () => {
|
|
verifyHelper(
|
|
[
|
|
{startTime: 20, endTime: 40, payload: 'Test', position: 45},
|
|
{startTime: 25, endTime: 45, payload: 'Test2', position: 12.3},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'00:00:20.000 --> 00:00:40.000 position:45%\n' +
|
|
'Test\n\n' +
|
|
'00:00:25.000 --> 00:00:45.000 position:12.3%\n' +
|
|
'Test2\n\n',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('supports position setting with optional part', () => {
|
|
verifyHelper(
|
|
[
|
|
{
|
|
startTime: 20, endTime: 40, payload: 'Test', position: 45,
|
|
positionAlign: Cue.positionAlign.LEFT,
|
|
},
|
|
{
|
|
startTime: 20, endTime: 40, payload: 'Test2', position: 45,
|
|
positionAlign: Cue.positionAlign.LEFT,
|
|
},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'00:00:20.000 --> 00:00:40.000 position:45%,line-left\n' +
|
|
'Test\n\n' +
|
|
'00:00:20.000 --> 00:00:40.000 position:45%,start\n' +
|
|
'Test2',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('supports size setting', () => {
|
|
verifyHelper(
|
|
[
|
|
{startTime: 20, endTime: 40, payload: 'Test', size: 56},
|
|
{startTime: 25, endTime: 45, payload: 'Test2', size: 12.3},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'00:00:20.000 --> 00:00:40.000 size:56%\n' +
|
|
'Test\n\n' +
|
|
'00:00:25.000 --> 00:00:45.000 size:12.3%\n' +
|
|
'Test2\n\n',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('supports align setting', () => {
|
|
verifyHelper(
|
|
[
|
|
{startTime: 20, endTime: 40, payload: 'Test', textAlign: 'center'},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'00:00:20.000 --> 00:00:40.000 align:center\n' +
|
|
'Test',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('supports multiple settings', () => {
|
|
verifyHelper(
|
|
[
|
|
{
|
|
startTime: 20,
|
|
endTime: 40,
|
|
payload: 'Test',
|
|
textAlign: Cue.textAlign.CENTER,
|
|
size: 56,
|
|
writingMode: Cue.writingMode.VERTICAL_LEFT_TO_RIGHT,
|
|
},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'00:00:20.000 --> 00:00:40.000 align:center size:56% vertical:lr\n' +
|
|
'Test',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('supports timestamps with one-digit hour at start time', () => {
|
|
verifyHelper(
|
|
[
|
|
{
|
|
startTime: 20,
|
|
endTime: 40,
|
|
payload: 'Test',
|
|
textAlign: Cue.textAlign.CENTER,
|
|
size: 56,
|
|
writingMode: Cue.writingMode.VERTICAL_LEFT_TO_RIGHT,
|
|
},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'0:00:20.000 --> 00:00:40.000 align:center size:56% vertical:lr\n' +
|
|
'Test',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('supports timestamps with one-digit hour at end time', () => {
|
|
verifyHelper(
|
|
[
|
|
{
|
|
startTime: 20,
|
|
endTime: 40,
|
|
payload: 'Test',
|
|
textAlign: Cue.textAlign.CENTER,
|
|
size: 56,
|
|
writingMode: Cue.writingMode.VERTICAL_LEFT_TO_RIGHT,
|
|
},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'00:00:20.000 --> 0:00:40.000 align:center size:56% vertical:lr\n' +
|
|
'Test',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('supports stamps with one-digit hours at start & end time', () => {
|
|
verifyHelper(
|
|
[
|
|
{
|
|
startTime: 20,
|
|
endTime: 40,
|
|
payload: 'Test',
|
|
textAlign: 'center',
|
|
size: 56,
|
|
writingMode: Cue.writingMode.VERTICAL_LEFT_TO_RIGHT,
|
|
},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'0:00:20.000 --> 0:00:40.000 align:center size:56% vertical:lr\n' +
|
|
'Test',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('uses time offset from vttOffset, not periodStart or segmentStart', () => {
|
|
verifyHelper(
|
|
[
|
|
{
|
|
startTime: 50,
|
|
endTime: 60,
|
|
payload: 'Test',
|
|
textAlign: 'center',
|
|
size: 56,
|
|
writingMode: Cue.writingMode.VERTICAL_LEFT_TO_RIGHT,
|
|
},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'0:00:10.000 --> 0:00:20.000 align:center size:56% vertical:lr\n' +
|
|
'Test',
|
|
{periodStart: 60, segmentStart: 80, segmentEnd: 100, vttOffset: 40});
|
|
});
|
|
|
|
it('parses VTTRegions', () => {
|
|
verifyHelper(
|
|
[
|
|
{
|
|
startTime: 20,
|
|
endTime: 40,
|
|
payload: 'Test',
|
|
region: jasmine.objectContaining({
|
|
id: 'reg1',
|
|
viewportAnchorX: 10,
|
|
viewportAnchorY: 90,
|
|
regionAnchorX: 0,
|
|
regionAnchorY: 100,
|
|
width: 50,
|
|
height: 3,
|
|
heightUnits: CueRegion.units.LINES,
|
|
widthUnits: CueRegion.units.PERCENTAGE,
|
|
viewportAnchorUnits: CueRegion.units.PERCENTAGE,
|
|
scroll: CueRegion.scrollMode.UP,
|
|
}),
|
|
},
|
|
],
|
|
'WEBVTT\n' +
|
|
'Region: id=reg1 width=50% lines=3 regionanchor=0%,100% ' +
|
|
'viewportanchor=10%,90% scroll=up\n\n' +
|
|
'0:00:20.000 --> 0:00:40.000 region:reg1\n' +
|
|
'Test',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('ignores and logs invalid settings', () => {
|
|
expect(logWarningSpy).not.toHaveBeenCalled();
|
|
|
|
verifyHelper(
|
|
[
|
|
{startTime: 20, endTime: 40, payload: 'Test'},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'00:00:20.000 --> 00:00:40.000 vertical:es\n' +
|
|
'Test\n\n',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
|
|
verifyHelper(
|
|
[
|
|
{startTime: 20, endTime: 40, payload: 'Test'},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'00:00:20.000 --> 00:00:40.000 vertical:\n' +
|
|
'Test\n\n',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
|
|
verifyHelper(
|
|
[
|
|
{startTime: 20, endTime: 40, payload: 'Test'},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'00:00:20.000 --> 00:00:40.000 vertical\n' +
|
|
'Test\n\n',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
|
|
verifyHelper(
|
|
[
|
|
{startTime: 20, endTime: 40, payload: 'Test'},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'00:00:20.000 --> 00:00:40.000 line:-3%\n' +
|
|
'Test\n\n',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
|
|
verifyHelper(
|
|
[
|
|
{startTime: 20, endTime: 40, payload: 'Test'},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'00:00:20.000 --> 00:00:40.000 line:45%%\n' +
|
|
'Test\n\n',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
|
|
verifyHelper(
|
|
[
|
|
{startTime: 20, endTime: 40, payload: 'Test'},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'00:00:20.000 --> 00:00:40.000 align:10\n' +
|
|
'Test\n\n',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
|
|
verifyHelper(
|
|
[
|
|
{startTime: 20, endTime: 40, payload: 'Test'},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'00:00:20.000 --> 00:00:40.000 align:foo\n' +
|
|
'Test\n\n',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
|
|
expect(logWarningSpy).toHaveBeenCalledTimes(7);
|
|
});
|
|
|
|
it('parses X-TIMESTAMP-MAP header', () => {
|
|
verifyHelper(
|
|
[
|
|
{startTime: 30, endTime: 50, payload: 'Test'},
|
|
{startTime: 50, endTime: 60, payload: 'Test2'},
|
|
],
|
|
// 900000 = 10 sec, so expect every timestamp to be 10
|
|
// seconds ahead of what is specified.
|
|
'WEBVTT\n' +
|
|
'X-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000\n\n' +
|
|
'00:00:20.000 --> 00:00:40.000 line:0\n' +
|
|
'Test\n\n' +
|
|
'00:00:40.000 --> 00:00:50.000 line:-1\n' +
|
|
'Test2',
|
|
{periodStart: 0, segmentStart: 25, segmentEnd: 65, vttOffset: 0},
|
|
/* sequenceMode= */ true);
|
|
});
|
|
|
|
it('ignores X-TIMESTAMP-MAP header if not in sequence mode', () => {
|
|
verifyHelper(
|
|
[
|
|
{startTime: 20, endTime: 40, payload: 'Test'},
|
|
{startTime: 40, endTime: 50, payload: 'Test2'},
|
|
],
|
|
'WEBVTT\n' +
|
|
'X-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:01:00:00.000\n\n' +
|
|
'00:00:20.000 --> 00:00:40.000 line:0\n' +
|
|
'Test\n\n' +
|
|
'00:00:40.000 --> 00:00:50.000 line:-1\n' +
|
|
'Test2',
|
|
{periodStart: 0, segmentStart: 25, segmentEnd: 65, vttOffset: 0},
|
|
/* sequenceMode= */ false);
|
|
});
|
|
|
|
it('parses X-TIMESTAMP-MAP header with non-zero local base', () => {
|
|
verifyHelper(
|
|
[
|
|
{startTime: 1800, endTime: 1810, payload: 'Test'},
|
|
{startTime: 1820, endTime: 1830, payload: 'Test2'},
|
|
],
|
|
// 162000000 = 30 * 60 * 90k = 30 minutes for the TS part of the map.
|
|
// The local (VTT) part of the map is 1 hour.
|
|
// So text times of 1 hour map to media times of 30 minutes.
|
|
'WEBVTT\n' +
|
|
'X-TIMESTAMP-MAP=MPEGTS:162000000,LOCAL:01:00:00.000\n\n' +
|
|
'01:00:00.000 --> 01:00:10.000 line:0\n' +
|
|
'Test\n\n' +
|
|
'01:00:20.000 --> 01:00:30.000 line:-1\n' +
|
|
'Test2',
|
|
{periodStart: 0, segmentStart: 25, segmentEnd: 65, vttOffset: 0},
|
|
/* sequenceMode= */ true);
|
|
});
|
|
|
|
it('combines X-TIMESTAMP-MAP header with periodStart', () => {
|
|
verifyHelper(
|
|
[
|
|
{startTime: 130, endTime: 150, payload: 'Test'},
|
|
{startTime: 150, endTime: 160, payload: 'Test2'},
|
|
],
|
|
// 900000 = 10 sec, so expect every timestamp to be 10
|
|
// seconds ahead of what is specified.
|
|
'WEBVTT\n' +
|
|
'X-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000\n\n' +
|
|
'00:00:20.000 --> 00:00:40.000 line:0\n' +
|
|
'Test\n\n' +
|
|
'00:00:40.000 --> 00:00:50.000 line:-1\n' +
|
|
'Test2',
|
|
{periodStart: 100, segmentStart: 25, segmentEnd: 65, vttOffset: 0},
|
|
/* sequenceMode= */ true);
|
|
});
|
|
|
|
it('handles timestamp rollover with X-TIMESTAMP-MAP header', () => {
|
|
verifyHelper(
|
|
[
|
|
{startTime: 95443, endTime: 95445, payload: 'Test'},
|
|
],
|
|
// 8589870000/90000 = 95443 sec, so expect every timestamp to be 95443
|
|
// seconds ahead of what is specified.
|
|
'WEBVTT\n' +
|
|
'X-TIMESTAMP-MAP=MPEGTS:8589870000,LOCAL:00:00:00.000\n\n' +
|
|
'00:00:00.000 --> 00:00:02.000 line:0\n' +
|
|
'Test',
|
|
// Non-null segmentStart takes precedence over X-TIMESTAMP-MAP.
|
|
// This protects us from rollover in the MPEGTS field.
|
|
{periodStart: 0, segmentStart: 95440, segmentEnd: 95550, vttOffset: 0},
|
|
/* sequenceMode= */ true);
|
|
|
|
verifyHelper(
|
|
[
|
|
{startTime: 95552, endTime: 95554, payload: 'Test2'},
|
|
],
|
|
// 95550 is larger than the roll over timestamp, so the timestamp offset
|
|
// gets rolled over.
|
|
// (9745408 + 0x200000000) / 90000 = 95552 sec
|
|
'WEBVTT\n' +
|
|
'X-TIMESTAMP-MAP=MPEGTS:9745408,LOCAL:00:00:00.000\n\n' +
|
|
'00:00:00.000 --> 00:00:02.000 line:0\n' +
|
|
'Test2',
|
|
{periodStart: 0, segmentStart: 95550, segmentEnd: 95560, vttOffset: 0},
|
|
/* sequenceMode= */ true);
|
|
});
|
|
|
|
// A mock-up of HLS live subs as seen in b/253104251.
|
|
it('handles timestamp rollover and negative offset in HLS live', () => {
|
|
// Similar to values seen in b/253104251, for a realistic regression test.
|
|
// When using sequence mode on live HLS, we get negative offsets that
|
|
// represent the timestamp of our first append in sequence mode.
|
|
verifyHelper(
|
|
[
|
|
{startTime: 3600, endTime: 3602, payload: 'Test'},
|
|
],
|
|
'WEBVTT\n' +
|
|
'X-TIMESTAMP-MAP=MPEGTS:8355814896,LOCAL:00:00:00.000\n\n' +
|
|
'00:00:00.000 --> 00:00:02.000 line:0\n' +
|
|
'Test',
|
|
{
|
|
periodStart: -1234567,
|
|
segmentStart: 3600,
|
|
segmentEnd: 3610,
|
|
vttOffset: -1234567,
|
|
},
|
|
/* sequenceMode= */ true);
|
|
});
|
|
|
|
it('supports global style blocks', () => {
|
|
const textShadow = '-1px 0 black, 0 1px black, 1px 0 black, 0 -1px black';
|
|
verifyHelper(
|
|
[
|
|
{
|
|
startTime: 20,
|
|
endTime: 40,
|
|
payload: 'Test',
|
|
color: 'cyan',
|
|
fontSize: '10px',
|
|
textShadow: textShadow,
|
|
},
|
|
{
|
|
startTime: 40,
|
|
endTime: 50,
|
|
payload: 'Test2',
|
|
color: 'cyan',
|
|
fontSize: '10px',
|
|
textShadow: textShadow,
|
|
},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'STYLE\n' +
|
|
'::cue {\n' +
|
|
'color: cyan;\n'+
|
|
'font-size: 10px;\n'+
|
|
`text-shadow: ${textShadow};\n`+
|
|
'}\n\n' +
|
|
'00:00:20.000 --> 00:00:40.000\n' +
|
|
'Test\n\n' +
|
|
'00:00:40.000 --> 00:00:50.000\n' +
|
|
'Test2',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('supports global style blocks without blank lines', () => {
|
|
verifyHelper(
|
|
[
|
|
{
|
|
startTime: 20,
|
|
endTime: 40,
|
|
payload: 'Test',
|
|
color: 'cyan',
|
|
fontSize: '10px',
|
|
},
|
|
{
|
|
startTime: 40,
|
|
endTime: 50,
|
|
payload: 'Test2',
|
|
color: 'cyan',
|
|
fontSize: '10px',
|
|
},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'STYLE\n' +
|
|
'::cue { color: cyan; font-size: 10px; }\n\n' +
|
|
'00:00:20.000 --> 00:00:40.000\n' +
|
|
'Test\n\n' +
|
|
'00:00:40.000 --> 00:00:50.000\n' +
|
|
'Test2',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('supports payload stylized', () => {
|
|
verifyHelper(
|
|
[
|
|
{
|
|
startTime: 10,
|
|
endTime: 20,
|
|
payload: '',
|
|
nestedCues: [
|
|
{
|
|
startTime: 10,
|
|
endTime: 20,
|
|
payload: 'Test',
|
|
fontWeight: Cue.fontWeight.BOLD,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
startTime: 20,
|
|
endTime: 30,
|
|
payload: '',
|
|
nestedCues: [
|
|
{
|
|
startTime: 20,
|
|
endTime: 30,
|
|
payload: 'Test2',
|
|
fontStyle: Cue.fontStyle.ITALIC,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
startTime: 30,
|
|
endTime: 40,
|
|
payload: '',
|
|
nestedCues: [
|
|
{
|
|
startTime: 30,
|
|
endTime: 40,
|
|
payload: 'Test3',
|
|
textDecoration: [Cue.textDecoration.UNDERLINE],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
startTime: 40,
|
|
endTime: 50,
|
|
payload: '',
|
|
nestedCues: [
|
|
{
|
|
startTime: 40,
|
|
endTime: 50,
|
|
payload: 'Test4',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
startTime: 50,
|
|
endTime: 60,
|
|
payload: '',
|
|
nestedCues: [
|
|
{
|
|
startTime: 50,
|
|
endTime: 60,
|
|
payload: 'Test',
|
|
fontWeight: Cue.fontWeight.BOLD,
|
|
fontStyle: Cue.fontStyle.NORMAL,
|
|
},
|
|
{
|
|
startTime: 50,
|
|
endTime: 60,
|
|
payload: '5',
|
|
fontWeight: Cue.fontWeight.BOLD,
|
|
fontStyle: Cue.fontStyle.ITALIC,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
startTime: 70,
|
|
endTime: 80,
|
|
payload: '',
|
|
nestedCues: [
|
|
{
|
|
startTime: 70,
|
|
endTime: 80,
|
|
payload: 'Test',
|
|
fontWeight: Cue.fontWeight.NORMAL,
|
|
},
|
|
{
|
|
startTime: 70,
|
|
endTime: 80,
|
|
payload: '6',
|
|
fontWeight: Cue.fontWeight.BOLD,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
startTime: 80,
|
|
endTime: 90,
|
|
payload: '',
|
|
nestedCues: [
|
|
{
|
|
startTime: 80,
|
|
endTime: 90,
|
|
payload: 'Test ',
|
|
fontWeight: Cue.fontWeight.BOLD,
|
|
fontStyle: Cue.fontStyle.NORMAL,
|
|
},
|
|
{
|
|
startTime: 80,
|
|
endTime: 90,
|
|
payload: '7',
|
|
fontWeight: Cue.fontWeight.BOLD,
|
|
fontStyle: Cue.fontStyle.ITALIC,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
startTime: 90,
|
|
endTime: 100,
|
|
payload: '<b>Test<i>8</b>',
|
|
},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'00:00:10.000 --> 00:00:20.000\n' +
|
|
'<b>Test</b>\n\n' +
|
|
'00:00:20.000 --> 00:00:30.000\n' +
|
|
'<i>Test2</i>\n\n' +
|
|
'00:00:30.000 --> 00:00:40.000\n' +
|
|
'<u>Test3</u>\n\n' +
|
|
'00:00:40.000 --> 00:00:50.000\n' +
|
|
'<a>Test4</a>\n\n' +
|
|
'00:00:50.000 --> 00:01:00.000\n' +
|
|
'<b>Test<i>5</i></b>\n\n' +
|
|
'00:01:10.000 --> 00:01:20.000\n' +
|
|
'Test<b>6</b>\n\n' +
|
|
'00:01:20.000 --> 00:01:30.000\n' +
|
|
'<b>Test <i>7</i></b>\n\n' +
|
|
'00:01:30.000 --> 00:01:40.000\n' +
|
|
'<b>Test<i>8</b>',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('supports specific style blocks', () => {
|
|
verifyHelper(
|
|
[
|
|
{
|
|
startTime: 20,
|
|
endTime: 40,
|
|
payload: '',
|
|
nestedCues: [
|
|
{
|
|
startTime: 20,
|
|
endTime: 40,
|
|
payload: 'Test',
|
|
color: 'cyan',
|
|
fontWeight: Cue.fontWeight.BOLD,
|
|
},
|
|
],
|
|
},
|
|
{startTime: 40, endTime: 50, payload: 'Test2'},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'STYLE\n::cue(b) { color: cyan; }\n\n' +
|
|
'00:00:20.000 --> 00:00:40.000\n' +
|
|
'<b>Test</b>\n\n' +
|
|
'00:00:40.000 --> 00:00:50.000\n' +
|
|
'Test2',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('supports only two digits in the timestamp', () => {
|
|
verifyHelper(
|
|
[
|
|
{startTime: 20, endTime: 40, payload: 'Test'},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'00:00:20.00 --> 00:00:40.00\n' +
|
|
'Test',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('supports class with default color', () => {
|
|
verifyHelper(
|
|
[
|
|
{
|
|
startTime: 20, endTime: 40,
|
|
payload: '',
|
|
nestedCues: [
|
|
{
|
|
startTime: 20,
|
|
endTime: 40,
|
|
payload: 'Test',
|
|
color: '#FF0',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
startTime: 40, endTime: 50,
|
|
payload: '',
|
|
nestedCues: [
|
|
{
|
|
startTime: 40,
|
|
endTime: 50,
|
|
payload: 'Test2',
|
|
color: '#0FF',
|
|
backgroundColor: '#00F',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
startTime: 50, endTime: 60,
|
|
payload: '',
|
|
nestedCues: [
|
|
{
|
|
startTime: 50,
|
|
endTime: 60,
|
|
payload: 'Test 3',
|
|
color: '#F0F',
|
|
backgroundColor: '#000',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
startTime: 60,
|
|
endTime: 70,
|
|
payload: '',
|
|
nestedCues: [
|
|
{
|
|
startTime: 60,
|
|
endTime: 70,
|
|
payload: 'First row',
|
|
},
|
|
{
|
|
startTime: 60,
|
|
endTime: 70,
|
|
payload: 'Test4.1',
|
|
color: '#FF0',
|
|
},
|
|
{
|
|
startTime: 60,
|
|
endTime: 70,
|
|
payload: '',
|
|
lineBreak: true,
|
|
},
|
|
{
|
|
startTime: 60,
|
|
endTime: 70,
|
|
payload: 'Second row',
|
|
},
|
|
{
|
|
startTime: 60,
|
|
endTime: 70,
|
|
payload: 'Test4.2',
|
|
color: '#00F',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
startTime: 70,
|
|
endTime: 80,
|
|
payload: '',
|
|
nestedCues: [
|
|
{
|
|
startTime: 70,
|
|
endTime: 80,
|
|
payload: 'Test5.1',
|
|
color: '#F00',
|
|
},
|
|
{
|
|
startTime: 70,
|
|
endTime: 80,
|
|
payload: 'Test5.2',
|
|
color: '#0F0',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
startTime: 80,
|
|
endTime: 90,
|
|
payload: '<b><c.lime>Parse fail 1</b></c>',
|
|
nestedCues: [],
|
|
},
|
|
{
|
|
startTime: 90,
|
|
endTime: 100,
|
|
payload: '<c.lime><b>Parse fail 2</c></b>',
|
|
nestedCues: [],
|
|
},
|
|
{
|
|
startTime: 100,
|
|
endTime: 110,
|
|
payload: '<c.lime>forward slash 1/2 in text</c>',
|
|
nestedCues: [],
|
|
},
|
|
{
|
|
startTime: 110,
|
|
endTime: 120,
|
|
payload: '<c.lime>less or more < > in text</c>',
|
|
nestedCues: [],
|
|
},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'00:00:20.000 --> 00:00:40.000\n' +
|
|
'<c.yellow>Test</c>\n\n' +
|
|
'00:00:40.000 --> 00:00:50.000\n' +
|
|
'<c.cyan.bg_blue>Test2</c>\n\n' +
|
|
'00:00:50.000 --> 00:01:00.000\n' +
|
|
'<c.yellow.bg_blue.magenta.bg_black>Test 3</c>\n\n' +
|
|
'00:01:00.000 --> 00:01:10.000\n' +
|
|
'First row<c.yellow>Test4.1</c>\nSecond row<c.blue>Test4.2</c>\n\n' +
|
|
'00:01:10.000 --> 00:01:20.000\n' +
|
|
'<c.red>Test5.1<c.lime>Test5.2</c></c>\n\n' +
|
|
'00:01:20.000 --> 00:01:30.000\n' +
|
|
'<b><c.lime>Parse fail 1</b></c>\n\n' +
|
|
'00:01:30.000 --> 00:01:40.000\n' +
|
|
'<c.lime><b>Parse fail 2</c></b>\n\n' +
|
|
'00:01:40.000 --> 00:01:50.000\n' +
|
|
'<c.lime>forward slash 1/2 in text</c>\n\n' +
|
|
'00:01:50.000 --> 00:02:00.000\n' +
|
|
'<c.lime>less or more < > in text</c>',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('supports karaoke style text', () => {
|
|
verifyHelper(
|
|
[
|
|
{
|
|
startTime: 20, endTime: 40,
|
|
payload: '',
|
|
nestedCues: [
|
|
{
|
|
startTime: 20,
|
|
endTime: 40,
|
|
payload: 'Test',
|
|
},
|
|
{
|
|
startTime: 25,
|
|
endTime: 40,
|
|
payload: ' 1',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'00:00:20.00 --> 00:00:40.00\n' +
|
|
'Test<00:00:25.00> 1',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
it('supports default color overriding', () => {
|
|
verifyHelper(
|
|
[
|
|
{
|
|
startTime: 10, endTime: 20,
|
|
payload: '',
|
|
nestedCues: [
|
|
{
|
|
startTime: 10,
|
|
endTime: 20,
|
|
payload: 'Example 1',
|
|
color: '#F00',
|
|
backgroundColor: '#FF0',
|
|
fontSize: '10px',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
'WEBVTT\n\n' +
|
|
'STYLE\n' +
|
|
'::cue(bg_blue) { font-size: 10px; background-color: #FF0 }\n\n' +
|
|
'00:00:10.000 --> 00:00:20.000\n' +
|
|
'<c.red.bg_blue>Example 1</c>\n\n',
|
|
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
|
|
});
|
|
|
|
/**
|
|
* @param {!Array} cues
|
|
* @param {string} text
|
|
* @param {shaka.extern.TextParser.TimeContext} time
|
|
* @param {boolean=} sequenceMode
|
|
*/
|
|
function verifyHelper(cues, text, time, sequenceMode = false) {
|
|
const data =
|
|
shaka.util.BufferUtils.toUint8(shaka.util.StringUtils.toUTF8(text));
|
|
const parser = new shaka.text.VttTextParser();
|
|
parser.setSequenceMode(sequenceMode);
|
|
const result = parser.parseMedia(data, time);
|
|
|
|
const expected = cues.map((cue) => {
|
|
if (cue.nestedCues) {
|
|
cue.nestedCues = cue.nestedCues.map(
|
|
(nestedCue) => jasmine.objectContaining(nestedCue));
|
|
}
|
|
return jasmine.objectContaining(cue);
|
|
});
|
|
expect(result).toEqual(expected);
|
|
}
|
|
|
|
/**
|
|
* @param {shaka.util.Error.Code} code
|
|
* @param {string} text
|
|
* @param {shaka.extern.TextParser.TimeContext} time
|
|
* @param {*=} errorData
|
|
*/
|
|
function errorHelper(code, text, time, errorData = undefined) {
|
|
let shakaError;
|
|
if (errorData) {
|
|
shakaError = new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.TEXT,
|
|
code, errorData);
|
|
} else {
|
|
shakaError = new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.TEXT,
|
|
code);
|
|
}
|
|
const error = shaka.test.Util.jasmineError(shakaError);
|
|
const data =
|
|
shaka.util.BufferUtils.toUint8(shaka.util.StringUtils.toUTF8(text));
|
|
expect(() => new shaka.text.VttTextParser().parseMedia(data, time))
|
|
.toThrow(error);
|
|
}
|
|
});
|