feat: Allow override subtitle position (#9522)

Close https://github.com/shaka-project/shaka-player/issues/9521

---------

Co-authored-by: Wojciech Tyczyński <tykus160@gmail.com>
This commit is contained in:
Álvaro Velad Galván
2025-12-19 13:38:33 +01:00
committed by GitHub
parent 94abc9c4fb
commit c6e6082bc2
25 changed files with 847 additions and 14 deletions
+22 -8
View File
@@ -344,15 +344,29 @@ shaka.text.Cue = class {
clone() {
const clone = new shaka.text.Cue(0, 0, '');
for (const k in this) {
clone[k] = this[k];
// Make copies of array fields, but only one level deep. That way, if we
// change, for instance, textDecoration on the clone, we don't affect the
// original.
if (Array.isArray(clone[k])) {
clone[k] = /** @type {!Array} */(clone[k]).slice();
/**
* Deep clone helper
* @param {*} value
* @return {*}
*/
const deepClone = (value) => {
if (value === null || typeof value !== 'object') {
return value;
}
if (Array.isArray(value)) {
return value.map(deepClone);
}
const result = {};
for (const key in value) {
result[key] = deepClone(value[key]);
}
return result;
};
for (const k in this) {
clone[k] = deepClone(this[k]);
}
return clone;
+32
View File
@@ -425,4 +425,36 @@ shaka.text.Utils = class {
texTrack.mode = 'disabled';
}
}
/**
* Reset all positioning-related properties of a cue, including all nested
* cues.
*
* @param {!shaka.text.Cue} cue
*/
static resetCuePositioning(cue) {
// Cue dummy to obtain the real default values
const defaultCue = new shaka.text.Cue(0, 0, '');
/**
* Copy default positioning values recursively
*
* @param {!shaka.text.Cue} target
*/
const reset = (target) => {
target.line = defaultCue.line;
target.lineAlign = defaultCue.lineAlign;
target.position = defaultCue.position;
target.positionAlign = defaultCue.positionAlign;
target.size = defaultCue.size;
target.displayAlign = defaultCue.displayAlign;
target.region = defaultCue.region;
for (const nested of target.nestedCues) {
reset(nested);
}
};
reset(cue);
}
};
+93 -4
View File
@@ -8,11 +8,13 @@
goog.provide('shaka.text.UITextDisplayer');
goog.require('goog.asserts');
goog.require('shaka.config.PositionArea');
goog.require('shaka.text.Cue');
goog.require('shaka.text.CueRegion');
goog.require('shaka.text.Utils');
goog.require('shaka.util.Dom');
goog.require('shaka.util.EventManager');
goog.require('shaka.util.Lazy');
goog.require('shaka.util.Timer');
goog.requireType('shaka.Player');
@@ -331,7 +333,18 @@ shaka.text.UITextDisplayer = class {
for (const cue of cues) {
parents.push(cue);
let cueKey = cue;
let cueRegistry = this.currentCuesMap_.get(cue);
if (!cueRegistry && this.config_ &&
this.config_.positionArea != shaka.config.PositionArea.DEFAULT) {
for (const key of this.currentCuesMap_.keys()) {
if (shaka.text.Cue.equal(cue, key)) {
cueKey = key;
cueRegistry = this.currentCuesMap_.get(key);
}
}
}
const shouldBeDisplayed =
cue.startTime <= currentTime && cue.endTime > currentTime;
let wrapper = cueRegistry ? cueRegistry.wrapper : null;
@@ -350,7 +363,7 @@ shaka.text.UITextDisplayer = class {
if (!shouldBeDisplayed) {
// Since something has to be removed, we will need to update the DOM.
updateDOM = true;
this.currentCuesMap_.delete(cue);
this.currentCuesMap_.delete(cueKey);
cueRegistry = null;
}
}
@@ -360,7 +373,7 @@ shaka.text.UITextDisplayer = class {
if (!cueRegistry) {
// The cue has to be made!
this.createCue_(cue, parents);
cueRegistry = this.currentCuesMap_.get(cue);
cueRegistry = this.currentCuesMap_.get(cueKey);
wrapper = cueRegistry.wrapper;
updateDOM = true;
} else if (!this.isElementUnderTextContainer_(wrapper)) {
@@ -399,7 +412,15 @@ shaka.text.UITextDisplayer = class {
}
});
for (const cue of toPlant) {
const cueRegistry = this.currentCuesMap_.get(cue);
let cueRegistry = this.currentCuesMap_.get(cue);
if (!cueRegistry &&
this.config_.positionArea != shaka.config.PositionArea.DEFAULT) {
for (const key of this.currentCuesMap_.keys()) {
if (shaka.text.Cue.equal(cue, key)) {
cueRegistry = this.currentCuesMap_.get(key);
}
}
}
goog.asserts.assert(cueRegistry, 'cueRegistry should exist.');
if (cueRegistry.regionElement) {
if (cueRegistry.regionElement.contains(container)) {
@@ -445,9 +466,15 @@ shaka.text.UITextDisplayer = class {
}
}
let cues = this.cues_;
if (this.config_ &&
this.config_.positionArea != shaka.config.PositionArea.DEFAULT) {
cues = cues.map((cue) => this.processCueStyle_(cue));
}
// Update the cues.
this.updateCuesRecursive_(
this.cues_, this.textContainer_, currentTime, /* parents= */ []);
cues, this.textContainer_, currentTime, /* parents= */ []);
if (goog.DEBUG) {
// Previously, we had an issue (#2076) where cues sometimes were not
@@ -466,6 +493,55 @@ shaka.text.UITextDisplayer = class {
}
}
/** @private */
processCueStyle_(cue) {
goog.asserts.assert(
this.config_.positionArea !== shaka.config.PositionArea.DEFAULT,
'processCueStyle_ is intended to use on non default positioning');
const modifiedCue = cue.clone();
shaka.text.Utils.resetCuePositioning(modifiedCue);
modifiedCue.region = shaka.text.UITextDisplayer.CustomRegion_.value();
switch (this.config_.positionArea) {
case shaka.config.PositionArea.TOP_LEFT:
modifiedCue.textAlign = shaka.text.Cue.textAlign.LEFT;
modifiedCue.displayAlign = shaka.text.Cue.displayAlign.BEFORE;
break;
case shaka.config.PositionArea.TOP_CENTER:
modifiedCue.textAlign = shaka.text.Cue.textAlign.CENTER;
modifiedCue.displayAlign = shaka.text.Cue.displayAlign.BEFORE;
break;
case shaka.config.PositionArea.TOP_RIGHT:
modifiedCue.textAlign = shaka.text.Cue.textAlign.RIGHT;
modifiedCue.displayAlign = shaka.text.Cue.displayAlign.BEFORE;
break;
case shaka.config.PositionArea.CENTER_LEFT:
modifiedCue.textAlign = shaka.text.Cue.textAlign.LEFT;
modifiedCue.displayAlign = shaka.text.Cue.displayAlign.CENTER;
break;
case shaka.config.PositionArea.CENTER:
modifiedCue.textAlign = shaka.text.Cue.textAlign.CENTER;
modifiedCue.displayAlign = shaka.text.Cue.displayAlign.CENTER;
break;
case shaka.config.PositionArea.CENTER_RIGHT:
modifiedCue.textAlign = shaka.text.Cue.textAlign.RIGHT;
modifiedCue.displayAlign = shaka.text.Cue.displayAlign.CENTER;
break;
case shaka.config.PositionArea.BOTTOM_LEFT:
modifiedCue.textAlign = shaka.text.Cue.textAlign.LEFT;
modifiedCue.displayAlign = shaka.text.Cue.displayAlign.AFTER;
break;
case shaka.config.PositionArea.BOTTOM_CENTER:
modifiedCue.textAlign = shaka.text.Cue.textAlign.CENTER;
modifiedCue.displayAlign = shaka.text.Cue.displayAlign.AFTER;
break;
case shaka.config.PositionArea.BOTTOM_RIGHT:
modifiedCue.textAlign = shaka.text.Cue.textAlign.RIGHT;
modifiedCue.displayAlign = shaka.text.Cue.displayAlign.AFTER;
break;
}
return modifiedCue;
}
/**
* Compute a unique internal id:
* Regions can reuse the id but have different dimensions, we need to
@@ -1037,3 +1113,16 @@ shaka.text.UITextDisplayer = class {
return null;
}
};
/**
* @private {!shaka.util.Lazy<!shaka.text.CueRegion>}
*/
shaka.text.UITextDisplayer.CustomRegion_ = new shaka.util.Lazy(() => {
const region = new shaka.text.CueRegion();
region.id = 'shaka-custom-region';
region.height = 90;
region.width = 90;
region.viewportAnchorX = 5;
region.viewportAnchorY = 5;
return region;
});