mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-14 15:56:38 +03:00
Add support of fontSize in percentages and cell resolution unit for TTML captions (#2442)
Closes #2403
This commit is contained in:
@@ -50,6 +50,7 @@ SameGoal Inc. <*@samegoal.com>
|
||||
Sanborn Hilland <sanbornh@rogers.com>
|
||||
Sander Saares <sander@saares.eu>
|
||||
TalkTalk Plc <*@talktalkplc.com>
|
||||
Tatsiana Gelahova <tatsiana.gelahova@gmail.com>
|
||||
Tomas Tichy <mr.tichyt@gmail.com>
|
||||
Tomohiro Matsuzawa <thmatuza75@hotmail.com>
|
||||
Toshihiro Suzuki <t.suzuki326@gmail.com>
|
||||
|
||||
@@ -76,6 +76,7 @@ Sandra Lokshina <ismena@google.com>
|
||||
Satheesh Velmurugan <satheesh@philo.com>
|
||||
Semih Gokceoglu <semih.gkcoglu@gmail.com>
|
||||
Seth Madison <seth@philo.com>
|
||||
Tatsiana Gelahova <tatsiana.gelahova@gmail.com>
|
||||
Theodore Abshire <theodab@google.com>
|
||||
Thomas Stephens <thomas@ustudio.com>
|
||||
Tim Plummer <objelisks@google.com>
|
||||
|
||||
@@ -246,6 +246,15 @@ shaka.extern.Cue = class {
|
||||
*/
|
||||
this.backgroundColor;
|
||||
|
||||
/**
|
||||
* The number of horizontal and vertical cells into which
|
||||
* the Root Container Region area is divided
|
||||
*
|
||||
* @type {{ columns: number, rows: number }}
|
||||
* @exportDoc
|
||||
*/
|
||||
this.cellResolution;
|
||||
|
||||
/**
|
||||
* Image background represented by any string that would be
|
||||
* accepted in image HTML element.
|
||||
|
||||
@@ -200,6 +200,15 @@ shaka.text.Cue = class {
|
||||
* @exportInterface
|
||||
*/
|
||||
this.spacer = false;
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @exportInterface
|
||||
*/
|
||||
this.cellResolution = {
|
||||
columns: 32,
|
||||
rows: 15,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@ shaka.text.TtmlTextParser = class {
|
||||
let tickRate = null;
|
||||
let spaceStyle = null;
|
||||
let extent = null;
|
||||
let cellResolution = null;
|
||||
const tts = xml.getElementsByTagName('tt');
|
||||
const tt = tts[0];
|
||||
// TTML should always have tt element.
|
||||
@@ -91,6 +92,7 @@ shaka.text.TtmlTextParser = class {
|
||||
frameRateMultiplier =
|
||||
XmlUtils.getAttributeNS(tt, ttpNs, 'frameRateMultiplier');
|
||||
tickRate = XmlUtils.getAttributeNS(tt, ttpNs, 'tickRate');
|
||||
cellResolution = XmlUtils.getAttributeNS(tt, ttpNs, 'cellResolution');
|
||||
spaceStyle = tt.getAttribute('xml:space') || 'default';
|
||||
extent = tt.getAttribute('tts:extent');
|
||||
}
|
||||
@@ -107,6 +109,9 @@ shaka.text.TtmlTextParser = class {
|
||||
const rateInfo = new TtmlTextParser.RateInfo_(
|
||||
frameRate, subFrameRate, frameRateMultiplier, tickRate);
|
||||
|
||||
const cellResolutionInfo =
|
||||
TtmlTextParser.getCellResolution_(cellResolution);
|
||||
|
||||
const metadataElements = TtmlTextParser.getLeafNodes_(
|
||||
tt.getElementsByTagName('metadata')[0]);
|
||||
const styles = TtmlTextParser.getLeafNodes_(
|
||||
@@ -127,7 +132,8 @@ shaka.text.TtmlTextParser = class {
|
||||
for (const node of textNodes) {
|
||||
const cue = TtmlTextParser.parseCue_(
|
||||
node, time.periodStart, rateInfo, metadataElements, styles,
|
||||
regionElements, cueRegions, whitespaceTrim, false);
|
||||
regionElements, cueRegions, whitespaceTrim, false,
|
||||
cellResolutionInfo);
|
||||
if (cue) {
|
||||
ret.push(cue);
|
||||
}
|
||||
@@ -239,12 +245,13 @@ shaka.text.TtmlTextParser = class {
|
||||
* @param {!Array.<!shaka.text.CueRegion>} cueRegions
|
||||
* @param {boolean} whitespaceTrim
|
||||
* @param {boolean} isNested
|
||||
* @param {?{columns: number, rows: number}} cellResolution
|
||||
* @return {shaka.text.Cue}
|
||||
* @private
|
||||
*/
|
||||
static parseCue_(
|
||||
cueElement, offset, rateInfo, metadataElements, styles, regionElements,
|
||||
cueRegions, whitespaceTrim, isNested) {
|
||||
cueRegions, whitespaceTrim, isNested, cellResolution) {
|
||||
if (isNested && cueElement.nodeName == 'br') {
|
||||
const cue = new shaka.text.Cue(0, 0, '');
|
||||
cue.spacer = true;
|
||||
@@ -322,7 +329,8 @@ shaka.text.TtmlTextParser = class {
|
||||
regionElements,
|
||||
cueRegions,
|
||||
whitespaceTrim,
|
||||
/* isNested= */ true
|
||||
/* isNested= */ true,
|
||||
cellResolution,
|
||||
);
|
||||
|
||||
if (nestedCue) {
|
||||
@@ -334,6 +342,10 @@ shaka.text.TtmlTextParser = class {
|
||||
const cue = new shaka.text.Cue(start, end, payload);
|
||||
cue.nestedCues = nestedCues;
|
||||
|
||||
if (cellResolution) {
|
||||
cue.cellResolution = cellResolution;
|
||||
}
|
||||
|
||||
// Get other properties if available.
|
||||
const regionElement = shaka.text.TtmlTextParser.getElementsFromCollection_(
|
||||
cueElement, 'region', regionElements, /* prefix= */ '')[0];
|
||||
@@ -543,7 +555,12 @@ shaka.text.TtmlTextParser = class {
|
||||
|
||||
const fontSize = TtmlTextParser.getStyleAttribute_(
|
||||
cueElement, region, styles, 'fontSize');
|
||||
if (fontSize && fontSize.match(TtmlTextParser.unitValues_)) {
|
||||
|
||||
const isValidFontSizeUnit = fontSize
|
||||
&& (fontSize.match(TtmlTextParser.unitValues_)
|
||||
|| fontSize.match(TtmlTextParser.percentValue_));
|
||||
|
||||
if (isValidFontSizeUnit) {
|
||||
cue.fontSize = fontSize;
|
||||
}
|
||||
|
||||
@@ -933,6 +950,31 @@ shaka.text.TtmlTextParser = class {
|
||||
|
||||
return (milliseconds / 1000) + seconds + (minutes * 60) + (hours * 3600);
|
||||
}
|
||||
|
||||
/**
|
||||
* If ttp:cellResolution provided returns cell resolution info
|
||||
* with number of columns and rows into which the Root Container
|
||||
* Region area is divided
|
||||
*
|
||||
* @param {?string} cellResolution
|
||||
* @return {?{columns: number, rows: number}}
|
||||
* @private
|
||||
*/
|
||||
static getCellResolution_(cellResolution) {
|
||||
if (!cellResolution) {
|
||||
return null;
|
||||
}
|
||||
const matches = /^(\d+) (\d+)$/.exec(cellResolution);
|
||||
|
||||
if (!matches) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const columns = parseInt(matches[1], 10);
|
||||
const rows = parseInt(matches[2], 10);
|
||||
|
||||
return {columns, rows};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -996,9 +1038,16 @@ shaka.text.TtmlTextParser.percentValues_ =
|
||||
/**
|
||||
* @const
|
||||
* @private {!RegExp}
|
||||
* @example 100px
|
||||
* @example 0.6% 90%
|
||||
*/
|
||||
shaka.text.TtmlTextParser.unitValues_ = /^(\d+px|\d+em)$/;
|
||||
shaka.text.TtmlTextParser.percentValue_ = /^(\d{1,2}(?:\.\d+)?|100)%$/;
|
||||
|
||||
/**
|
||||
* @const
|
||||
* @private {!RegExp}
|
||||
* @example 100px, 8em, 0.80c
|
||||
*/
|
||||
shaka.text.TtmlTextParser.unitValues_ = /^(\d+px|\d+em|\d*\.?\d+c)$/;
|
||||
|
||||
/**
|
||||
* @const
|
||||
|
||||
@@ -792,8 +792,8 @@ describe('TtmlTextParser', () => {
|
||||
verifyHelper(
|
||||
[
|
||||
{
|
||||
startTime: 62.05,
|
||||
endTime: 3723.2,
|
||||
startTime: 1,
|
||||
endTime: 2,
|
||||
payload: 'Test',
|
||||
color: 'red',
|
||||
backgroundColor: 'blue',
|
||||
@@ -803,6 +803,12 @@ describe('TtmlTextParser', () => {
|
||||
lineHeight: '20px',
|
||||
fontSize: '10em',
|
||||
},
|
||||
{
|
||||
startTime: 2,
|
||||
endTime: 4,
|
||||
payload: 'Test 2',
|
||||
fontSize: '0.80c',
|
||||
},
|
||||
],
|
||||
'<tt xmlns:tts="http://www.w3.org/ns/ttml#styling">' +
|
||||
'<styling>' +
|
||||
@@ -813,12 +819,14 @@ describe('TtmlTextParser', () => {
|
||||
'tts:fontStyle="italic" ' +
|
||||
'tts:lineHeight="20px" ' +
|
||||
'tts:fontSize="10em"/>' +
|
||||
'<style xml:id="s2" tts:fontSize="0.80c" />' +
|
||||
'</styling>' +
|
||||
'<layout>' +
|
||||
'<region xml:id="subtitleArea" />' +
|
||||
'</layout>' +
|
||||
'<body region="subtitleArea">' +
|
||||
'<p begin="01:02.05" end="01:02:03.200" style="s1">Test</p>' +
|
||||
'<p begin="00:01.00" end="00:02.00" style="s1">Test</p>' +
|
||||
'<p begin="00:02.00" end="00:04.00" style="s2">Test 2</p>' +
|
||||
'</body>' +
|
||||
'</tt>',
|
||||
{periodStart: 0, segmentStart: 0, segmentEnd: 0});
|
||||
@@ -875,6 +883,58 @@ describe('TtmlTextParser', () => {
|
||||
{periodStart: 0, segmentStart: 0, segmentEnd: 0});
|
||||
});
|
||||
|
||||
it('cues should have default cellResolution', () => {
|
||||
verifyHelper(
|
||||
[
|
||||
{
|
||||
startTime: 1,
|
||||
endTime: 2,
|
||||
cellResolution: {
|
||||
columns: 32,
|
||||
rows: 15,
|
||||
},
|
||||
fontSize: '0.45c',
|
||||
},
|
||||
],
|
||||
'<tt xmlns:tts="http://www.w3.org/ns/ttml#styling">' +
|
||||
'<styling>' +
|
||||
'<style xml:id="s1" tts:fontSize="0.45c"/>' +
|
||||
'</styling>' +
|
||||
'<body >' +
|
||||
'<p begin="00:01.00" end="00:02.00" style="s1">Test</p>' +
|
||||
'</body>' +
|
||||
'</tt>',
|
||||
{periodStart: 0, segmentStart: 0, segmentEnd: 0});
|
||||
});
|
||||
|
||||
it('parses cellResolution', () => {
|
||||
verifyHelper(
|
||||
[
|
||||
{
|
||||
startTime: 1,
|
||||
endTime: 2,
|
||||
payload: 'Test',
|
||||
cellResolution: {
|
||||
columns: 60,
|
||||
rows: 20,
|
||||
},
|
||||
fontSize: '67%',
|
||||
},
|
||||
],
|
||||
'<tt ' +
|
||||
'xmlns:ttp="http://www.w3.org/ns/ttml#parameter" ' +
|
||||
'xmlns:tts="http://www.w3.org/ns/ttml#styling" ' +
|
||||
'ttp:cellResolution="60 20">' +
|
||||
'<styling>' +
|
||||
'<style xml:id="s1" tts:fontSize="67%"/>' +
|
||||
'</styling>' +
|
||||
'<body >' +
|
||||
'<p begin="00:01.00" end="00:02.00" style="s1">Test</p>' +
|
||||
'</body>' +
|
||||
'</tt>',
|
||||
{periodStart: 0, segmentStart: 0, segmentEnd: 0});
|
||||
});
|
||||
|
||||
it('chooses style on element over style on region', () => {
|
||||
verifyHelper(
|
||||
[
|
||||
@@ -913,7 +973,6 @@ describe('TtmlTextParser', () => {
|
||||
{periodStart: 0, segmentStart: 0, segmentEnd: 0});
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* @param {!Array} cues
|
||||
* @param {string} text
|
||||
|
||||
@@ -10,6 +10,8 @@ describe('UITextDisplayer', () => {
|
||||
let video;
|
||||
/** @type {shaka.ui.TextDisplayer} */
|
||||
let textDisplayer;
|
||||
/** @type {number} */
|
||||
const videoContainerHeight = 450;
|
||||
|
||||
/**
|
||||
* Transform a cssText to an object.
|
||||
@@ -42,12 +44,20 @@ describe('UITextDisplayer', () => {
|
||||
beforeAll(() => {
|
||||
videoContainer =
|
||||
/** @type {!HTMLElement} */ (document.createElement('div'));
|
||||
videoContainer.style.height = `${videoContainerHeight}px`;
|
||||
document.body.appendChild(videoContainer);
|
||||
video = shaka.test.UiUtils.createVideoElement();
|
||||
videoContainer.appendChild(video);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
textDisplayer = new shaka.ui.TextDisplayer(video, videoContainer);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await textDisplayer.destroy();
|
||||
});
|
||||
|
||||
it('correctly displays styles for cues', async () => {
|
||||
/** @type {!shaka.text.Cue} */
|
||||
const cue = new shaka.text.Cue(0, 100, 'Captain\'s log.');
|
||||
@@ -129,4 +139,56 @@ describe('UITextDisplayer', () => {
|
||||
// 'writing-mode': 'horizontal-tb',
|
||||
}));
|
||||
});
|
||||
|
||||
it('correctly displays styles for cellResolution units', async () => {
|
||||
/** @type {!shaka.text.Cue} */
|
||||
const cue = new shaka.text.Cue(0, 100, 'Captain\'s log.');
|
||||
cue.fontSize = '0.80c';
|
||||
cue.cellResolution = {
|
||||
columns: 60,
|
||||
rows: 20,
|
||||
};
|
||||
|
||||
textDisplayer.setTextVisibility(true);
|
||||
textDisplayer.append([cue]);
|
||||
// Wait until updateCaptions_() gets called.
|
||||
await shaka.test.Util.delay(0.5);
|
||||
|
||||
// Expected value is calculated based on ttp:cellResolution="60 20"
|
||||
// videoContainerHeight=450px and tts:fontSize="0.80c" on the default style.
|
||||
const expectedFontSize = '18px';
|
||||
|
||||
const textContainer =
|
||||
videoContainer.querySelector('.shaka-text-container');
|
||||
const captions = textContainer.querySelector('span');
|
||||
const cssObj = parseCssText(captions.style.cssText);
|
||||
expect(cssObj).toEqual(
|
||||
jasmine.objectContaining({'font-size': expectedFontSize}));
|
||||
});
|
||||
|
||||
it('correctly displays styles for percentages units', async () => {
|
||||
/** @type {!shaka.text.Cue} */
|
||||
const cue = new shaka.text.Cue(0, 100, 'Captain\'s log.');
|
||||
cue.fontSize = '90%';
|
||||
cue.cellResolution = {
|
||||
columns: 32,
|
||||
rows: 15,
|
||||
};
|
||||
|
||||
textDisplayer.setTextVisibility(true);
|
||||
textDisplayer.append([cue]);
|
||||
// Wait until updateCaptions_() gets called.
|
||||
await shaka.test.Util.delay(0.5);
|
||||
|
||||
// Expected value is calculated based on ttp:cellResolution="32 15"
|
||||
// videoContainerHeight=450px and tts:fontSize="90%" on the default style.
|
||||
const expectedFontSize = '27px';
|
||||
|
||||
const textContainer =
|
||||
videoContainer.querySelector('.shaka-text-container');
|
||||
const captions = textContainer.querySelector('span');
|
||||
const cssObj = parseCssText(captions.style.cssText);
|
||||
expect(cssObj).toEqual(
|
||||
jasmine.objectContaining({'font-size': expectedFontSize}));
|
||||
});
|
||||
});
|
||||
|
||||
+74
-1
@@ -270,9 +270,11 @@ shaka.ui.TextDisplayer = class {
|
||||
|
||||
captionsStyle.fontFamily = cue.fontFamily;
|
||||
captionsStyle.fontWeight = cue.fontWeight.toString();
|
||||
captionsStyle.fontSize = cue.fontSize;
|
||||
captionsStyle.fontStyle = cue.fontStyle;
|
||||
captionsStyle.letterSpacing = cue.letterSpacing;
|
||||
captionsStyle.fontSize = shaka.ui.TextDisplayer.convertLengthValue_(
|
||||
cue.fontSize, cue, this.videoContainer_
|
||||
);
|
||||
|
||||
// The line attribute defines the positioning of the text container inside
|
||||
// the video container.
|
||||
@@ -361,4 +363,75 @@ shaka.ui.TextDisplayer = class {
|
||||
panelStyle.height = cue.size + '%';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns info about provided lengthValue
|
||||
* @example 100px => { value: 100, unit: 'px' }
|
||||
* @param {?string} lengthValue
|
||||
*
|
||||
* @return {?{ value: number, unit: string }}
|
||||
* @private
|
||||
*/
|
||||
static getLengthValueInfo_(lengthValue) {
|
||||
const matches = new RegExp(/(\d*\.?\d+)([a-z]+|%+)/).exec(lengthValue);
|
||||
|
||||
if (!matches) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
value: Number(matches[1]),
|
||||
unit: matches[2],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts length value to an absolute value in pixels.
|
||||
* If lengthValue is already an absolute value it will not
|
||||
* be modified. Relative lengthValue will be converted to an
|
||||
* absolute value in pixels based on Computed Cell Size
|
||||
*
|
||||
* @param {string} lengthValue
|
||||
* @param {!shaka.extern.Cue} cue
|
||||
* @param {HTMLElement} videoContainer
|
||||
* @return {string}
|
||||
* @private
|
||||
*/
|
||||
static convertLengthValue_(lengthValue, cue, videoContainer) {
|
||||
const lengthValueInfo =
|
||||
shaka.ui.TextDisplayer.getLengthValueInfo_(lengthValue);
|
||||
|
||||
if (!lengthValueInfo) {
|
||||
return lengthValue;
|
||||
}
|
||||
|
||||
const {unit, value} = lengthValueInfo;
|
||||
|
||||
switch (unit) {
|
||||
case '%':
|
||||
return shaka.ui.TextDisplayer.getAbsoluteLengthInPixels_(
|
||||
value / 100, cue, videoContainer);
|
||||
case 'c':
|
||||
return shaka.ui.TextDisplayer.getAbsoluteLengthInPixels_(
|
||||
value, cue, videoContainer);
|
||||
default:
|
||||
return lengthValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns computed absolute length value in pixels based on cell
|
||||
* and a video container size
|
||||
* @param {number} value
|
||||
* @param {!shaka.extern.Cue} cue
|
||||
* @param {HTMLElement} videoContainer
|
||||
* @return {string}
|
||||
*
|
||||
* @private
|
||||
* */
|
||||
static getAbsoluteLengthInPixels_(value, cue, videoContainer) {
|
||||
const containerHeight = videoContainer.clientHeight;
|
||||
|
||||
return (containerHeight * value / cue.cellResolution.rows) + 'px';
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user