feat!: Remove LRC, SBV, SSA support (#9330)

Related to https://github.com/shaka-project/shaka-player/issues/9321
This commit is contained in:
Álvaro Velad Galván
2025-11-04 20:07:02 +01:00
committed by GitHub
parent e85b648ef0
commit 4bc2518e29
11 changed files with 0 additions and 1143 deletions
-6
View File
@@ -298,12 +298,6 @@ Shaka Player supports:
(depends on browser support via MediaSource).
- SubRip (SRT)
- UTF-8 encoding only
- LyRiCs (LRC)
- UTF-8 encoding only
- SubStation Alpha (SSA, ASS)
- UTF-8 encoding only
- SubViewer (SBV)
- UTF-8 encoding only
Subtitles are rendered by the browser by default. Applications can create a
[text display plugin][] for customer rendering to go beyond browser-supported
-3
View File
@@ -1,6 +1,3 @@
# Optional plugins related to text parsing and displaying.
+../../lib/text/lrc_text_parser.js
+../../lib/text/sbv_text_parser.js
+../../lib/text/srt_text_parser.js
+../../lib/text/ssa_text_parser.js
-3
View File
@@ -39,9 +39,6 @@ __Subtitle/caption parsers__
- TTML: {@linksource shaka.text.TtmlTextParser} and
{@linksource shaka.text.Mp4TtmlParser}
- SubRip (SRT): {@linksource shaka.text.SrtTextParser}
- LyRiCs (LRC): {@linksource shaka.text.LrcTextParser}
- SubStation Alpha (SSA, ASS): {@linksource shaka.text.SsaTextParser}
- SubViewer (SBV): {@linksource shaka.text.SbvTextParser}
__Subtitle/caption displayers__
- Configured at runtime on a Player instance
-4
View File
@@ -94,14 +94,10 @@ shaka.net.NetworkingUtils.EXTENSIONS_TO_MIME_TYPES_ = new Map()
.set('aac', 'audio/aac')
.set('flac', 'audio/flac')
.set('wav', 'audio/wav')
.set('sbv', 'text/x-subviewer')
.set('srt', 'text/srt')
.set('vtt', 'text/vtt')
.set('webvtt', 'text/vtt')
.set('ttml', 'application/ttml+xml')
.set('lrc', 'application/x-subtitle-lrc')
.set('ssa', 'text/x-ssa')
.set('ass', 'text/x-ssa')
.set('jpeg', 'image/jpeg')
.set('jpg', 'image/jpeg')
.set('png', 'image/png')
-123
View File
@@ -1,123 +0,0 @@
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.text.LrcTextParser');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.text.Cue');
goog.require('shaka.text.TextEngine');
goog.require('shaka.util.StringUtils');
/**
* LRC file format: https://en.wikipedia.org/wiki/LRC_(file_format)
*
* @implements {shaka.extern.TextParser}
* @export
*/
shaka.text.LrcTextParser = class {
/**
* @override
* @export
*/
parseInit(data) {
goog.asserts.assert(false, 'LRC does not have init segments');
}
/**
* @override
* @export
*/
setManifestType(manifestType) {
// Unused.
}
/**
* @override
* @export
*/
parseMedia(data, time) {
const StringUtils = shaka.util.StringUtils;
const LrcTextParser = shaka.text.LrcTextParser;
// Get the input as a string.
const str = StringUtils.fromUTF8(data);
/** @type {shaka.text.Cue} */
let prevCue = null;
/** @type {!Array<!shaka.text.Cue>} */
const cues = [];
const lines = str.split(/\r?\n/);
for (const line of lines) {
if (!line || /^\s+$/.test(line)) {
continue;
}
// LRC content
const match = LrcTextParser.lyricLine_.exec(line);
if (match) {
const startTime = LrcTextParser.parseTime_(match[1]);
// This time can be overwritten by a subsequent cue.
// By default we add 2 seconds of duration.
const endTime = time.segmentEnd ? time.segmentEnd : startTime + 2;
const payload = match[2];
const cue = new shaka.text.Cue(startTime, endTime, payload);
// Update previous
if (prevCue) {
prevCue.endTime = startTime;
cues.push(prevCue);
}
prevCue = cue;
continue;
}
shaka.log.warning('LrcTextParser encountered an unknown line.', line);
}
if (prevCue) {
cues.push(prevCue);
}
return cues;
}
/**
* Parses a LRC time from the given parser.
*
* @param {string} string
* @return {number}
* @private
*/
static parseTime_(string) {
const LrcTextParser = shaka.text.LrcTextParser;
const match = LrcTextParser.timeFormat_.exec(string);
const minutes = parseInt(match[1], 10);
const seconds = parseFloat(match[2].replace(',', '.'));
return minutes * 60 + seconds;
}
};
/**
* @const
* @private {!RegExp}
* @example [00:12.0]Text or [00:12.00]Text or [00:12.000]Text or
* [00:12,0]Text or [00:12,00]Text or [00:12,000]Text
*/
shaka.text.LrcTextParser.lyricLine_ =
/^\[(\d{1,2}:\d{1,2}(?:[.,]\d{1,3})?)\](.*)/;
/**
* @const
* @private {!RegExp}
* @example 00:12.0 or 00:12.00 or 00:12.000 or
* 00:12,0 or 00:12,00 or 00:12,000
*/
shaka.text.LrcTextParser.timeFormat_ =
/^(\d+):(\d{1,2}(?:[.,]\d{1,3})?)$/;
shaka.text.TextEngine.registerParser(
'application/x-subtitle-lrc', () => new shaka.text.LrcTextParser());
-91
View File
@@ -1,91 +0,0 @@
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.text.SbvTextParser');
goog.require('goog.asserts');
goog.require('shaka.text.Cue');
goog.require('shaka.text.TextEngine');
goog.require('shaka.util.Error');
goog.require('shaka.util.StringUtils');
goog.require('shaka.util.TextParser');
/**
* @implements {shaka.extern.TextParser}
* @export
*/
shaka.text.SbvTextParser = class {
/**
* @override
* @export
*/
parseInit(data) {
goog.asserts.assert(false, 'SubViewer does not have init segments');
}
/**
* @override
* @export
*/
setManifestType(manifestType) {
// Unused.
}
/**
* @override
* @export
*/
parseMedia(data, time) {
const StringUtils = shaka.util.StringUtils;
// Get the input as a string.
const strFromData = StringUtils.fromUTF8(data);
// remove dos newlines
let str = strFromData.replace(/\r+/g, '');
// trim white space start and end
str = str.trim();
/** @type {!Array<!shaka.text.Cue>} */
const cues = [];
// Supports no cues
if (str == '') {
return cues;
}
// get cues
const blocklist = str.split('\n\n');
for (const block of blocklist) {
const lines = block.split('\n');
// Parse the times.
const parser = new shaka.util.TextParser(lines[0]);
const start = parser.parseTime();
const expect = parser.readRegex(/,/g);
const end = parser.parseTime();
if (start == null || expect == null || end == null) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.TEXT,
shaka.util.Error.Code.INVALID_TEXT_CUE,
'Could not parse cue time range in SubViewer');
}
// Get the payload.
const payload = lines.slice(1).join('\n').trim();
const cue = new shaka.text.Cue(start, end, payload);
cues.push(cue);
}
return cues;
}
};
shaka.text.TextEngine.registerParser(
'text/x-subviewer', () => new shaka.text.SbvTextParser());
-347
View File
@@ -1,347 +0,0 @@
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
// cspell:ignore AABBGGRR HAABBGGRR
goog.provide('shaka.text.SsaTextParser');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.text.Cue');
goog.require('shaka.text.TextEngine');
goog.require('shaka.util.StringUtils');
/**
* Documentation: http://moodub.free.fr/video/ass-specs.doc
* https://en.wikipedia.org/wiki/SubStation_Alpha
* @implements {shaka.extern.TextParser}
* @export
*/
shaka.text.SsaTextParser = class {
/**
* @override
* @export
*/
parseInit(data) {
goog.asserts.assert(false, 'SSA does not have init segments');
}
/**
* @override
* @export
*/
setManifestType(manifestType) {
// Unused.
}
/**
* @override
* @export
*/
parseMedia(data, time) {
const StringUtils = shaka.util.StringUtils;
const SsaTextParser = shaka.text.SsaTextParser;
// Get the input as a string.
const str = StringUtils.fromUTF8(data);
const section = {
styles: '',
events: '',
};
let tag = null;
let lines = null;
const parts = str.split(/\r?\n\s*\r?\n/);
for (const part of parts) {
lines = part;
// SSA content
const match = SsaTextParser.ssaContent_.exec(part);
if (match) {
tag = match[1];
lines = match[2];
}
if (tag == 'V4 Styles' || tag == 'V4+ Styles') {
section.styles = lines;
if (section.events) {
section.styles += '\n' + lines;
} else {
section.styles = lines;
}
continue;
}
if (tag == 'Events') {
if (section.events) {
section.events += '\n' + lines;
} else {
section.events = lines;
}
continue;
}
if (tag == 'Script Info') {
continue;
}
shaka.log.warning('SsaTextParser parser encountered an unknown part.',
lines);
}
// Process styles
const styles = [];
// Used to be able to iterate over the style parameters.
let styleColumns = null;
const styleLines = section.styles.split(/\r?\n/);
for (const line of styleLines) {
if (/^\s*;/.test(line)) {
// Skip comment
continue;
}
const lineParts = SsaTextParser.lineParts_.exec(line);
if (lineParts) {
const name = lineParts[1].trim();
const value = lineParts[2].trim();
if (name == 'Format') {
styleColumns = value.split(SsaTextParser.valuesFormat_);
continue;
}
if (name == 'Style') {
const values = value.split(SsaTextParser.valuesFormat_);
const style = {};
for (let c = 0; c < styleColumns.length && c < values.length; c++) {
style[styleColumns[c]] = values[c];
}
styles.push(style);
continue;
}
}
}
// Process cues
/** @type {!Array<!shaka.text.Cue>} */
const cues = [];
// Used to be able to iterate over the event parameters.
let eventColumns = null;
const eventLines = section.events.split(/\r?\n/);
for (const line of eventLines) {
if (/^\s*;/.test(line)) {
// Skip comment
continue;
}
const lineParts = SsaTextParser.lineParts_.exec(line);
if (lineParts) {
const name = lineParts[1].trim();
const value = lineParts[2].trim();
if (name == 'Format') {
eventColumns = value.split(SsaTextParser.valuesFormat_);
continue;
}
if (name == 'Dialogue') {
const values = value.split(SsaTextParser.valuesFormat_);
const data = {};
for (let c = 0; c < eventColumns.length && c < values.length; c++) {
data[eventColumns[c]] = values[c];
}
const startTime = SsaTextParser.parseTime_(data['Start']);
const endTime = SsaTextParser.parseTime_(data['End']);
// Note: Normally, you should take the "Text" field, but if it
// has a comma, it fails.
const payload = values.slice(eventColumns.length - 1).join(',')
.replace(/\\N/g, '\n') // '\n' for new line
.replace(/\{[^}]+\}/g, ''); // {\pos(400,570)}
const cue = new shaka.text.Cue(startTime, endTime, payload);
const styleName = data['Style'];
const styleData = styles.find((s) => s['Name'] == styleName);
if (styleData) {
SsaTextParser.addStyle_(cue, styleData);
}
cues.push(cue);
continue;
}
}
}
return cues;
}
/**
* Adds applicable style properties to a cue.
*
* @param {shaka.text.Cue} cue
* @param {Object} style
* @private
*/
static addStyle_(cue, style) {
const Cue = shaka.text.Cue;
const SsaTextParser = shaka.text.SsaTextParser;
const fontFamily = style['Fontname'];
if (fontFamily) {
cue.fontFamily = fontFamily;
}
const fontSize = style['Fontsize'];
if (fontSize) {
cue.fontSize = fontSize + 'px';
}
const color = style['PrimaryColour'];
if (color) {
const ccsColor = SsaTextParser.parseSsaColor_(color);
if (ccsColor) {
cue.color = ccsColor;
}
}
const backgroundColor = style['BackColour'];
if (backgroundColor) {
const cssBackgroundColor = SsaTextParser.parseSsaColor_(backgroundColor);
if (cssBackgroundColor) {
cue.backgroundColor = cssBackgroundColor;
}
}
const bold = style['Bold'];
if (bold) {
cue.fontWeight = Cue.fontWeight.BOLD;
}
const italic = style['Italic'];
if (italic) {
cue.fontStyle = Cue.fontStyle.ITALIC;
}
const underline = style['Underline'];
if (underline) {
cue.textDecoration.push(Cue.textDecoration.UNDERLINE);
}
const letterSpacing = style['Spacing'];
if (letterSpacing) {
cue.letterSpacing = letterSpacing + 'px';
}
const alignment = style['Alignment'];
if (alignment) {
const alignmentInt = parseInt(alignment, 10);
switch (alignmentInt) {
case 1:
cue.displayAlign = Cue.displayAlign.AFTER;
cue.textAlign = Cue.textAlign.START;
break;
case 2:
cue.displayAlign = Cue.displayAlign.AFTER;
cue.textAlign = Cue.textAlign.CENTER;
break;
case 3:
cue.displayAlign = Cue.displayAlign.AFTER;
cue.textAlign = Cue.textAlign.END;
break;
case 5:
cue.displayAlign = Cue.displayAlign.BEFORE;
cue.textAlign = Cue.textAlign.START;
break;
case 6:
cue.displayAlign = Cue.displayAlign.BEFORE;
cue.textAlign = Cue.textAlign.CENTER;
break;
case 7:
cue.displayAlign = Cue.displayAlign.BEFORE;
cue.textAlign = Cue.textAlign.END;
break;
case 9:
cue.displayAlign = Cue.displayAlign.CENTER;
cue.textAlign = Cue.textAlign.START;
break;
case 10:
cue.displayAlign = Cue.displayAlign.CENTER;
cue.textAlign = Cue.textAlign.CENTER;
break;
case 11:
cue.displayAlign = Cue.displayAlign.CENTER;
cue.textAlign = Cue.textAlign.END;
break;
}
}
const opacity = style['AlphaLevel'];
if (opacity) {
cue.opacity = parseFloat(opacity);
}
}
/**
* Parses a SSA color .
*
* @param {string} colorString
* @return {?string}
* @private
*/
static parseSsaColor_(colorString) {
// The SSA V4+ color can be represented in hex (&HAABBGGRR) or in decimal
// format (byte order AABBGGRR) and in both cases the alpha channel's
// value needs to be inverted as in case of SSA the 0xFF alpha value means
// transparent and 0x00 means opaque
/** @type {number} */
const abgr = parseInt(colorString.replace('&H', ''), 16);
if (abgr >= 0) {
const a = ((abgr >> 24) & 0xFF) ^ 0xFF; // Flip alpha.
const alpha = a / 255;
const b = (abgr >> 16) & 0xFF;
const g = (abgr >> 8) & 0xFF;
const r = abgr & 0xff;
return 'rgba(' + r + ',' + g + ',' + b + ',' + alpha + ')';
}
return null;
}
/**
* Parses a SSA time from the given parser.
*
* @param {string} string
* @return {number}
* @private
*/
static parseTime_(string) {
const SsaTextParser = shaka.text.SsaTextParser;
const match = SsaTextParser.timeFormat_.exec(string);
const hours = match[1] ? parseInt(match[1].replace(':', ''), 10) : 0;
const minutes = parseInt(match[2], 10);
const seconds = parseFloat(match[3]);
return hours * 3600 + minutes * 60 + seconds;
}
};
/**
* @const
* @private {!RegExp}
* @example [V4 Styles]\nFormat: Name\nStyle: DefaultVCD
*/
shaka.text.SsaTextParser.ssaContent_ =
/^\s*\[([^\]]+)\]\r?\n([\s\S]*)/;
/**
* @const
* @private {!RegExp}
* @example Style: DefaultVCD,...
*/
shaka.text.SsaTextParser.lineParts_ =
/^\s*([^:]+):\s*(.*)/;
/**
* @const
* @private {!RegExp}
* @example Style: DefaultVCD,...
*/
shaka.text.SsaTextParser.valuesFormat_ = /\s*,\s*/;
/**
* @const
* @private {!RegExp}
* @example 0:00:01.1 or 0:00:01.18 or 0:00:01.180
*/
shaka.text.SsaTextParser.timeFormat_ =
/^(\d+:)?(\d{1,2}):(\d{1,2}(?:[.]\d{1,3})?)?$/;
shaka.text.TextEngine.registerParser(
'text/x-ssa', () => new shaka.text.SsaTextParser());
-3
View File
@@ -61,14 +61,11 @@ goog.require('shaka.polyfill.VideoPlayPromise');
goog.require('shaka.polyfill.VideoPlaybackQuality');
goog.require('shaka.polyfill');
goog.require('shaka.text.Cue');
goog.require('shaka.text.LrcTextParser');
goog.require('shaka.text.Mp4TtmlParser');
goog.require('shaka.text.Mp4VttParser');
goog.require('shaka.text.TextEngine');
goog.require('shaka.text.SbvTextParser');
goog.require('shaka.text.SpeechToText');
goog.require('shaka.text.SrtTextParser');
goog.require('shaka.text.SsaTextParser');
goog.require('shaka.text.TtmlTextParser');
goog.require('shaka.text.VttTextParser');
goog.require('shaka.text.WebVttGenerator');
-98
View File
@@ -1,98 +0,0 @@
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
describe('LrcTextParser', () => {
it('supports no cues', () => {
verifyHelper([],
'',
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
});
it('handles a blank line at the start of the file', () => {
verifyHelper(
[
{startTime: 0, endTime: 2, payload: 'Test'},
],
'\n\n' +
'[00:00.00]Test',
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
});
it('handles a blank line at the end of the file', () => {
verifyHelper(
[
{startTime: 0, endTime: 2, payload: 'Test'},
],
'[00:00.00]Test' +
'\n\n',
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
});
it('handles no blank line at the end of the file', () => {
verifyHelper(
[
{startTime: 0, endTime: 2, payload: 'Test'},
],
'[00:00.00]Test',
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
});
it('supports multiple cues', () => {
verifyHelper(
[
{startTime: 0, endTime: 10, payload: 'Test'},
{startTime: 10, endTime: 20, payload: 'Test2'},
{startTime: 20, endTime: 22, payload: 'Test3'},
],
'[00:00.00]Test\n' +
'[00:10.00]Test2\n' +
'[00:20.00]Test3',
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
});
it('supports different time formats', () => {
verifyHelper(
[
{startTime: 0.1, endTime: 10.001, payload: 'Test'},
{startTime: 10.001, endTime: 20.02, payload: 'Test2'},
{startTime: 20.02, endTime: 30.1, payload: 'Test3'},
{startTime: 30.1, endTime: 40.001, payload: 'Test4'},
{startTime: 40.001, endTime: 50.02, payload: 'Test5'},
{startTime: 50.02, endTime: 52.02, payload: 'Test6'},
],
'[00:00.1]Test\n' +
'[00:10.001]Test2\n' +
'[00:20.02]Test3\n' +
'[00:30,1]Test4\n' +
'[00:40,001]Test5\n' +
'[00:50,02]Test6',
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
});
/**
* @param {!Array} cues
* @param {string} text
* @param {shaka.extern.TextParser.TimeContext} time
*/
function verifyHelper(cues, text, time) {
const BufferUtils = shaka.util.BufferUtils;
const StringUtils = shaka.util.StringUtils;
const data = BufferUtils.toUint8(StringUtils.toUTF8(text));
const parser = new shaka.text.LrcTextParser();
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);
}
});
-81
View File
@@ -1,81 +0,0 @@
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
describe('SbvTextParser', () => {
it('supports no cues', () => {
verifyHelper([],
'',
{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'},
],
'0:00:20.000,0: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'},
],
'0:00:20.000,0: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'},
],
'0:00:20.000,0:00:40.000\n' +
'Test',
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
});
it('supports multiple cues', () => {
verifyHelper(
[
{startTime: 20, endTime: 40, payload: 'Test'},
{startTime: 40, endTime: 50, payload: 'Test2'},
],
'0:00:20.000,0:00:40.000\n' +
'Test\n\n' +
'0:00:40.000,0:00:50.000\n' +
'Test2',
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
});
/**
* @param {!Array} cues
* @param {string} text
* @param {shaka.extern.TextParser.TimeContext} time
*/
function verifyHelper(cues, text, time) {
const BufferUtils = shaka.util.BufferUtils;
const StringUtils = shaka.util.StringUtils;
const data = BufferUtils.toUint8(StringUtils.toUTF8(text));
const parser = new shaka.text.SbvTextParser();
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);
}
});
-384
View File
@@ -1,384 +0,0 @@
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
// cspell:ignore FCFC
describe('SsaTextParser', () => {
const Cue = shaka.text.Cue;
it('supports no cues', () => {
verifyHelper([],
'',
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
});
it('handles a blank line at the start of the file', () => {
verifyHelper(
[
{startTime: 0, endTime: 2, payload: 'Test'},
],
'\n\n' +
'[Script Info]\n' +
'Title: Foo\n\n' +
'[V4+ Styles]\n' +
'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, ' +
'OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ' +
'ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, ' +
'Alignment, MarginL, MarginR, MarginV, Encoding\n' +
'Style: DefaultVCD, Arial,28,&H00B4FCFC,&H00B4FCFC,&H00000008,' +
'&H80000008,-1,0,0,0,100,100,0.00,0.00,1,1.00,2.00,2,30,30,30,0\n\n' +
'[Events]\n' +
'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, ' +
'Effect, Text\n' +
'Dialogue: 0,0:00:00.00,0:00:02.00,DefaultVCD, NTP,0000,0000,0000' +
',,{\\pos(400,570)}Test',
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
});
it('handles a blank line at the end of the file', () => {
verifyHelper(
[
{startTime: 0, endTime: 2, payload: 'Test'},
],
'[Script Info]\n' +
'Title: Foo\n\n' +
'[V4+ Styles]\n' +
'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, ' +
'OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ' +
'ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, ' +
'Alignment, MarginL, MarginR, MarginV, Encoding\n' +
'Style: DefaultVCD, Arial,28,&H00B4FCFC,&H00B4FCFC,&H00000008,' +
'&H80000008,-1,0,0,0,100,100,0.00,0.00,1,1.00,2.00,2,30,30,30,0\n\n' +
'[Events]\n' +
'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, ' +
'Effect, Text\n' +
'Dialogue: 0,0:00:00.00,0:00:02.00,DefaultVCD, NTP,0000,0000,0000' +
',,{\\pos(400,570)}Test' +
'\n\n',
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
});
it('handles no blank line at the end of the file', () => {
verifyHelper(
[
{startTime: 0, endTime: 2, payload: 'Test'},
],
'[Script Info]\n' +
'Title: Foo\n\n' +
'[V4+ Styles]\n' +
'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, ' +
'OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ' +
'ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, ' +
'Alignment, MarginL, MarginR, MarginV, Encoding\n' +
'Style: DefaultVCD, Arial,28,&H00B4FCFC,&H00B4FCFC,&H00000008,' +
'&H80000008,-1,0,0,0,100,100,0.00,0.00,1,1.00,2.00,2,30,30,30,0\n\n' +
'[Events]\n' +
'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, ' +
'Effect, Text\n' +
'Dialogue: 0,0:00:00.00,0:00:02.00,DefaultVCD, NTP,0000,0000,0000' +
',,{\\pos(400,570)}Test',
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
});
it('supports no styles', () => {
verifyHelper(
[
{startTime: 0, endTime: 2, payload: 'Test'},
],
'[Script Info]\n' +
'Title: Foo\n\n' +
'[Events]\n' +
'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, ' +
'Effect, Text\n' +
'Dialogue: 0,0:00:00.00,0:00:02.00,DefaultVCD, NTP,0000,0000,0000' +
',,{\\pos(400,570)}Test',
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
});
it('support no script info', () => {
verifyHelper(
[
{startTime: 0, endTime: 2, payload: 'Test'},
],
'[V4+ Styles]\n' +
'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, ' +
'OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ' +
'ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, ' +
'Alignment, MarginL, MarginR, MarginV, Encoding\n' +
'Style: DefaultVCD, Arial,28,&H00B4FCFC,&H00B4FCFC,&H00000008,' +
'&H80000008,-1,0,0,0,100,100,0.00,0.00,1,1.00,2.00,2,30,30,30,0\n\n' +
'[Events]\n' +
'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, ' +
'Effect, Text\n' +
'Dialogue: 0,0:00:00.00,0:00:02.00,DefaultVCD, NTP,0000,0000,0000' +
',,{\\pos(400,570)}Test',
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
});
it('supports only events', () => {
verifyHelper(
[
{startTime: 0, endTime: 2, payload: 'Test'},
],
'[Events]\n' +
'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, ' +
'Effect, Text\n' +
'Dialogue: 0,0:00:00.00,0:00:02.00,DefaultVCD, NTP,0000,0000,0000' +
',,{\\pos(400,570)}Test',
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
});
it('supports text with commas', () => {
verifyHelper(
[
{startTime: 0, endTime: 2, payload: 'Test,1,Test2'},
],
'[Events]\n' +
'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, ' +
'Effect, Text\n' +
'Dialogue: 0,0:00:00.00,0:00:02.00,DefaultVCD, NTP,0000,0000,0000' +
',,{\\pos(400,570)}Test,1,Test2',
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
});
it('supports different time formats', () => {
verifyHelper(
[
{startTime: 0, endTime: 2, payload: 'Test'},
{startTime: 4.5, endTime: 6.1, payload: 'Test2'},
{startTime: 8.01, endTime: 10.001, payload: 'Test3'},
],
'[Events]\n' +
'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, ' +
'Effect, Text\n' +
'Dialogue: 0,0:00:00.00,0:00:02.00,DefaultVCD, NTP,0000,0000,0000' +
',,{\\pos(400,570)}Test\n' +
'Dialogue: 0,0:00:04.5,0:00:06.1,DefaultVCD, NTP,0000,0000,0000' +
',,{\\pos(400,570)}Test2\n' +
'Dialogue: 0,0:00:08.01,0:00:10.001,DefaultVCD, NTP,0000,0000,0000' +
',,{\\pos(400,570)}Test3',
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
});
it('supports multiple cues', () => {
verifyHelper(
[
{startTime: 0, endTime: 2, payload: 'Test'},
{startTime: 4.5, endTime: 6.1, payload: 'Test2'},
{startTime: 8.01, endTime: 10.1, payload: 'Test3'},
],
'[Events]\n' +
'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, ' +
'Effect, Text\n' +
'Dialogue: 0,0:00:00.00,0:00:02.00,DefaultVCD, NTP,0000,0000,0000' +
',,{\\pos(400,570)}Test\n\n' +
'Dialogue: 0,0:00:04.50,0:00:06.10,DefaultVCD, NTP,0000,0000,0000' +
',,{\\pos(400,570)}Test2\n\n' +
'Dialogue: 0,0:00:08.01,0:00:10.10,DefaultVCD, NTP,0000,0000,0000' +
',,{\\pos(400,570)}Test3',
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
});
it('supports fontFamily style', () => {
verifyHelper(
[
{
startTime: 0,
endTime: 2,
payload: 'Test',
fontFamily: 'Arial',
},
],
'[V4+ Styles]\n' +
'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, ' +
'OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ' +
'ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, ' +
'Alignment, MarginL, MarginR, MarginV, Encoding\n' +
'Style: DefaultVCD, Arial,28,&H00B4FCFC,&H00B4FCFC,&H00000008,' +
'&H80000008,-1,0,0,0,100,100,0.00,0.00,1,1.00,2.00,2,30,30,30,0\n\n' +
'[Events]\n' +
'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, ' +
'Effect, Text\n' +
'Dialogue: 0,0:00:00.00,0:00:02.00,DefaultVCD, NTP,0000,0000,0000' +
',,{\\pos(400,570)}Test',
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
});
it('supports color & backgroundColor style', () => {
verifyHelper(
[
{
startTime: 0,
endTime: 2,
payload: 'Test',
color: 'rgba(252,252,180,1)',
backgroundColor: 'rgba(8,0,0,0)',
},
],
'[V4+ Styles]\n' +
'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, ' +
'OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ' +
'ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, ' +
'Alignment, MarginL, MarginR, MarginV, Encoding\n' +
'Style: DefaultVCD, Arial,28,&H00B4FCFC,&H00B4FCFC,&H00000008,' +
'&HFF000008,-1,0,0,0,100,100,0.00,0.00,1,1.00,2.00,2,30,30,30,0\n\n' +
'[Events]\n' +
'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, ' +
'Effect, Text\n' +
'Dialogue: 0,0:00:00.00,0:00:02.00,DefaultVCD, NTP,0000,0000,0000' +
',,{\\pos(400,570)}Test',
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
});
it('supports bold style', () => {
verifyHelper(
[
{
startTime: 0,
endTime: 2,
payload: 'Test',
fontWeight: Cue.fontWeight.BOLD,
},
],
'[V4+ Styles]\n' +
'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, ' +
'OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ' +
'ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, ' +
'Alignment, MarginL, MarginR, MarginV, Encoding\n' +
'Style: DefaultVCD, Arial,28,&H00B4FCFC,&H00B4FCFC,&H00000008,' +
'&H80000008,-1,1,0,0,100,100,0.00,0.00,1,1.00,2.00,2,30,30,30,0\n\n' +
'[Events]\n' +
'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, ' +
'Effect, Text\n' +
'Dialogue: 0,0:00:00.00,0:00:02.00,DefaultVCD, NTP,0000,0000,0000' +
',,{\\pos(400,570)}Test',
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
});
it('supports italic style', () => {
verifyHelper(
[
{
startTime: 0,
endTime: 2,
payload: 'Test',
fontStyle: Cue.fontStyle.ITALIC,
},
],
'[V4+ Styles]\n' +
'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, ' +
'OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ' +
'ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, ' +
'Alignment, MarginL, MarginR, MarginV, Encoding\n' +
'Style: DefaultVCD, Arial,28,&H00B4FCFC,&H00B4FCFC,&H00000008,' +
'&H80000008,-1,0,1,0,100,100,0.00,0.00,1,1.00,2.00,2,30,30,30,0\n\n' +
'[Events]\n' +
'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, ' +
'Effect, Text\n' +
'Dialogue: 0,0:00:00.00,0:00:02.00,DefaultVCD, NTP,0000,0000,0000' +
',,{\\pos(400,570)}Test',
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
});
it('supports underline style', () => {
verifyHelper(
[
{
startTime: 0,
endTime: 2,
payload: 'Test',
textDecoration: [Cue.textDecoration.UNDERLINE],
},
],
'[V4+ Styles]\n' +
'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, ' +
'OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ' +
'ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, ' +
'Alignment, MarginL, MarginR, MarginV, Encoding\n' +
'Style: DefaultVCD, Arial,28,&H00B4FCFC,&H00B4FCFC,&H00000008,' +
'&H80000008,-1,0,0,1,100,100,0.00,0.00,1,1.00,2.00,2,30,30,30,0\n\n' +
'[Events]\n' +
'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, ' +
'Effect, Text\n' +
'Dialogue: 0,0:00:00.00,0:00:02.00,DefaultVCD, NTP,0000,0000,0000' +
',,{\\pos(400,570)}Test',
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
});
it('supports letterSpacing style', () => {
verifyHelper(
[
{
startTime: 0,
endTime: 2,
payload: 'Test',
letterSpacing: '2px',
},
],
'[V4+ Styles]\n' +
'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, ' +
'OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ' +
'ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, ' +
'Alignment, MarginL, MarginR, MarginV, Encoding\n' +
'Style: DefaultVCD, Arial,28,&H00B4FCFC,&H00B4FCFC,&H00000008,' +
'&H80000008,-1,0,0,0,100,100,2,0.00,1,1.00,2.00,2,30,30,30,0\n\n' +
'[Events]\n' +
'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, ' +
'Effect, Text\n' +
'Dialogue: 0,0:00:00.00,0:00:02.00,DefaultVCD, NTP,0000,0000,0000' +
',,{\\pos(400,570)}Test',
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
});
it('supports V4 style', () => {
verifyHelper(
[
{
startTime: 0,
endTime: 2,
payload: 'Test',
fontFamily: 'Arial',
},
],
'[V4 Styles]\n' +
'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, ' +
'TertiaryColour, BackColour, Bold, Italic, BorderStyle, Outline, ' +
'Shadow, Alignment, MarginL, MarginR, MarginV, AlphaLevel, '+
'Encoding\n' +
'Style: DefaultVCD, Arial,28,11861244,11861244,11861244,' +
'-2147483640,-1,0,1,1,2,2,30,30,30,0,0\n\n' +
'[Events]\n' +
'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, ' +
'Effect, Text\n' +
'Dialogue: 0,0:00:00.00,0:00:02.00,DefaultVCD, NTP,0000,0000,0000' +
',,{\\pos(400,570)}Test',
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
});
/**
* @param {!Array} cues
* @param {string} text
* @param {shaka.extern.TextParser.TimeContext} time
*/
function verifyHelper(cues, text, time) {
const BufferUtils = shaka.util.BufferUtils;
const StringUtils = shaka.util.StringUtils;
const data = BufferUtils.toUint8(StringUtils.toUTF8(text));
const parser = new shaka.text.SsaTextParser();
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);
}
});