Skip to content

Commit

Permalink
fix(WebVTT): Fix voices with styles and support to multiple styles (#…
Browse files Browse the repository at this point in the history
…4922)

Backported to v3.3.x, also includes #4257
  • Loading branch information
Álvaro Velad Galván authored and joeyparrish committed Jan 30, 2023
1 parent ff36a0a commit 8624759
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 80 deletions.
7 changes: 7 additions & 0 deletions externs/shaka/text.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,13 @@ shaka.extern.Cue = class {
*/
this.fontFamily;

/**
* Text shadow color as a CSS text-shadow value.
* @type {string}
* @exportDoc
*/
this.textShadow = '';

/**
* Text letter spacing as a CSS letter-spacing value.
* @type {string}
Expand Down
6 changes: 6 additions & 0 deletions lib/text/cue.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@ shaka.text.Cue = class {
*/
this.border = '';

/**
* @override
* @exportInterface
*/
this.textShadow = '';

/**
* @override
* @exportInterface
Expand Down
1 change: 1 addition & 0 deletions lib/text/ui_text_displayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,7 @@ shaka.text.UITextDisplayer = class {
style.paddingRight =
shaka.text.UITextDisplayer.convertLengthValue_(
cue.linePadding, cue, this.videoContainer_);
style.textShadow = cue.textShadow;

if (cue.backgroundImage) {
style.backgroundImage = 'url(\'' + cue.backgroundImage + '\')';
Expand Down
180 changes: 103 additions & 77 deletions lib/text/vtt_text_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,92 +199,113 @@ shaka.text.VttTextParser = class {
return;
}

if (!text[1].includes('::cue')) {
return;
}
let styleSelector = 'global';
// Look for what is within parentisesis. For example:
// <code>:: cue (b) {</code>, what we are looking for is <code>b</code>
const selector = text[1].match(/\((.*)\)/);
if (selector) {
styleSelector = selector.pop();
/** @type {!Array.<!Array.<string>>} */
const styleBlocks = [];
let lastBlockIndex = -1;
for (let i = 1; i < text.length; i++) {
if (text[i].includes('::cue')) {
styleBlocks.push([]);
lastBlockIndex = styleBlocks.length - 1;
}
if (lastBlockIndex == -1) {
continue;
}
styleBlocks[lastBlockIndex].push(text[i]);
if (text[i].includes('}')) {
lastBlockIndex = -1;
}
}

// We start at 2 to avoid '::cue' and end earlier to avoid '}'
let propertyLines = text.slice(2, -1);
if (text[1].includes('}')) {
const payload = /\{(.*?)\}/.exec(text[1]);
if (payload) {
propertyLines = payload[1].split(';');
for (const styleBlock of styleBlocks) {
let styleSelector = 'global';
// Look for what is within parentheses. For example:
// <code>:: cue (b) {</code>, what we are looking for is <code>b</code>
const selector = styleBlock[0].match(/\((.*)\)/);
if (selector) {
styleSelector = selector.pop();
}
}

const cue = new shaka.text.Cue(0, 0, '');
let validStyle = false;
for (let i = 0; i < propertyLines.length; i++) {
// We look for CSS properties. As a general rule they are separated by
// <code>:</code>. Eg: <code>color: red;</code>
const lineParts = /^\s*([^:]+):\s*(.*)/.exec(propertyLines[i]);
if (lineParts) {
const name = lineParts[1].trim();
const value = lineParts[2].trim().replace(';', '');
switch (name) {
case 'background-color':
validStyle = true;
cue.backgroundColor = value;
break;
case 'color':
validStyle = true;
cue.color = value;
break;
case 'font-family':
validStyle = true;
cue.fontFamily = value;
break;
case 'font-size':
validStyle = true;
cue.fontSize = value;
break;
case 'font-weight':
if (parseInt(value, 10) >= 700) {
// We start at 1 to avoid '::cue' and end earlier to avoid '}'
let propertyLines = styleBlock.slice(1, -1);
if (styleBlock[0].includes('}')) {
const payload = /\{(.*?)\}/.exec(styleBlock[0]);
if (payload) {
propertyLines = payload[1].split(';');
}
}

const cue = new shaka.text.Cue(0, 0, '');
let validStyle = false;
for (let i = 0; i < propertyLines.length; i++) {
// We look for CSS properties. As a general rule they are separated by
// <code>:</code>. Eg: <code>color: red;</code>
const lineParts = /^\s*([^:]+):\s*(.*)/.exec(propertyLines[i]);
if (lineParts) {
const name = lineParts[1].trim();
const value = lineParts[2].trim().replace(';', '');
switch (name) {
case 'background-color':
case 'background':
validStyle = true;
cue.fontWeight = shaka.text.Cue.fontWeight.BOLD;
}
break;
case 'font-style':
switch (value) {
case 'normal':
validStyle = true;
cue.fontStyle = shaka.text.Cue.fontStyle.NORMAL;
break;
case 'italic':
validStyle = true;
cue.fontStyle = shaka.text.Cue.fontStyle.ITALIC;
break;
case 'oblique':
cue.backgroundColor = value;
break;
case 'color':
validStyle = true;
cue.color = value;
break;
case 'font-family':
validStyle = true;
cue.fontFamily = value;
break;
case 'font-size':
validStyle = true;
cue.fontSize = value;
break;
case 'font-weight':
if (parseInt(value, 10) >= 700 || value == 'bold') {
validStyle = true;
cue.fontStyle = shaka.text.Cue.fontStyle.OBLIQUE;
break;
}
break;
case 'opacity':
validStyle = true;
cue.opacity = parseFloat(value);
break;
case 'white-space':
validStyle = true;
cue.wrapLine = value != 'noWrap';
break;
default:
shaka.log.warning('VTT parser encountered an unsupported style: ',
lineParts);
break;
cue.fontWeight = shaka.text.Cue.fontWeight.BOLD;
}
break;
case 'font-style':
switch (value) {
case 'normal':
validStyle = true;
cue.fontStyle = shaka.text.Cue.fontStyle.NORMAL;
break;
case 'italic':
validStyle = true;
cue.fontStyle = shaka.text.Cue.fontStyle.ITALIC;
break;
case 'oblique':
validStyle = true;
cue.fontStyle = shaka.text.Cue.fontStyle.OBLIQUE;
break;
}
break;
case 'opacity':
validStyle = true;
cue.opacity = parseFloat(value);
break;
case 'text-shadow':
validStyle = true;
cue.textShadow = value;
break;
case 'white-space':
validStyle = true;
cue.wrapLine = value != 'noWrap';
break;
default:
shaka.log.warning('VTT parser encountered an unsupported style: ',
lineParts);
break;
}
}
}
}

if (validStyle) {
styles.set(styleSelector, cue);
if (validStyle) {
styles.set(styleSelector, cue);
}
}
}

Expand Down Expand Up @@ -609,6 +630,11 @@ shaka.text.VttTextParser = class {
if (styleTag.startsWith('.voice-')) {
const voice = styleTag.split('-').pop();
styleTag = `v[voice="${voice}"]`;
// The specification allows to have quotes and not, so we check to
// see which one is being used.
if (!styles.has(styleTag)) {
styleTag = `v[voice=${voice}]`;
}
}
if (styles.has(styleTag)) {
VttTextParser.mergeStyle_(nestedCue, styles.get(styleTag));
Expand Down
12 changes: 9 additions & 3 deletions test/text/vtt_text_parser_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,7 @@ describe('VttTextParser', () => {
});

it('supports global style blocks', () => {
const textShadow = '-1px 0 black, 0 1px black, 1px 0 black, 0 -1px black';
verifyHelper(
[
{
Expand All @@ -578,20 +579,23 @@ describe('VttTextParser', () => {
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' +
Expand Down Expand Up @@ -993,7 +997,7 @@ describe('VttTextParser', () => {
startTime: 40,
endTime: 50,
payload: 'Test',
color: 'cyan',
color: 'red',
},
{
startTime: 40,
Expand All @@ -1005,11 +1009,13 @@ describe('VttTextParser', () => {
},
],
'WEBVTT\n\n' +
'STYLE\n::cue(v[voice="Shaka"]) { color: cyan; }\n\n' +
'STYLE\n' +
'::cue(v[voice="Shaka"]) { color: cyan; }\n' +
'::cue(v[voice=ShakaBis]) { color: red; }\n\n' +
'00:00:20.000 --> 00:00:40.000\n' +
'<v Shaka>Test\n\n' +
'00:00:40.000 --> 00:00:50.000\n' +
'<v Shaka>Test</v><i>2</i>',
'<v ShakaBis>Test</v><i>2</i>',
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
});

Expand Down

0 comments on commit 8624759

Please sign in to comment.