Skip to content

Commit f42ccd2

Browse files
author
Álvaro Velad Galván
authored
feat(VTT): Adds VTT tag rendering for <b>, <i> and <u> (#2776)
Closes #2348
1 parent 1cebdf9 commit f42ccd2

File tree

4 files changed

+239
-9
lines changed

4 files changed

+239
-9
lines changed

lib/text/mp4_vtt_parser.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,9 @@ shaka.text.Mp4VttParser = class {
287287
* @private
288288
*/
289289
static assembleCue_(payload, id, settings, startTime, endTime) {
290-
const cue = new shaka.text.Cue(startTime, endTime, payload);
290+
const cue = new shaka.text.Cue(startTime, endTime, '');
291+
292+
shaka.text.VttTextParser.parseCueStyles(payload, cue);
291293

292294
if (id) {
293295
cue.id = id;

lib/text/vtt_text_parser.js

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ goog.require('shaka.text.TextEngine');
1414
goog.require('shaka.util.Error');
1515
goog.require('shaka.util.StringUtils');
1616
goog.require('shaka.util.TextParser');
17+
goog.require('shaka.util.XmlUtils');
1718

1819

1920
/**
@@ -198,7 +199,9 @@ shaka.text.VttTextParser = class {
198199
// Get the payload.
199200
const payload = text.slice(1).join('\n').trim();
200201

201-
const cue = new shaka.text.Cue(start, end, payload);
202+
const cue = new shaka.text.Cue(start, end, '');
203+
204+
VttTextParser.parseCueStyles(payload, cue);
202205

203206
// Parse optional settings.
204207
parser.skipWhitespace();
@@ -219,6 +222,76 @@ shaka.text.VttTextParser = class {
219222
return cue;
220223
}
221224

225+
/**
226+
* Parses a WebVTT styles from the given payload.
227+
*
228+
* @param {string} payload
229+
* @param {!shaka.text.Cue} rootCue
230+
*/
231+
static parseCueStyles(payload, rootCue) {
232+
const xmlPayload = '<span>' + payload + '</span>';
233+
const element = shaka.util.XmlUtils.parseXmlString(xmlPayload, 'span');
234+
if (element) {
235+
/** @type {!Array.<!shaka.extern.Cue>} */
236+
const cues = [];
237+
const VttTextParser = shaka.text.VttTextParser;
238+
const childNodes = element.childNodes;
239+
if (childNodes.length == 1) {
240+
const childNode = childNodes[0];
241+
if (childNode.nodeType == Node.TEXT_NODE ||
242+
childNode.nodeType == Node.CDATA_SECTION_NODE) {
243+
rootCue.payload = payload;
244+
return;
245+
}
246+
}
247+
for (const childNode of childNodes) {
248+
VttTextParser.generateCueFromElement_(childNode, rootCue, cues);
249+
}
250+
rootCue.nestedCues = cues;
251+
} else {
252+
shaka.log.warning('The cue\'s markup could not be parsed: ', payload);
253+
rootCue.payload = payload;
254+
}
255+
}
256+
257+
/**
258+
* @param {!Node} element
259+
* @param {Array.<!shaka.extern.Cue>} cues
260+
* @private
261+
*/
262+
static generateCueFromElement_(element, rootCue, cues) {
263+
const nestedCue = rootCue.clone();
264+
if (element.nodeType === Node.ELEMENT_NODE && element.nodeName) {
265+
const bold = shaka.text.Cue.fontWeight.BOLD;
266+
const italic = shaka.text.Cue.fontStyle.ITALIC;
267+
const underline = shaka.text.Cue.textDecoration.UNDERLINE;
268+
const tags = element.nodeName.split(/[ .]+/);
269+
for (const tag of tags) {
270+
switch (tag) {
271+
case 'b':
272+
nestedCue.fontWeight = bold;
273+
break;
274+
case 'i':
275+
nestedCue.fontStyle = italic;
276+
break;
277+
case 'u':
278+
nestedCue.textDecoration.push(underline);
279+
break;
280+
}
281+
}
282+
}
283+
const isTextNode = shaka.util.XmlUtils.isText(element);
284+
if (isTextNode) {
285+
nestedCue.payload = element.textContent;
286+
cues.push(nestedCue);
287+
} else {
288+
const VttTextParser = shaka.text.VttTextParser;
289+
for (const childNode of element.childNodes) {
290+
VttTextParser.generateCueFromElement_(childNode, nestedCue, cues);
291+
}
292+
}
293+
}
294+
222295
/**
223296
* Parses a WebVTT setting from the given word.
224297
*

lib/util/xml_utils.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,18 +97,24 @@ shaka.util.XmlUtils = class {
9797
* @return {?string} The text contents, or null if there are none.
9898
*/
9999
static getContents(elem) {
100-
const isText = (child) => {
101-
return child.nodeType == Node.TEXT_NODE ||
102-
child.nodeType == Node.CDATA_SECTION_NODE;
103-
};
104-
if (!Array.from(elem.childNodes).every(isText)) {
100+
const XmlUtils = shaka.util.XmlUtils;
101+
if (!Array.from(elem.childNodes).every(XmlUtils.isText)) {
105102
return null;
106103
}
107104

108105
// Read merged text content from all text nodes.
109106
return elem.textContent.trim();
110107
}
111108

109+
/**
110+
* Checks if a node is of type text.
111+
* @param {!Node} elem The XML element.
112+
* @return {boolean} True if it is a text node.
113+
*/
114+
static isText(elem) {
115+
return elem.nodeType == Node.TEXT_NODE ||
116+
elem.nodeType == Node.CDATA_SECTION_NODE;
117+
}
112118

113119
/**
114120
* Parses an attribute by its name.

test/text/vtt_text_parser_unit.js

Lines changed: 151 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,147 @@ describe('VttTextParser', () => {
586586
{periodStart: 0, segmentStart: 0, segmentEnd: 0});
587587
});
588588

589+
it('supports payload stylized', () => {
590+
verifyHelper(
591+
[
592+
{
593+
startTime: 10,
594+
endTime: 20,
595+
payload: '',
596+
nestedCues: [
597+
{
598+
startTime: 10,
599+
endTime: 20,
600+
payload: 'Test',
601+
fontWeight: Cue.fontWeight.BOLD,
602+
},
603+
],
604+
},
605+
{
606+
startTime: 20,
607+
endTime: 30,
608+
payload: '',
609+
nestedCues: [
610+
{
611+
startTime: 20,
612+
endTime: 30,
613+
payload: 'Test2',
614+
fontStyle: Cue.fontStyle.ITALIC,
615+
},
616+
],
617+
},
618+
{
619+
startTime: 30,
620+
endTime: 40,
621+
payload: '',
622+
nestedCues: [
623+
{
624+
startTime: 30,
625+
endTime: 40,
626+
payload: 'Test3',
627+
textDecoration: [Cue.textDecoration.UNDERLINE],
628+
},
629+
],
630+
},
631+
{
632+
startTime: 40,
633+
endTime: 50,
634+
payload: '',
635+
nestedCues: [
636+
{
637+
startTime: 40,
638+
endTime: 50,
639+
payload: 'Test4',
640+
},
641+
],
642+
},
643+
{
644+
startTime: 50,
645+
endTime: 60,
646+
payload: '',
647+
nestedCues: [
648+
{
649+
startTime: 50,
650+
endTime: 60,
651+
payload: 'Test',
652+
fontWeight: Cue.fontWeight.BOLD,
653+
fontStyle: Cue.fontStyle.NORMAL,
654+
},
655+
{
656+
startTime: 50,
657+
endTime: 60,
658+
payload: '5',
659+
fontWeight: Cue.fontWeight.BOLD,
660+
fontStyle: Cue.fontStyle.ITALIC,
661+
},
662+
],
663+
},
664+
{
665+
startTime: 70,
666+
endTime: 80,
667+
payload: '',
668+
nestedCues: [
669+
{
670+
startTime: 70,
671+
endTime: 80,
672+
payload: 'Test',
673+
fontWeight: Cue.fontWeight.NORMAL,
674+
},
675+
{
676+
startTime: 70,
677+
endTime: 80,
678+
payload: '6',
679+
fontWeight: Cue.fontWeight.BOLD,
680+
},
681+
],
682+
},
683+
{
684+
startTime: 80,
685+
endTime: 90,
686+
payload: '',
687+
nestedCues: [
688+
{
689+
startTime: 80,
690+
endTime: 90,
691+
payload: 'Test ',
692+
fontWeight: Cue.fontWeight.BOLD,
693+
fontStyle: Cue.fontStyle.NORMAL,
694+
},
695+
{
696+
startTime: 80,
697+
endTime: 90,
698+
payload: '7',
699+
fontWeight: Cue.fontWeight.BOLD,
700+
fontStyle: Cue.fontStyle.ITALIC,
701+
},
702+
],
703+
},
704+
{
705+
startTime: 90,
706+
endTime: 100,
707+
payload: '<b>Test<i>8</b>',
708+
},
709+
],
710+
'WEBVTT\n\n' +
711+
'00:00:10.000 --> 00:00:20.000\n' +
712+
'<b>Test</b>\n\n' +
713+
'00:00:20.000 --> 00:00:30.000\n' +
714+
'<i>Test2</i>\n\n' +
715+
'00:00:30.000 --> 00:00:40.000\n' +
716+
'<u>Test3</u>\n\n' +
717+
'00:00:40.000 --> 00:00:50.000\n' +
718+
'<a>Test4</a>\n\n' +
719+
'00:00:50.000 --> 00:01:00.000\n' +
720+
'<b>Test<i>5</i></b>\n\n' +
721+
'00:01:10.000 --> 00:01:20.000\n' +
722+
'Test<b>6</b>\n\n' +
723+
'00:01:20.000 --> 00:01:30.000\n' +
724+
'<b>Test <i>7</i></b>\n\n' +
725+
'00:01:30.000 --> 00:01:40.000\n' +
726+
'<b>Test<i>8</b>',
727+
{periodStart: 0, segmentStart: 0, segmentEnd: 0});
728+
});
729+
589730

590731
/**
591732
* @param {!Array} cues
@@ -595,9 +736,17 @@ describe('VttTextParser', () => {
595736
function verifyHelper(cues, text, time) {
596737
const data =
597738
shaka.util.BufferUtils.toUint8(shaka.util.StringUtils.toUTF8(text));
598-
599739
const result = new shaka.text.VttTextParser().parseMedia(data, time);
600-
expect(result).toEqual(cues.map((c) => jasmine.objectContaining(c)));
740+
741+
const expected = cues.map((cue) => {
742+
if (cue.nestedCues) {
743+
cue.nestedCues = cue.nestedCues.map(
744+
(nestedCue) => jasmine.objectContaining(nestedCue)
745+
);
746+
}
747+
return jasmine.objectContaining(cue);
748+
});
749+
expect(result).toEqual(expected);
601750
}
602751

603752
/**

0 commit comments

Comments
 (0)