Skip to content

Commit

Permalink
feat(VTT): Adds VTT tag rendering for <b>, <i> and <u> (#2776)
Browse files Browse the repository at this point in the history
Closes #2348
  • Loading branch information
Álvaro Velad Galván authored Aug 11, 2020
1 parent 1cebdf9 commit f42ccd2
Show file tree
Hide file tree
Showing 4 changed files with 239 additions and 9 deletions.
4 changes: 3 additions & 1 deletion lib/text/mp4_vtt_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,9 @@ shaka.text.Mp4VttParser = class {
* @private
*/
static assembleCue_(payload, id, settings, startTime, endTime) {
const cue = new shaka.text.Cue(startTime, endTime, payload);
const cue = new shaka.text.Cue(startTime, endTime, '');

shaka.text.VttTextParser.parseCueStyles(payload, cue);

if (id) {
cue.id = id;
Expand Down
75 changes: 74 additions & 1 deletion lib/text/vtt_text_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ goog.require('shaka.text.TextEngine');
goog.require('shaka.util.Error');
goog.require('shaka.util.StringUtils');
goog.require('shaka.util.TextParser');
goog.require('shaka.util.XmlUtils');


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

const cue = new shaka.text.Cue(start, end, payload);
const cue = new shaka.text.Cue(start, end, '');

VttTextParser.parseCueStyles(payload, cue);

// Parse optional settings.
parser.skipWhitespace();
Expand All @@ -219,6 +222,76 @@ shaka.text.VttTextParser = class {
return cue;
}

/**
* Parses a WebVTT styles from the given payload.
*
* @param {string} payload
* @param {!shaka.text.Cue} rootCue
*/
static parseCueStyles(payload, rootCue) {
const xmlPayload = '<span>' + payload + '</span>';
const element = shaka.util.XmlUtils.parseXmlString(xmlPayload, 'span');
if (element) {
/** @type {!Array.<!shaka.extern.Cue>} */
const cues = [];
const VttTextParser = shaka.text.VttTextParser;
const childNodes = element.childNodes;
if (childNodes.length == 1) {
const childNode = childNodes[0];
if (childNode.nodeType == Node.TEXT_NODE ||
childNode.nodeType == Node.CDATA_SECTION_NODE) {
rootCue.payload = payload;
return;
}
}
for (const childNode of childNodes) {
VttTextParser.generateCueFromElement_(childNode, rootCue, cues);
}
rootCue.nestedCues = cues;
} else {
shaka.log.warning('The cue\'s markup could not be parsed: ', payload);
rootCue.payload = payload;
}
}

/**
* @param {!Node} element
* @param {Array.<!shaka.extern.Cue>} cues
* @private
*/
static generateCueFromElement_(element, rootCue, cues) {
const nestedCue = rootCue.clone();
if (element.nodeType === Node.ELEMENT_NODE && element.nodeName) {
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(/[ .]+/);
for (const tag of tags) {
switch (tag) {
case 'b':
nestedCue.fontWeight = bold;
break;
case 'i':
nestedCue.fontStyle = italic;
break;
case 'u':
nestedCue.textDecoration.push(underline);
break;
}
}
}
const isTextNode = shaka.util.XmlUtils.isText(element);
if (isTextNode) {
nestedCue.payload = element.textContent;
cues.push(nestedCue);
} else {
const VttTextParser = shaka.text.VttTextParser;
for (const childNode of element.childNodes) {
VttTextParser.generateCueFromElement_(childNode, nestedCue, cues);
}
}
}

/**
* Parses a WebVTT setting from the given word.
*
Expand Down
16 changes: 11 additions & 5 deletions lib/util/xml_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,18 +97,24 @@ shaka.util.XmlUtils = class {
* @return {?string} The text contents, or null if there are none.
*/
static getContents(elem) {
const isText = (child) => {
return child.nodeType == Node.TEXT_NODE ||
child.nodeType == Node.CDATA_SECTION_NODE;
};
if (!Array.from(elem.childNodes).every(isText)) {
const XmlUtils = shaka.util.XmlUtils;
if (!Array.from(elem.childNodes).every(XmlUtils.isText)) {
return null;
}

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

/**
* Checks if a node is of type text.
* @param {!Node} elem The XML element.
* @return {boolean} True if it is a text node.
*/
static isText(elem) {
return elem.nodeType == Node.TEXT_NODE ||
elem.nodeType == Node.CDATA_SECTION_NODE;
}

/**
* Parses an attribute by its name.
Expand Down
153 changes: 151 additions & 2 deletions test/text/vtt_text_parser_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,147 @@ describe('VttTextParser', () => {
{periodStart: 0, segmentStart: 0, segmentEnd: 0});
});

it('supports payload stylized', () => {
verifyHelper(
[
{
startTime: 10,
endTime: 20,
payload: '',
nestedCues: [
{
startTime: 10,
endTime: 20,
payload: 'Test',
fontWeight: Cue.fontWeight.BOLD,
},
],
},
{
startTime: 20,
endTime: 30,
payload: '',
nestedCues: [
{
startTime: 20,
endTime: 30,
payload: 'Test2',
fontStyle: Cue.fontStyle.ITALIC,
},
],
},
{
startTime: 30,
endTime: 40,
payload: '',
nestedCues: [
{
startTime: 30,
endTime: 40,
payload: 'Test3',
textDecoration: [Cue.textDecoration.UNDERLINE],
},
],
},
{
startTime: 40,
endTime: 50,
payload: '',
nestedCues: [
{
startTime: 40,
endTime: 50,
payload: 'Test4',
},
],
},
{
startTime: 50,
endTime: 60,
payload: '',
nestedCues: [
{
startTime: 50,
endTime: 60,
payload: 'Test',
fontWeight: Cue.fontWeight.BOLD,
fontStyle: Cue.fontStyle.NORMAL,
},
{
startTime: 50,
endTime: 60,
payload: '5',
fontWeight: Cue.fontWeight.BOLD,
fontStyle: Cue.fontStyle.ITALIC,
},
],
},
{
startTime: 70,
endTime: 80,
payload: '',
nestedCues: [
{
startTime: 70,
endTime: 80,
payload: 'Test',
fontWeight: Cue.fontWeight.NORMAL,
},
{
startTime: 70,
endTime: 80,
payload: '6',
fontWeight: Cue.fontWeight.BOLD,
},
],
},
{
startTime: 80,
endTime: 90,
payload: '',
nestedCues: [
{
startTime: 80,
endTime: 90,
payload: 'Test ',
fontWeight: Cue.fontWeight.BOLD,
fontStyle: Cue.fontStyle.NORMAL,
},
{
startTime: 80,
endTime: 90,
payload: '7',
fontWeight: Cue.fontWeight.BOLD,
fontStyle: Cue.fontStyle.ITALIC,
},
],
},
{
startTime: 90,
endTime: 100,
payload: '<b>Test<i>8</b>',
},
],
'WEBVTT\n\n' +
'00:00:10.000 --> 00:00:20.000\n' +
'<b>Test</b>\n\n' +
'00:00:20.000 --> 00:00:30.000\n' +
'<i>Test2</i>\n\n' +
'00:00:30.000 --> 00:00:40.000\n' +
'<u>Test3</u>\n\n' +
'00:00:40.000 --> 00:00:50.000\n' +
'<a>Test4</a>\n\n' +
'00:00:50.000 --> 00:01:00.000\n' +
'<b>Test<i>5</i></b>\n\n' +
'00:01:10.000 --> 00:01:20.000\n' +
'Test<b>6</b>\n\n' +
'00:01:20.000 --> 00:01:30.000\n' +
'<b>Test <i>7</i></b>\n\n' +
'00:01:30.000 --> 00:01:40.000\n' +
'<b>Test<i>8</b>',
{periodStart: 0, segmentStart: 0, segmentEnd: 0});
});


/**
* @param {!Array} cues
Expand All @@ -595,9 +736,17 @@ describe('VttTextParser', () => {
function verifyHelper(cues, text, time) {
const data =
shaka.util.BufferUtils.toUint8(shaka.util.StringUtils.toUTF8(text));

const result = new shaka.text.VttTextParser().parseMedia(data, time);
expect(result).toEqual(cues.map((c) => jasmine.objectContaining(c)));

const expected = cues.map((cue) => {
if (cue.nestedCues) {
cue.nestedCues = cue.nestedCues.map(
(nestedCue) => jasmine.objectContaining(nestedCue)
);
}
return jasmine.objectContaining(cue);
});
expect(result).toEqual(expected);
}

/**
Expand Down

0 comments on commit f42ccd2

Please sign in to comment.