mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-19 16:47:01 +03:00
6c85c8cbfc
TTML nested cues are meant to be displayed horizontally as inline elements. This fixes the rendering of these nested cues in both SimpleTextDisplayer and UITextDisplayer. In UITextDisplayer, the styles have been adjusted to lay out the nested cues horizontally rather than vertically. In SimpleTextDisplayer, the nested cues were being displayed as if they were top-level cues. This change concatenates the nested cues into a single cue displayed in the browser. This also improves comments on the poorly-named "spacer" property, which represents a line break in TTML. This fixes the rendering of "spacer" in SimpleTextDisplayer by inserting an actual newline character into the collapsed nested cues. Finally, this fixes and clarifies names used internally in UITextDisplayer. For example, there is a difference between a nested cue and leaf cue. A nested cue and a top-level cue without nested cues are both "leaf" cues, but a top-level cue is never a "nested" cue, since it is at the top level. The conflation of these names before this fix made it difficult to understand and fix the code in the first place. Closes #2760 Change-Id: I89633761d12704e253371d17e2e786c5b2ed67a7
299 lines
8.7 KiB
JavaScript
299 lines
8.7 KiB
JavaScript
/*! @license
|
|
* Shaka Player
|
|
* Copyright 2016 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
/**
|
|
* @fileoverview
|
|
* @suppress {missingRequire} TODO(b/152540451): this shouldn't be needed
|
|
*/
|
|
|
|
goog.provide('shaka.text.SimpleTextDisplayer');
|
|
|
|
goog.require('goog.asserts');
|
|
goog.require('shaka.log');
|
|
goog.require('shaka.text.Cue');
|
|
|
|
|
|
/**
|
|
* @summary
|
|
* This defines the default text displayer plugin. An instance of this
|
|
* class is used when no custom displayer is given.
|
|
*
|
|
* This class simply converts shaka.text.Cue objects to
|
|
* TextTrackCues and feeds them to the browser.
|
|
*
|
|
* @implements {shaka.extern.TextDisplayer}
|
|
* @export
|
|
*/
|
|
shaka.text.SimpleTextDisplayer = class {
|
|
/** @param {HTMLMediaElement} video */
|
|
constructor(video) {
|
|
/** @private {TextTrack} */
|
|
this.textTrack_ = null;
|
|
|
|
// TODO: Test that in all cases, the built-in CC controls in the video
|
|
// element are toggling our TextTrack.
|
|
|
|
// If the video element has TextTracks, disable them. If we see one that
|
|
// was created by a previous instance of Shaka Player, reuse it.
|
|
for (const track of Array.from(video.textTracks)) {
|
|
// NOTE: There is no API available to remove a TextTrack from a video
|
|
// element.
|
|
track.mode = 'disabled';
|
|
|
|
if (track.label == shaka.Player.TextTrackLabel) {
|
|
this.textTrack_ = track;
|
|
}
|
|
}
|
|
|
|
if (!this.textTrack_) {
|
|
// As far as I can tell, there is no observable difference between setting
|
|
// kind to 'subtitles' or 'captions' when creating the TextTrack object.
|
|
// The individual text tracks from the manifest will still have their own
|
|
// kinds which can be displayed in the app's UI.
|
|
this.textTrack_ = video.addTextTrack(
|
|
'subtitles', shaka.Player.TextTrackLabel);
|
|
}
|
|
this.textTrack_.mode = 'hidden';
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @export
|
|
*/
|
|
remove(start, end) {
|
|
// Check that the displayer hasn't been destroyed.
|
|
if (!this.textTrack_) {
|
|
return false;
|
|
}
|
|
|
|
const removeInRange = (cue) => {
|
|
const inside = cue.startTime < end && cue.endTime > start;
|
|
return inside;
|
|
};
|
|
|
|
shaka.text.SimpleTextDisplayer.removeWhere_(this.textTrack_, removeInRange);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @export
|
|
*/
|
|
append(cues) {
|
|
// Flatten nestedCues. If a cue has nested cues, their contents should be
|
|
// combined and replace the payload of the parent. However, we don't want
|
|
// to modify the array or objects passed in, since we don't technically own
|
|
// them. So we build a new array and replace certain items in it if they
|
|
// need to be flattened.
|
|
const flattenedCues = cues.map((cue) => {
|
|
if (cue.nestedCues.length) {
|
|
const payload = cue.nestedCues.map((inner) => {
|
|
if (inner.spacer) {
|
|
// This is a vertical spacer, so insert a newline.
|
|
return '\n';
|
|
} else {
|
|
// This is a real cue. Add a space after it. Extra spaces at the
|
|
// end or before a vertical spacer are removed with a Regexp below.
|
|
return inner.payload + ' ';
|
|
}
|
|
}).join('').replace(/ $/m, '');
|
|
|
|
const flatCue = cue.clone();
|
|
flatCue.nestedCues = [];
|
|
flatCue.payload = payload;
|
|
return flatCue;
|
|
} else {
|
|
return cue;
|
|
}
|
|
});
|
|
|
|
// Convert cues.
|
|
const textTrackCues = [];
|
|
const cuesInTextTrack = this.textTrack_.cues ?
|
|
Array.from(this.textTrack_.cues) : [];
|
|
|
|
for (const inCue of flattenedCues) {
|
|
// When a VTT cue spans a segment boundary, the cue will be duplicated
|
|
// into two segments.
|
|
// To avoid displaying duplicate cues, if the current textTrack cues
|
|
// list already contains the cue, skip it.
|
|
const containsCue = cuesInTextTrack.some((cueInTextTrack) => {
|
|
if (cueInTextTrack.startTime == inCue.startTime &&
|
|
cueInTextTrack.endTime == inCue.endTime &&
|
|
cueInTextTrack.text == inCue.payload) {
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
if (!containsCue) {
|
|
const cue =
|
|
shaka.text.SimpleTextDisplayer.convertToTextTrackCue_(inCue);
|
|
if (cue) {
|
|
textTrackCues.push(cue);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort the cues based on start/end times. Make a copy of the array so
|
|
// we can get the index in the original ordering. Out of order cues are
|
|
// rejected by IE/Edge. See https://bit.ly/2K9VX3s
|
|
const sortedCues = textTrackCues.slice().sort((a, b) => {
|
|
if (a.startTime != b.startTime) {
|
|
return a.startTime - b.startTime;
|
|
} else if (a.endTime != b.endTime) {
|
|
return a.endTime - b.startTime;
|
|
} else {
|
|
// The browser will display cues with identical time ranges from the
|
|
// bottom up. Reversing the order of equal cues means the first one
|
|
// parsed will be at the top, as you would expect.
|
|
// See https://github.com/google/shaka-player/issues/848 for more info.
|
|
return textTrackCues.indexOf(b) - textTrackCues.indexOf(a);
|
|
}
|
|
});
|
|
|
|
for (const cue of sortedCues) {
|
|
this.textTrack_.addCue(cue);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @export
|
|
*/
|
|
destroy() {
|
|
if (this.textTrack_) {
|
|
const removeIt = (cue) => true;
|
|
shaka.text.SimpleTextDisplayer.removeWhere_(this.textTrack_, removeIt);
|
|
|
|
// NOTE: There is no API available to remove a TextTrack from a video
|
|
// element.
|
|
this.textTrack_.mode = 'disabled';
|
|
}
|
|
|
|
this.textTrack_ = null;
|
|
return Promise.resolve();
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @export
|
|
*/
|
|
isTextVisible() {
|
|
return this.textTrack_.mode == 'showing';
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @export
|
|
*/
|
|
setTextVisibility(on) {
|
|
this.textTrack_.mode = on ? 'showing' : 'hidden';
|
|
}
|
|
|
|
/**
|
|
* @param {!shaka.extern.Cue} shakaCue
|
|
* @return {TextTrackCue}
|
|
* @private
|
|
*/
|
|
static convertToTextTrackCue_(shakaCue) {
|
|
if (shakaCue.startTime >= shakaCue.endTime) {
|
|
// IE/Edge will throw in this case.
|
|
// See issue #501
|
|
shaka.log.warning('Invalid cue times: ' + shakaCue.startTime +
|
|
' - ' + shakaCue.endTime);
|
|
return null;
|
|
}
|
|
|
|
const Cue = shaka.text.Cue;
|
|
/** @type {VTTCue} */
|
|
const vttCue = new VTTCue(shakaCue.startTime,
|
|
shakaCue.endTime,
|
|
shakaCue.payload);
|
|
|
|
// NOTE: positionAlign and lineAlign settings are not supported by Chrome
|
|
// at the moment, so setting them will have no effect.
|
|
// The bug on chromium to implement them:
|
|
// https://bugs.chromium.org/p/chromium/issues/detail?id=633690
|
|
|
|
vttCue.lineAlign = shakaCue.lineAlign;
|
|
vttCue.positionAlign = shakaCue.positionAlign;
|
|
if (shakaCue.size) {
|
|
vttCue.size = shakaCue.size;
|
|
}
|
|
|
|
try {
|
|
// Safari 10 seems to throw on align='center'.
|
|
vttCue.align = shakaCue.textAlign;
|
|
} catch (exception) {}
|
|
|
|
if (shakaCue.textAlign == 'center' && vttCue.align != 'center') {
|
|
// We want vttCue.position = 'auto'. By default, |position| is set to
|
|
// "auto". If we set it to "auto" safari will throw an exception, so we
|
|
// must rely on the default value.
|
|
vttCue.align = 'middle';
|
|
}
|
|
|
|
if (shakaCue.writingMode ==
|
|
Cue.writingMode.VERTICAL_LEFT_TO_RIGHT) {
|
|
vttCue.vertical = 'lr';
|
|
} else if (shakaCue.writingMode ==
|
|
Cue.writingMode.VERTICAL_RIGHT_TO_LEFT) {
|
|
vttCue.vertical = 'rl';
|
|
}
|
|
|
|
// snapToLines flag is true by default
|
|
if (shakaCue.lineInterpretation == Cue.lineInterpretation.PERCENTAGE) {
|
|
vttCue.snapToLines = false;
|
|
}
|
|
|
|
if (shakaCue.line != null) {
|
|
vttCue.line = shakaCue.line;
|
|
}
|
|
|
|
if (shakaCue.position != null) {
|
|
vttCue.position = shakaCue.position;
|
|
}
|
|
|
|
return vttCue;
|
|
}
|
|
|
|
/**
|
|
* Iterate over all the cues in a text track and remove all those for which
|
|
* |predicate(cue)| returns true.
|
|
*
|
|
* @param {!TextTrack} track
|
|
* @param {function(!TextTrackCue):boolean} predicate
|
|
* @private
|
|
*/
|
|
static removeWhere_(track, predicate) {
|
|
// Since |track.cues| can be null if |track.mode| is "disabled", force it to
|
|
// something other than "disabled".
|
|
//
|
|
// If the track is already showing, then we should keep it as showing. But
|
|
// if it something else, we will use hidden so that we don't "flash" cues on
|
|
// the screen.
|
|
const oldState = track.mode;
|
|
const tempState = oldState == 'showing' ? 'showing' : 'hidden';
|
|
|
|
track.mode = tempState;
|
|
|
|
goog.asserts.assert(
|
|
track.cues,
|
|
'Cues should be accessible when mode is set to "' + tempState + '".');
|
|
|
|
// Create a copy of the list to avoid errors while iterating.
|
|
for (const cue of Array.from(track.cues)) {
|
|
if (cue && predicate(cue)) {
|
|
track.removeCue(cue);
|
|
}
|
|
}
|
|
|
|
track.mode = oldState;
|
|
}
|
|
};
|