From 68968c17d8ad1eaca6afa6d86bb4f8b1baa69d10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Velad=20Galv=C3=A1n?= Date: Tue, 24 Jan 2023 19:53:48 +0100 Subject: [PATCH] fix(WebVTT): Fix voices with styles and support to multiple styles (#4922) --- lib/text/vtt_text_parser.js | 184 +++++++++++++++++------------- test/text/vtt_text_parser_unit.js | 8 +- 2 files changed, 108 insertions(+), 84 deletions(-) diff --git a/lib/text/vtt_text_parser.js b/lib/text/vtt_text_parser.js index 6d8e2cbf25..c13cd9e56a 100644 --- a/lib/text/vtt_text_parser.js +++ b/lib/text/vtt_text_parser.js @@ -221,96 +221,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 '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; + 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); + } } } @@ -674,6 +691,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 6c02656ab5..836590be17 100644 --- a/test/text/vtt_text_parser_unit.js +++ b/test/text/vtt_text_parser_unit.js @@ -1099,7 +1099,7 @@ describe('VttTextParser', () => { startTime: 40, endTime: 50, payload: 'Test', - color: 'cyan', + color: 'red', }, { startTime: 40, @@ -1111,11 +1111,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}); });