diff --git a/externs/shaka/text.js b/externs/shaka/text.js
index e9e64f15df..6b93330ed5 100644
--- a/externs/shaka/text.js
+++ b/externs/shaka/text.js
@@ -297,6 +297,13 @@ shaka.extern.Cue = class {
*/
this.fontFamily;
+ /**
+ * Text shadow color as a CSS text-shadow value.
+ * @type {string}
+ * @exportDoc
+ */
+ this.textShadow = '';
+
/**
* Text letter spacing as a CSS letter-spacing value.
* @type {string}
diff --git a/lib/text/cue.js b/lib/text/cue.js
index e1f6c5b344..1dec8f22d9 100644
--- a/lib/text/cue.js
+++ b/lib/text/cue.js
@@ -139,6 +139,12 @@ shaka.text.Cue = class {
*/
this.border = '';
+ /**
+ * @override
+ * @exportInterface
+ */
+ this.textShadow = '';
+
/**
* @override
* @exportInterface
diff --git a/lib/text/ui_text_displayer.js b/lib/text/ui_text_displayer.js
index d6a6cd3525..9ae660b57d 100644
--- a/lib/text/ui_text_displayer.js
+++ b/lib/text/ui_text_displayer.js
@@ -529,6 +529,7 @@ shaka.text.UITextDisplayer = class {
style.paddingRight =
shaka.text.UITextDisplayer.convertLengthValue_(
cue.linePadding, cue, this.videoContainer_);
+ style.textShadow = cue.textShadow;
if (cue.backgroundImage) {
style.backgroundImage = 'url(\'' + cue.backgroundImage + '\')';
diff --git a/lib/text/vtt_text_parser.js b/lib/text/vtt_text_parser.js
index d634ba9fac..83a7ca2cb3 100644
--- a/lib/text/vtt_text_parser.js
+++ b/lib/text/vtt_text_parser.js
@@ -199,92 +199,113 @@ shaka.text.VttTextParser = class {
return;
}
- if (!text[1].includes('::cue')) {
- return;
- }
- let styleSelector = 'global';
- // Look for what is within parentisesis. For example:
- // :: cue (b) {
, what we are looking for is b
- const selector = text[1].match(/\((.*)\)/);
- if (selector) {
- styleSelector = selector.pop();
+ /** @type {!Array.>} */
+ const styleBlocks = [];
+ let lastBlockIndex = -1;
+ for (let i = 1; i < text.length; i++) {
+ if (text[i].includes('::cue')) {
+ styleBlocks.push([]);
+ lastBlockIndex = styleBlocks.length - 1;
+ }
+ if (lastBlockIndex == -1) {
+ continue;
+ }
+ styleBlocks[lastBlockIndex].push(text[i]);
+ if (text[i].includes('}')) {
+ lastBlockIndex = -1;
+ }
}
- // We start at 2 to avoid '::cue' and end earlier to avoid '}'
- let propertyLines = text.slice(2, -1);
- if (text[1].includes('}')) {
- const payload = /\{(.*?)\}/.exec(text[1]);
- if (payload) {
- propertyLines = payload[1].split(';');
+ for (const styleBlock of styleBlocks) {
+ let styleSelector = 'global';
+ // Look for what is within parentheses. For example:
+ // :: cue (b) {
, what we are looking for is b
+ const selector = styleBlock[0].match(/\((.*)\)/);
+ if (selector) {
+ styleSelector = selector.pop();
}
- }
- const cue = new shaka.text.Cue(0, 0, '');
- let validStyle = false;
- for (let i = 0; i < propertyLines.length; i++) {
- // We look for CSS properties. As a general rule they are separated by
- // :
. Eg: color: red;
- const lineParts = /^\s*([^:]+):\s*(.*)/.exec(propertyLines[i]);
- if (lineParts) {
- const name = lineParts[1].trim();
- const value = lineParts[2].trim().replace(';', '');
- switch (name) {
- case 'background-color':
- validStyle = true;
- cue.backgroundColor = value;
- break;
- case 'color':
- validStyle = true;
- cue.color = value;
- break;
- case 'font-family':
- validStyle = true;
- cue.fontFamily = value;
- break;
- case 'font-size':
- validStyle = true;
- cue.fontSize = value;
- break;
- case 'font-weight':
- if (parseInt(value, 10) >= 700) {
+ // We start at 1 to avoid '::cue' and end earlier to avoid '}'
+ let propertyLines = styleBlock.slice(1, -1);
+ if (styleBlock[0].includes('}')) {
+ const payload = /\{(.*?)\}/.exec(styleBlock[0]);
+ if (payload) {
+ propertyLines = payload[1].split(';');
+ }
+ }
+
+ const cue = new shaka.text.Cue(0, 0, '');
+ let validStyle = false;
+ for (let i = 0; i < propertyLines.length; i++) {
+ // We look for CSS properties. As a general rule they are separated by
+ // :
. Eg: color: red;
+ const lineParts = /^\s*([^:]+):\s*(.*)/.exec(propertyLines[i]);
+ if (lineParts) {
+ const name = lineParts[1].trim();
+ const value = lineParts[2].trim().replace(';', '');
+ switch (name) {
+ case 'background-color':
+ case 'background':
validStyle = true;
- cue.fontWeight = shaka.text.Cue.fontWeight.BOLD;
- }
- break;
- case 'font-style':
- switch (value) {
- case 'normal':
- validStyle = true;
- cue.fontStyle = shaka.text.Cue.fontStyle.NORMAL;
- break;
- case 'italic':
- validStyle = true;
- cue.fontStyle = shaka.text.Cue.fontStyle.ITALIC;
- break;
- case 'oblique':
+ cue.backgroundColor = value;
+ break;
+ case 'color':
+ validStyle = true;
+ cue.color = value;
+ break;
+ case 'font-family':
+ validStyle = true;
+ cue.fontFamily = value;
+ break;
+ case 'font-size':
+ validStyle = true;
+ cue.fontSize = value;
+ break;
+ case 'font-weight':
+ if (parseInt(value, 10) >= 700 || value == 'bold') {
validStyle = true;
- cue.fontStyle = shaka.text.Cue.fontStyle.OBLIQUE;
- break;
- }
- break;
- case 'opacity':
- validStyle = true;
- cue.opacity = parseFloat(value);
- break;
- case 'white-space':
- validStyle = true;
- cue.wrapLine = value != 'noWrap';
- break;
- default:
- shaka.log.warning('VTT parser encountered an unsupported style: ',
- lineParts);
- break;
+ cue.fontWeight = shaka.text.Cue.fontWeight.BOLD;
+ }
+ break;
+ case 'font-style':
+ switch (value) {
+ case 'normal':
+ validStyle = true;
+ cue.fontStyle = shaka.text.Cue.fontStyle.NORMAL;
+ break;
+ case 'italic':
+ validStyle = true;
+ cue.fontStyle = shaka.text.Cue.fontStyle.ITALIC;
+ break;
+ case 'oblique':
+ validStyle = true;
+ cue.fontStyle = shaka.text.Cue.fontStyle.OBLIQUE;
+ break;
+ }
+ break;
+ case 'opacity':
+ validStyle = true;
+ cue.opacity = parseFloat(value);
+ break;
+ case 'text-shadow':
+ validStyle = true;
+ cue.textShadow = value;
+ break;
+ case 'white-space':
+ validStyle = true;
+ cue.wrapLine = value != 'noWrap';
+ break;
+ default:
+ shaka.log.warning('VTT parser encountered an unsupported style: ',
+ lineParts);
+ break;
+ }
}
}
- }
- if (validStyle) {
- styles.set(styleSelector, cue);
+ if (validStyle) {
+ styles.set(styleSelector, cue);
+ }
}
}
@@ -609,6 +630,11 @@ shaka.text.VttTextParser = class {
if (styleTag.startsWith('.voice-')) {
const voice = styleTag.split('-').pop();
styleTag = `v[voice="${voice}"]`;
+ // The specification allows to have quotes and not, so we check to
+ // see which one is being used.
+ if (!styles.has(styleTag)) {
+ styleTag = `v[voice=${voice}]`;
+ }
}
if (styles.has(styleTag)) {
VttTextParser.mergeStyle_(nestedCue, styles.get(styleTag));
diff --git a/test/text/vtt_text_parser_unit.js b/test/text/vtt_text_parser_unit.js
index 87d746ccf1..2c27ccb7a5 100644
--- a/test/text/vtt_text_parser_unit.js
+++ b/test/text/vtt_text_parser_unit.js
@@ -570,6 +570,7 @@ describe('VttTextParser', () => {
});
it('supports global style blocks', () => {
+ const textShadow = '-1px 0 black, 0 1px black, 1px 0 black, 0 -1px black';
verifyHelper(
[
{
@@ -578,6 +579,7 @@ describe('VttTextParser', () => {
payload: 'Test',
color: 'cyan',
fontSize: '10px',
+ textShadow: textShadow,
},
{
startTime: 40,
@@ -585,6 +587,7 @@ describe('VttTextParser', () => {
payload: 'Test2',
color: 'cyan',
fontSize: '10px',
+ textShadow: textShadow,
},
],
'WEBVTT\n\n' +
@@ -592,6 +595,7 @@ describe('VttTextParser', () => {
'::cue {\n' +
'color: cyan;\n'+
'font-size: 10px;\n'+
+ `text-shadow: ${textShadow};\n`+
'}\n\n' +
'00:00:20.000 --> 00:00:40.000\n' +
'Test\n\n' +
@@ -993,7 +997,7 @@ describe('VttTextParser', () => {
startTime: 40,
endTime: 50,
payload: 'Test',
- color: 'cyan',
+ color: 'red',
},
{
startTime: 40,
@@ -1005,11 +1009,13 @@ describe('VttTextParser', () => {
},
],
'WEBVTT\n\n' +
- 'STYLE\n::cue(v[voice="Shaka"]) { color: cyan; }\n\n' +
+ 'STYLE\n' +
+ '::cue(v[voice="Shaka"]) { color: cyan; }\n' +
+ '::cue(v[voice=ShakaBis]) { color: red; }\n\n' +
'00:00:20.000 --> 00:00:40.000\n' +
'Test\n\n' +
'00:00:40.000 --> 00:00:50.000\n' +
- 'Test2',
+ 'Test2',
{periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0});
});