diff --git a/lib/text/vtt_text_parser.js b/lib/text/vtt_text_parser.js
index 04dc734c0e..562863ec77 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);
}
}
@@ -414,6 +414,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) {
@@ -439,6 +440,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(`${name}>`, `${newName}>`);
+ if (!hasVoiceEndTag) {
+ newPayload += `${newName}>`;
+ }
+ }
+ return newPayload;
+ }
+
/**
* Converts karaoke style tag to be valid for xml parsing
* For example,
@@ -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));
@@ -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;
diff --git a/test/text/vtt_text_parser_unit.js b/test/text/vtt_text_parser_unit.js
index 56c5cc389a..a33025b981 100644
--- a/test/text/vtt_text_parser_unit.js
+++ b/test/text/vtt_text_parser_unit.js
@@ -997,25 +997,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: [],
},
],
@@ -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' +
+ '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(
[
@@ -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' +
'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