Skip to content

Commit

Permalink
feat(WebVTT): Add support to voice tag styles (shaka-project#4845)
Browse files Browse the repository at this point in the history
  • Loading branch information
avelad committed Jan 19, 2023
1 parent a19c658 commit a5f8b43
Show file tree
Hide file tree
Showing 2 changed files with 199 additions and 17 deletions.
116 changes: 105 additions & 11 deletions lib/text/vtt_text_parser.js
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -414,6 +414,7 @@ shaka.text.VttTextParser = class {
}
payload = VttTextParser.replaceColorPayload_(payload);
payload = VttTextParser.replaceKaraokeStylePayload_(payload);
payload = VttTextParser.replaceVoiceStylePayload_(payload);
const xmlPayload = '<span>' + payload + '</span>';
const element = shaka.util.XmlUtils.parseXmlString(xmlPayload, 'span');
if (element) {
Expand All @@ -439,6 +440,79 @@ shaka.text.VttTextParser = class {
}
}

/**
* Converts voice style tag to be valid for xml parsing
* For example,
* input: <v Shaka>Test
* output: <v.voice-Shaka>Test</v.voice-Shaka>
*
* @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(`</${name}>`, `</${newName}>`);
if (!hasVoiceEndTag) {
newPayload += `</${newName}>`;
}
}
return newPayload;
}

/**
* Converts karaoke style tag to be valid for xml parsing
* For example,
Expand Down Expand Up @@ -495,28 +569,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));
Expand Down Expand Up @@ -585,12 +667,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;
Expand Down
100 changes: 94 additions & 6 deletions test/text/vtt_text_parser_unit.js
Expand Up @@ -997,25 +997,32 @@ describe('VttTextParser', () => {
{
startTime: 80,
endTime: 90,
payload: '<b><c.lime>Parse fail 1</b></c>',
payload: '<b><c.lime>Parse fail 1</b></c.lime>',
nestedCues: [],
},
{
startTime: 90,
endTime: 100,
payload: '<c.lime><b>Parse fail 2</c></b>',
payload: '<c.lime><b>Parse fail 2</c.lime></b>',
nestedCues: [],
},
{
startTime: 100,
endTime: 110,
payload: '<c.lime>forward slash 1/2 in text</c>',
nestedCues: [],
payload: '',
nestedCues: [
{
startTime: 100,
endTime: 110,
payload: 'forward slash 1/2 in text',
color: '#0F0',
},
],
},
{
startTime: 110,
endTime: 120,
payload: '<c.lime>less or more < > in text</c>',
payload: '<c.lime>less or more < > in text</c.lime>',
nestedCues: [],
},
],
Expand Down Expand Up @@ -1067,6 +1074,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' +
'<v Shaka>Test\n\n' +
'00:00:40.000 --> 00:00:50.000\n' +
'<v Shaka>Test</v><i>2</i>',
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
});

it('supports default color overriding', () => {
verifyHelper(
[
Expand All @@ -1087,12 +1139,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' +
'<c.red.bg_blue>Example 1</c>\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' +
'<c.magenta>1</c><br/><c.magenta><i>2</i></c>\n\n',
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
});

/**
* @param {!Array} cues
* @param {string} text
Expand Down

0 comments on commit a5f8b43

Please sign in to comment.