From e8465a2e1c1999d1b2bae3cc2819f8a2666bf7c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Velad=20Galv=C3=A1n?= Date: Thu, 19 Jan 2023 11:35:21 +0100 Subject: [PATCH] fix(WebVTT): Add support to voice tag styles (#4845) closes https://github.com/shaka-project/shaka-player/issues/4844 fixes https://github.com/shaka-project/shaka-player/issues/4843 fixes https://github.com/shaka-project/shaka-player/issues/4479 --- lib/text/vtt_text_parser.js | 116 +++++++++++++++++++++++++++--- test/text/vtt_text_parser_unit.js | 100 ++++++++++++++++++++++++-- 2 files changed, 199 insertions(+), 17 deletions(-) diff --git a/lib/text/vtt_text_parser.js b/lib/text/vtt_text_parser.js index 1c2604a4c3..a76cd01d19 100644 --- a/lib/text/vtt_text_parser.js +++ b/lib/text/vtt_text_parser.js @@ -154,14 +154,14 @@ shaka.text.VttTextParser = class { for (const [key, value] of Object.entries(textColor)) { const cue = new shaka.text.Cue(0, 0, ''); cue.color = value; - styles.set(key, cue); + styles.set('.' + key, cue); } const bgColor = shaka.text.Cue.defaultTextBackgroundColor; for (const [key, value] of Object.entries(bgColor)) { const cue = new shaka.text.Cue(0, 0, ''); cue.backgroundColor = value; - styles.set(key, cue); + styles.set('.' + key, cue); } } @@ -413,6 +413,7 @@ shaka.text.VttTextParser = class { } payload = VttTextParser.replaceColorPayload_(payload); payload = VttTextParser.replaceKaraokeStylePayload_(payload); + payload = VttTextParser.replaceVoiceStylePayload_(payload); const xmlPayload = '' + payload + ''; const element = shaka.util.XmlUtils.parseXmlString(xmlPayload, 'span'); if (element) { @@ -438,6 +439,79 @@ shaka.text.VttTextParser = class { } } + /** + * Converts voice style tag to be valid for xml parsing + * For example, + * input: Test + * output: Test + * + * @param {string} payload + * @return {string} processed payload + * @private + */ + static replaceVoiceStylePayload_(payload) { + const voiceTag = 'v'; + const names = []; + let nameStart = -1; + let newPayload = ''; + let hasVoiceEndTag = false; + for (let i = 0; i < payload.length; i++) { + // This condition is used to manage tags that have end tags. + if (payload[i] === '/') { + const end = payload.indexOf('>', i); + if (end === -1) { + return payload; + } + const tagEnd = payload.substring(i + 1, end); + if (!tagEnd || tagEnd != voiceTag) { + newPayload += payload[i]; + continue; + } + hasVoiceEndTag = true; + let tagStart = null; + if (names.length) { + tagStart = names[names.length -1]; + } + if (!tagStart) { + newPayload += payload[i]; + } else if (tagStart === tagEnd) { + newPayload += '/' + tagEnd + '>'; + i += tagEnd.length + 1; + } else { + if (!tagStart.startsWith(voiceTag)) { + newPayload += payload[i]; + continue; + } + newPayload += '/' + tagStart + '>'; + i += tagEnd.length + 1; + } + } else { + // Here we only want the tag name, not any other payload. + if (payload[i] === '<') { + nameStart = i + 1; + if (payload[nameStart] != voiceTag) { + nameStart = -1; + } + } else if (payload[i] === '>') { + if (nameStart > 0) { + names.push(payload.substr(nameStart, i - nameStart)); + nameStart = -1; + } + } + newPayload += payload[i]; + } + } + for (const name of names) { + const newName = name.replace(' ', '.voice-'); + newPayload = newPayload.replace(`<${name}>`, `<${newName}>`); + newPayload = newPayload.replace(``, ``); + if (!hasVoiceEndTag) { + newPayload += ``; + } + } + return newPayload; + } + /** * Converts karaoke style tag to be valid for xml parsing * For example, @@ -494,28 +568,36 @@ shaka.text.VttTextParser = class { let nameStart = -1; let newPayload = ''; for (let i = 0; i < payload.length; i++) { - if (payload[i] === '/') { + if (payload[i] === '/' && i > 0 && payload[i - 1] === '<') { const end = payload.indexOf('>', i); if (end <= i) { return payload; } const tagEnd = payload.substring(i + 1, end); + if (!tagEnd || tagEnd !== 'c') { + newPayload += payload[i]; + continue; + } const tagStart = names.pop(); - if (!tagEnd || !tagStart) { - return payload; + if (!tagStart) { + newPayload += payload[i]; } else if (tagStart === tagEnd) { newPayload += '/' + tagEnd + '>'; i += tagEnd.length + 1; } else { - if (!tagStart.startsWith('c.') || tagEnd !== 'c') { - return payload; + if (!tagStart.startsWith('c.')) { + newPayload += payload[i]; + continue; } - newPayload += '/' + tagStart + '>'; i += tagEnd.length + 1; + newPayload += '/' + tagStart + '>'; } } else { if (payload[i] === '<') { nameStart = i + 1; + if (payload[nameStart] != 'c') { + nameStart = -1; + } } else if (payload[i] === '>') { if (nameStart > 0) { names.push(payload.substr(nameStart, i - nameStart)); @@ -584,12 +666,24 @@ shaka.text.VttTextParser = class { const bold = shaka.text.Cue.fontWeight.BOLD; const italic = shaka.text.Cue.fontStyle.ITALIC; const underline = shaka.text.Cue.textDecoration.UNDERLINE; - const tags = element.nodeName.split(/[ .]+/); + const tags = element.nodeName.split(/(?=[ .])+/g); for (const tag of tags) { - if (styles.has(tag)) { - VttTextParser.mergeStyle_(nestedCue, styles.get(tag)); + let styleTag = tag; + // White blanks at start indicate that the style is a voice + if (styleTag.startsWith('.voice-')) { + const voice = styleTag.split('-').pop(); + styleTag = `v[voice="${voice}"]`; + } + if (styles.has(styleTag)) { + VttTextParser.mergeStyle_(nestedCue, styles.get(styleTag)); } switch (tag) { + case 'br': { + const lineBreakCue = rootCue.clone(); + lineBreakCue.lineBreak = true; + cues.push(lineBreakCue); + break; + } case 'b': nestedCue.fontWeight = bold; break; diff --git a/test/text/vtt_text_parser_unit.js b/test/text/vtt_text_parser_unit.js index 7f7d052bbc..e8229e69b2 100644 --- a/test/text/vtt_text_parser_unit.js +++ b/test/text/vtt_text_parser_unit.js @@ -986,25 +986,32 @@ describe('VttTextParser', () => { { startTime: 80, endTime: 90, - payload: 'Parse fail 1', + payload: 'Parse fail 1', nestedCues: [], }, { startTime: 90, endTime: 100, - payload: 'Parse fail 2', + payload: 'Parse fail 2', nestedCues: [], }, { startTime: 100, endTime: 110, - payload: 'forward slash 1/2 in text', - nestedCues: [], + payload: '', + nestedCues: [ + { + startTime: 100, + endTime: 110, + payload: 'forward slash 1/2 in text', + color: '#0F0', + }, + ], }, { startTime: 110, endTime: 120, - payload: 'less or more < > in text', + payload: 'less or more < > in text', nestedCues: [], }, ], @@ -1056,6 +1063,51 @@ describe('VttTextParser', () => { {periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0}); }); + it('supports voice style blocks', () => { + verifyHelper( + [ + { + startTime: 20, + endTime: 40, + payload: '', + nestedCues: [ + { + startTime: 20, + endTime: 40, + payload: 'Test', + color: 'cyan', + }, + ], + }, + { + startTime: 40, + endTime: 50, + payload: '', + nestedCues: [ + { + startTime: 40, + endTime: 50, + payload: 'Test', + color: 'cyan', + }, + { + startTime: 40, + endTime: 50, + payload: '2', + fontStyle: Cue.fontStyle.ITALIC, + }, + ], + }, + ], + 'WEBVTT\n\n' + + 'STYLE\n::cue(v[voice="Shaka"]) { color: cyan; }\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 default color overriding', () => { verifyHelper( [ @@ -1076,12 +1128,48 @@ describe('VttTextParser', () => { ], 'WEBVTT\n\n' + 'STYLE\n' + - '::cue(bg_blue) { font-size: 10px; background-color: #FF0 }\n\n' + + '::cue(.bg_blue) { font-size: 10px; background-color: #FF0 }\n\n' + '00:00:10.000 --> 00:00:20.000\n' + 'Example 1\n\n', {periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0}); }); + // https://github.com/shaka-project/shaka-player/issues/4479 + it('keep styles when there are line breaks', () => { + verifyHelper( + [ + { + startTime: 10, endTime: 20, + payload: '', + nestedCues: [ + { + startTime: 10, + endTime: 20, + payload: '1', + color: '#F0F', + }, + { + startTime: 10, + endTime: 20, + payload: '', + lineBreak: true, + }, + { + startTime: 10, + endTime: 20, + payload: '2', + color: '#F0F', + fontStyle: Cue.fontStyle.ITALIC, + }, + ], + }, + ], + 'WEBVTT\n\n' + + '00:00:10.000 --> 00:00:20.000\n' + + '1
2\n\n', + {periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0}); + }); + /** * @param {!Array} cues * @param {string} text