From 86247596f217bc4faa0a834d4f487044a8a97afb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Velad=20Galv=C3=A1n?= Date: Fri, 27 May 2022 07:52:55 +0200 Subject: [PATCH] fix(WebVTT): Fix voices with styles and support to multiple styles (#4922) Backported to v3.3.x, also includes #4257 --- externs/shaka/text.js | 7 ++ lib/text/cue.js | 6 + lib/text/ui_text_displayer.js | 1 + lib/text/vtt_text_parser.js | 180 +++++++++++++++++------------- test/text/vtt_text_parser_unit.js | 12 +- 5 files changed, 126 insertions(+), 80 deletions(-) diff --git a/externs/shaka/text.js b/externs/shaka/text.js index e9e64f15df..6b93330ed5 100644 --- a/externs/shaka/text.js +++ b/externs/shaka/text.js @@ -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} diff --git a/lib/text/cue.js b/lib/text/cue.js index e1f6c5b344..1dec8f22d9 100644 --- a/lib/text/cue.js +++ b/lib/text/cue.js @@ -139,6 +139,12 @@ shaka.text.Cue = class { */ this.border = ''; + /** + * @override + * @exportInterface + */ + this.textShadow = ''; + /** * @override * @exportInterface diff --git a/lib/text/ui_text_displayer.js b/lib/text/ui_text_displayer.js index d6a6cd3525..9ae660b57d 100644 --- a/lib/text/ui_text_displayer.js +++ b/lib/text/ui_text_displayer.js @@ -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 + '\')'; diff --git a/lib/text/vtt_text_parser.js b/lib/text/vtt_text_parser.js index d634ba9fac..83a7ca2cb3 100644 --- a/lib/text/vtt_text_parser.js +++ b/lib/text/vtt_text_parser.js @@ -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: - // :: cue (b) {, what we are looking for is b - const selector = text[1].match(/\((.*)\)/); - if (selector) { - styleSelector = selector.pop(); + /** @type {!Array.>} */ + 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: + // :: cue (b) {, what we are looking for is b + 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 - // :. Eg: color: red; - 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 + // :. Eg: color: red; + 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); + } } } @@ -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)); diff --git a/test/text/vtt_text_parser_unit.js b/test/text/vtt_text_parser_unit.js index 87d746ccf1..2c27ccb7a5 100644 --- a/test/text/vtt_text_parser_unit.js +++ b/test/text/vtt_text_parser_unit.js @@ -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( [ { @@ -578,6 +579,7 @@ describe('VttTextParser', () => { payload: 'Test', color: 'cyan', fontSize: '10px', + textShadow: textShadow, }, { startTime: 40, @@ -585,6 +587,7 @@ describe('VttTextParser', () => { payload: 'Test2', color: 'cyan', fontSize: '10px', + textShadow: textShadow, }, ], 'WEBVTT\n\n' + @@ -592,6 +595,7 @@ describe('VttTextParser', () => { '::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' + @@ -993,7 +997,7 @@ describe('VttTextParser', () => { startTime: 40, endTime: 50, payload: 'Test', - color: 'cyan', + color: 'red', }, { startTime: 40, @@ -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' + 'Test\n\n' + '00:00:40.000 --> 00:00:50.000\n' + - 'Test2', + 'Test2', {periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0}); });