/**
* The copyright in this software is being made available under the BSD License,
* included below. This software may be subject to other third party and contributor
* rights, including patent rights, and no such rights are granted under this license.
*
* Copyright (c) 2013, Dash Industry Forum.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation and/or
* other materials provided with the distribution.
* * Neither the name of Dash Industry Forum nor the names of its
* contributors may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
import Constants from '../constants/Constants.js';
import EventBus from '../../core/EventBus.js';
import Events from '../../core/events/Events.js';
import MediaPlayerEvents from '../../streaming/MediaPlayerEvents.js';
import FactoryMaker from '../../core/FactoryMaker.js';
import Debug from '../../core/Debug.js';
import Utils from '../../core/Utils.js';
import {CueIntervalTree} from './CueIntervalTree.js';
import {renderHTML} from 'imsc';
const CUE_PROPS_TO_COMPARE = [
'text',
'align',
'fontSize',
'id',
'isd',
'line',
'lineAlign',
'lineHeight',
'linePadding',
'position',
'positionAlign',
'region',
'size',
'snapToLines',
'vertical',
];
function TextTracks(config) {
const context = this.context;
const eventBus = EventBus(context).getInstance();
const videoModel = config.videoModel;
const streamInfo = config.streamInfo;
const settings = config.settings;
let instance,
logger,
Cue,
textTrackInfos,
nativeTexttracks,
currentTrackIdx,
actualVideoLeft,
actualVideoTop,
actualVideoWidth,
actualVideoHeight,
captionContainer,
vttCaptionContainer,
videoSizeCheckInterval,
fullscreenAttribute,
displayCCOnTop,
previousISDState,
topZIndex,
resizeObserver,
hasRequestAnimationFrame,
currentCaptionEventCue;
/**
* Data about cues for each track.
* @type {Map<TextTrack, TrackCueData>}
*/
const tracksCueData = new Map();
function setup() {
logger = Debug(context).getInstance().getLogger(instance);
}
function initialize() {
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
return;
}
Cue = window.VTTCue || window.TextTrackCue;
textTrackInfos = [];
nativeTexttracks = [];
currentTrackIdx = -1;
actualVideoLeft = 0;
actualVideoTop = 0;
actualVideoWidth = 0;
actualVideoHeight = 0;
captionContainer = null;
vttCaptionContainer = null;
videoSizeCheckInterval = null;
displayCCOnTop = false;
topZIndex = 2147483647;
previousISDState = null;
hasRequestAnimationFrame = ('requestAnimationFrame' in window);
tracksCueData.clear();
if (document.fullscreenElement !== undefined) {
fullscreenAttribute = 'fullscreenElement'; // Standard and Edge
} else if (document.webkitIsFullScreen !== undefined) {
fullscreenAttribute = 'webkitIsFullScreen'; // Chrome and Safari (and Edge)
} else if (document.msFullscreenElement) { // IE11
fullscreenAttribute = 'msFullscreenElement';
} else if (document.mozFullScreen) { // Firefox
fullscreenAttribute = 'mozFullScreen';
}
}
function getStreamId() {
return streamInfo.id;
}
function createTracks() {
//Sort in same order as in manifest
textTrackInfos.sort(function (a, b) {
return a.index - b.index;
});
captionContainer = videoModel.getTTMLRenderingDiv();
vttCaptionContainer = videoModel.getVttRenderingDiv();
let defaultIndex = -1;
for (let i = 0; i < textTrackInfos.length; i++) {
const nativeTexttrack = _createNativeTextrackElement(textTrackInfos[i]);
//used to remove tracks from video element when added manually
nativeTexttracks.push(nativeTexttrack);
if (textTrackInfos[i].defaultTrack) {
// track.default is an object property identifier that is a reserved word
nativeTexttrack.default = true;
defaultIndex = i;
}
const textTrack = getTrackByIdx(i);
if (textTrack) {
//each time a track is created, its mode should be showing by default
//sometime, it's not on Chrome
textTrack.mode = Constants.TEXT_SHOWING;
if (captionContainer && (textTrackInfos[i].isTTML || textTrackInfos[i].isEmbedded)) {
textTrack.renderingType = 'html';
} else {
textTrack.renderingType = 'default';
}
// Initialize the track data for the newly created track
tracksCueData.set(textTrack, {
allCues: new CueIntervalTree(),
lastCueWindowUpdate: -Infinity,
activeCues: []
});
}
addCaptions(i, 0, textTrackInfos[i].captionData);
eventBus.trigger(MediaPlayerEvents.TEXT_TRACK_ADDED);
}
//set current track index in textTrackQueue array
setCurrentTrackIdx.call(this, defaultIndex);
if (defaultIndex >= 0) {
let onMetadataLoaded = function () {
const track = getTrackByIdx(defaultIndex);
if (track && track.renderingType === 'html') {
checkVideoSize.call(this, track, true);
}
eventBus.off(MediaPlayerEvents.PLAYBACK_METADATA_LOADED, onMetadataLoaded, this);
};
eventBus.on(MediaPlayerEvents.PLAYBACK_METADATA_LOADED, onMetadataLoaded, this);
for (let idx = 0; idx < textTrackInfos.length; idx++) {
const videoTextTrack = getTrackByIdx(idx);
if (videoTextTrack) {
const dispatchForManualRendering = settings.get().streaming.text.dispatchForManualRendering;
videoTextTrack.mode = (idx === defaultIndex && !dispatchForManualRendering) ? Constants.TEXT_SHOWING : Constants.TEXT_HIDDEN;
videoTextTrack.manualMode = (idx === defaultIndex) ? Constants.TEXT_SHOWING : Constants.TEXT_HIDDEN;
}
}
}
eventBus.trigger(Events.TEXT_TRACKS_QUEUE_INITIALIZED, {
index: currentTrackIdx,
tracks: textTrackInfos,
streamId: streamInfo.id
});
}
function _createNativeTextrackElement(element) {
const kind = element.kind;
const label = element.id !== undefined ? element.id : element.lang;
const lang = element.lang;
const isTTML = element.isTTML;
const isEmbedded = element.isEmbedded;
const track = videoModel.addTextTrack(kind, label, lang, isTTML, isEmbedded);
return track;
}
function addTextTrackInfo(textTrackInfoVO) {
textTrackInfos.push(textTrackInfoVO);
}
/**
* Updates all native `TextTrack`s with cues within a window around currentTime.
* Only actually updates the TextTrack periodically, according to bufferPruningInterval setting.
*
* @param {number} currentTime - Current playback time
* @param {boolean} forceUpdate - Force an update regardless of time since last update
*/
function updateTextTrackWindow(currentTime, forceUpdate = false) {
const trackInfos = getTextTrackInfos();
const customVttRenderingEnabled = settings.get().streaming.text.webvtt.customRenderingEnabled;
// Iterate over all tracks and update those that need native rendering
for (let trackIdx = 0; trackIdx < trackInfos.length; trackIdx++) {
const trackInfo = trackInfos[trackIdx];
const track = getTrackByIdx(trackIdx);
const cueData = tracksCueData.get(track);
if (!track || !cueData) {
continue;
}
// Skip updates for VTT tracks that use custom rendering
if (!trackInfo.isEmbedded && customVttRenderingEnabled) {
continue;
}
const { bufferToKeep, bufferPruningInterval } = settings.get().streaming.buffer;
const now = Date.now();
const lastUpdate = cueData.lastCueWindowUpdate;
const secondsSinceLastUpdate = (now - lastUpdate) / 1000;
// Only update if enough time has passed or if this is a forced update
if (secondsSinceLastUpdate < bufferPruningInterval && !forceUpdate) {
continue;
}
// Calculate window based on buffer settings with safety margin and adjusted for playback rate
const playbackRate = videoModel.getPlaybackRate() || 1;
const windowStart = Math.max(0, currentTime - (bufferToKeep / playbackRate));
const windowEnd = currentTime + (2 * bufferPruningInterval / playbackRate);
// Clear existing cues from TextTrack
if (track.cues) {
while (track.cues.length > 0) {
track.removeCue(track.cues[0]);
}
}
// Add to TextTrack only cues that are within the current window
if (track.mode !== Constants.TEXT_DISABLED) {
const windowCues = cueData.allCues.findCuesInRange(windowStart, windowEnd);
windowCues.forEach(cue => {
track.addCue(cue);
});
}
cueData.lastCueWindowUpdate = now;
}
}
function getVideoVisibleVideoSize(viewWidth, viewHeight, videoWidth, videoHeight, aspectRatio, use80Percent) {
const viewAspectRatio = viewWidth / viewHeight;
const videoAspectRatio = videoWidth / videoHeight;
let videoPictureWidth = 0;
let videoPictureHeight = 0;
if (viewAspectRatio > videoAspectRatio) {
videoPictureHeight = viewHeight;
videoPictureWidth = (videoPictureHeight / videoHeight) * videoWidth;
} else {
videoPictureWidth = viewWidth;
videoPictureHeight = (videoPictureWidth / videoWidth) * videoHeight;
}
let videoPictureXAspect = 0;
let videoPictureYAspect = 0;
let videoPictureWidthAspect = 0;
let videoPictureHeightAspect = 0;
const videoPictureAspect = videoPictureWidth / videoPictureHeight;
if (videoPictureAspect > aspectRatio) {
videoPictureHeightAspect = videoPictureHeight;
videoPictureWidthAspect = videoPictureHeight * aspectRatio;
} else {
videoPictureWidthAspect = videoPictureWidth;
videoPictureHeightAspect = videoPictureWidth / aspectRatio;
}
videoPictureXAspect = (viewWidth - videoPictureWidthAspect) / 2;
videoPictureYAspect = (viewHeight - videoPictureHeightAspect) / 2;
if (use80Percent) {
return {
x: videoPictureXAspect + (videoPictureWidthAspect * 0.1),
y: videoPictureYAspect + (videoPictureHeightAspect * 0.1),
w: videoPictureWidthAspect * 0.8,
h: videoPictureHeightAspect * 0.8
}; /* Maximal picture size in videos aspect ratio */
} else {
return {
x: videoPictureXAspect,
y: videoPictureYAspect,
w: videoPictureWidthAspect,
h: videoPictureHeightAspect
}; /* Maximal picture size in videos aspect ratio */
}
}
function checkVideoSize(track, forceDrawing) {
const clientWidth = videoModel.getClientWidth();
const clientHeight = videoModel.getClientHeight();
const videoWidth = videoModel.getVideoWidth();
const videoHeight = videoModel.getVideoHeight();
const videoOffsetTop = videoModel.getVideoRelativeOffsetTop();
const videoOffsetLeft = videoModel.getVideoRelativeOffsetLeft();
if (videoWidth !== 0 && videoHeight !== 0) {
let aspectRatio = videoWidth / videoHeight;
let use80Percent = false;
if (track.isFromCEA608) {
// If this is CEA608 then use predefined aspect ratio
aspectRatio = 3.5 / 3.0;
use80Percent = true;
}
const realVideoSize = getVideoVisibleVideoSize.call(this, clientWidth, clientHeight, videoWidth, videoHeight, aspectRatio, use80Percent);
const newVideoWidth = realVideoSize.w;
const newVideoHeight = realVideoSize.h;
const newVideoLeft = realVideoSize.x;
const newVideoTop = realVideoSize.y;
if (newVideoWidth != actualVideoWidth || newVideoHeight != actualVideoHeight || newVideoLeft != actualVideoLeft || newVideoTop != actualVideoTop || forceDrawing) {
actualVideoLeft = newVideoLeft + videoOffsetLeft;
actualVideoTop = newVideoTop + videoOffsetTop;
actualVideoWidth = newVideoWidth;
actualVideoHeight = newVideoHeight;
if (captionContainer) {
const containerStyle = captionContainer.style;
if (containerStyle) {
containerStyle.left = actualVideoLeft + 'px';
containerStyle.top = actualVideoTop + 'px';
containerStyle.width = actualVideoWidth + 'px';
containerStyle.height = actualVideoHeight + 'px';
containerStyle.zIndex = (fullscreenAttribute && document[fullscreenAttribute]) || displayCCOnTop ? topZIndex : null;
eventBus.trigger(MediaPlayerEvents.CAPTION_CONTAINER_RESIZE);
}
}
// Video view has changed size, so resize any active cues
const activeCues = track.activeCues;
if (activeCues) {
const len = activeCues.length;
for (let i = 0; i < len; ++i) {
const cue = activeCues[i];
cue.scaleCue(cue);
}
}
}
}
}
function _scaleCue(activeCue) {
const videoWidth = actualVideoWidth;
const videoHeight = actualVideoHeight;
let key,
replaceValue,
valueFontSize,
valueLineHeight,
elements;
if (activeCue.cellResolution) {
const cellUnit = [videoWidth / activeCue.cellResolution[0], videoHeight / activeCue.cellResolution[1]];
if (activeCue.linePadding) {
for (key in activeCue.linePadding) {
if (activeCue.linePadding.hasOwnProperty(key)) {
const valueLinePadding = activeCue.linePadding[key];
replaceValue = (valueLinePadding * cellUnit[0]).toString();
// Compute the CellResolution unit in order to process properties using sizing (fontSize, linePadding, etc).
const elementsSpan = document.getElementsByClassName('spanPadding');
for (let i = 0; i < elementsSpan.length; i++) {
elementsSpan[i].style.cssText = elementsSpan[i].style.cssText.replace(/(padding-left\s*:\s*)[\d.,]+(?=\s*px)/gi, '$1' + replaceValue);
elementsSpan[i].style.cssText = elementsSpan[i].style.cssText.replace(/(padding-right\s*:\s*)[\d.,]+(?=\s*px)/gi, '$1' + replaceValue);
}
}
}
}
if (activeCue.fontSize) {
for (key in activeCue.fontSize) {
if (activeCue.fontSize.hasOwnProperty(key)) {
if (activeCue.fontSize[key][0] === '%') {
valueFontSize = activeCue.fontSize[key][1] / 100;
} else if (activeCue.fontSize[key][0] === 'c') {
valueFontSize = activeCue.fontSize[key][1];
}
replaceValue = (valueFontSize * cellUnit[1]).toString();
if (key !== 'defaultFontSize') {
elements = document.getElementsByClassName(key);
} else {
elements = document.getElementsByClassName('paragraph');
}
for (let j = 0; j < elements.length; j++) {
elements[j].style.cssText = elements[j].style.cssText.replace(/(font-size\s*:\s*)[\d.,]+(?=\s*px)/gi, '$1' + replaceValue);
}
}
}
if (activeCue.lineHeight) {
for (key in activeCue.lineHeight) {
if (activeCue.lineHeight.hasOwnProperty(key)) {
if (activeCue.lineHeight[key][0] === '%') {
valueLineHeight = activeCue.lineHeight[key][1] / 100;
} else if (activeCue.fontSize[key][0] === 'c') {
valueLineHeight = activeCue.lineHeight[key][1];
}
replaceValue = (valueLineHeight * cellUnit[1]).toString();
elements = document.getElementsByClassName(key);
for (let k = 0; k < elements.length; k++) {
elements[k].style.cssText = elements[k].style.cssText.replace(/(line-height\s*:\s*)[\d.,]+(?=\s*px)/gi, '$1' + replaceValue);
}
}
}
}
}
}
if (activeCue.isd) {
let htmlCaptionDiv = document.getElementById(activeCue.cueID);
if (htmlCaptionDiv) {
captionContainer.removeChild(htmlCaptionDiv);
}
_renderCaption(activeCue);
}
}
function _resolveImageSrc(cue, src) {
const imsc1ImgUrnTester = /^(urn:)(mpeg:[a-z0-9][a-z0-9-]{0,31}:)(subs:)([0-9]+)$/;
const smpteImgUrnTester = /^#(.*)$/;
if (imsc1ImgUrnTester.test(src)) {
const match = imsc1ImgUrnTester.exec(src);
const imageId = parseInt(match[4], 10) - 1;
const imageData = btoa(cue.images[imageId]);
const imageSrc = 'data:image/png;base64,' + imageData;
return imageSrc;
} else if (smpteImgUrnTester.test(src)) {
const match = smpteImgUrnTester.exec(src);
const imageId = match[1];
const imageSrc = 'data:image/png;base64,' + cue.embeddedImages[imageId];
return imageSrc;
} else {
return src;
}
}
function _renderCaption(cue) {
if (captionContainer) {
clearCaptionContainer.call(this);
const finalCue = document.createElement('div');
captionContainer.appendChild(finalCue);
previousISDState = renderHTML(
cue.isd,
finalCue,
function (src) {
return _resolveImageSrc(cue, src)
},
captionContainer.clientHeight,
captionContainer.clientWidth,
settings.get().streaming.text.imsc.displayForcedOnlyMode,
function (err) {
logger.info('renderCaption :', err) /*TODO: add ErrorHandler management*/
},
previousISDState,
settings.get().streaming.text.imsc.enableRollUp
);
finalCue.id = cue.cueID;
eventBus.trigger(MediaPlayerEvents.CAPTION_RENDERED, { captionDiv: finalCue, currentTrackIdx });
}
}
/**
* Finds an existing cue in the interval tree that can be extended with the new cue.
* Looks for cues that are adjacent and have identical content.
*
* @param {TextTrackCue} newCue - The new cue to potentially extend
* @param {CueIntervalTree} intervalTree - The interval tree containing existing cues
* @returns {TextTrackCue|null} The cue to extend, or null if no suitable cue found
*/
function _findCueToExtend(newCue, intervalTree) {
// Find cues that might be adjacent to the new cue
const adjacentCues = intervalTree.findCuesInRange(
newCue.startTime - 0.1, // Small buffer for floating point precision
newCue.endTime + 0.1
);
for (const existingCue of adjacentCues) {
// Check if cues are adjacent and have identical content
if (_areCuesAdjacent(newCue, existingCue) && _cuesContentAreEqual(newCue, existingCue, CUE_PROPS_TO_COMPARE)) {
return existingCue;
} else if (_areCuesAdjacent(existingCue, newCue) && _cuesContentAreEqual(newCue, existingCue, CUE_PROPS_TO_COMPARE)) {
return existingCue;
}
}
return null;
}
// Check that a new cue immediately follows the previous cue
function _areCuesAdjacent(cue, prevCue) {
if (!prevCue) {
return false;
}
// Check previous cue endTime with current cue startTime
// (should we consider an epsilon margin? for example to get around rounding issues)
return prevCue.endTime >= cue.startTime;
}
/**
* Mutates and returns an existing cue by extending it with a new cue by merging their timing.
* Assumes that the two cues are adjacent and have identical content.
*
* @param {TextTrackCue} existingCue - The existing cue to extend
* @param {TextTrackCue} newCue - The new cue to merge with
* @returns {TextTrackCue} The extended existing cue
*/
function _extendCue(existingCue, newCue) {
existingCue.startTime = Math.min(existingCue.startTime, newCue.startTime);
existingCue.endTime = Math.max(existingCue.endTime, newCue.endTime);
return existingCue;
}
function _cuesContentAreEqual(cue1, cue2, props) {
for (let i = 0; i < props.length; i++) {
const key = props[i];
if (JSON.stringify(cue1[key]) !== JSON.stringify(cue2[key])) {
return false;
}
}
return true;
}
function _resolveImagesInContents(cue, contents) {
if (!contents) {
return;
}
contents.forEach(c => {
if (c.kind && c.kind === 'image') {
c.src = _resolveImageSrc(cue, c.src);
}
_resolveImagesInContents(cue, c.contents);
});
}
/*
* Add captions to track, store for later adding, or add captions added before
*/
function addCaptions(trackIdx, timeOffset, captionData) {
const track = getTrackByIdx(trackIdx);
const cueData = tracksCueData.get(track);
const dispatchForManualRendering = settings.get().streaming.text.dispatchForManualRendering;
if (!track || !cueData) {
return;
}
if (!Array.isArray(captionData) || captionData.length === 0) {
return;
}
for (let item = 0; item < captionData.length; item++) {
let cue = null;
const currentItem = captionData[item];
track.cellResolution = currentItem.cellResolution;
track.isFromCEA608 = currentItem.isFromCEA608;
if (!isNaN(currentItem.start) && !isNaN(currentItem.end)) {
if (dispatchForManualRendering) {
cue = _handleCaptionEvents(currentItem, timeOffset);
} else if (_isHTMLCue(currentItem) && captionContainer) {
cue = _handleHtmlCaption(currentItem, timeOffset, track)
} else if (currentItem.data) {
cue = _handleNonHtmlCaption(currentItem, timeOffset, track)
}
}
if (cue) {
if (settings.get().streaming.text.extendSegmentedCues) {
const cueToExtend = _findCueToExtend(cue, cueData.allCues);
if (cueToExtend) {
cueData.allCues.removeCue(cueToExtend);
cue = _extendCue(cueToExtend, cue);
}
}
cueData.allCues.addCue(cue);
} else {
logger.error('Impossible to display subtitles. You might have missed setting a TTML rendering div via player.attachTTMLRenderingDiv(TTMLRenderingDiv)');
}
}
invalidateCueWindow();
}
function _handleCaptionEvents(currentItem, timeOffset) {
let cue = _getCueInformation(currentItem, timeOffset)
cue.onenter = function () {
// HTML Tracks don't trigger the onexit event when a new cue is entered,
// we need to manually trigger it
if (_isHTMLCue(currentItem) && currentCaptionEventCue && currentCaptionEventCue.cueID !== cue.cueID) {
_triggerCueExit(currentCaptionEventCue);
}
// We need to delete the type attribute to be able to dispatch via th event bus
delete cue.type;
currentCaptionEventCue = cue;
_triggerCueEnter(cue);
}
cue.onexit = function () {
_triggerCueExit(cue);
currentCaptionEventCue = null;
}
return cue;
}
function _triggerCueEnter(cue) {
eventBus.trigger(MediaPlayerEvents.CUE_ENTER, cue);
}
function _triggerCueExit(cue) {
eventBus.trigger(MediaPlayerEvents.CUE_EXIT, {
cueID: cue.cueID
});
}
function _handleHtmlCaption(currentItem, timeOffset, track) {
const self = this;
let cue = _getCueInformation(currentItem, timeOffset)
captionContainer.style.left = actualVideoLeft + 'px';
captionContainer.style.top = actualVideoTop + 'px';
captionContainer.style.width = actualVideoWidth + 'px';
captionContainer.style.height = actualVideoHeight + 'px';
cue.onenter = function () {
if (track.mode === Constants.TEXT_SHOWING) {
if (this.isd) {
if (hasRequestAnimationFrame) {
// Ensure everything in _renderCaption happens in the same frame
requestAnimationFrame(() => _renderCaption(this));
} else {
_renderCaption(this)
}
logger.debug('Cue enter id:' + this.cueID);
} else {
captionContainer.appendChild(this.cueHTMLElement);
_scaleCue.call(self, this);
eventBus.trigger(MediaPlayerEvents.CAPTION_RENDERED, {
captionDiv: this.cueHTMLElement,
currentTrackIdx
});
}
}
};
// For imsc subs, this could be reassigned to not do anything if there is a cue that immediately follows this one
cue.onexit = function () {
if (captionContainer) {
const divs = captionContainer.childNodes;
for (let i = 0; i < divs.length; ++i) {
if (divs[i].id === this.cueID) {
logger.debug('Cue exit id:' + divs[i].id);
captionContainer.removeChild(divs[i]);
--i;
}
}
}
};
return cue;
}
function _handleNonHtmlCaption(currentItem, timeOffset, track) {
let cue = _getCueInformation(currentItem, timeOffset)
if (currentItem.styles) {
try {
if (currentItem.styles.align !== undefined && 'align' in cue) {
cue.align = currentItem.styles.align;
}
if (currentItem.styles.line !== undefined && 'line' in cue) {
cue.line = currentItem.styles.line;
}
if (currentItem.styles.lineAlign !== undefined) {
cue.lineAlign = currentItem.styles.lineAlign;
}
if (currentItem.styles.snapToLines !== undefined && 'snapToLines' in cue) {
cue.snapToLines = currentItem.styles.snapToLines;
}
if (currentItem.styles.position !== undefined && 'position' in cue) {
cue.position = currentItem.styles.position;
}
if (currentItem.styles.positionAlign !== undefined) {
cue.positionAlign = currentItem.styles.positionAlign;
}
if (currentItem.styles.size !== undefined && 'size' in cue) {
cue.size = currentItem.styles.size;
}
} catch (e) {
logger.error(e);
}
}
cue.onenter = function () {
if (track.mode === Constants.TEXT_SHOWING) {
eventBus.trigger(MediaPlayerEvents.CAPTION_RENDERED, { currentTrackIdx });
}
};
return cue;
}
function _isHTMLCue(cue) {
return (cue.type === 'html')
}
function _getCueInformation(currentItem, timeOffset) {
if (_isHTMLCue(currentItem)) {
return _getCueInformationForHtml(currentItem, timeOffset);
}
return _getCueInformationForNonHtml(currentItem, timeOffset);
}
function _getCueInformationForHtml(currentItem, timeOffset) {
let cue = new Cue(currentItem.start + timeOffset, currentItem.end + timeOffset, '');
cue.cueHTMLElement = currentItem.cueHTMLElement;
cue.isd = currentItem.isd;
cue.images = currentItem.images;
cue.embeddedImages = currentItem.embeddedImages;
cue.cueID = currentItem.cueID;
cue.scaleCue = _scaleCue.bind(self);
//useful parameters for cea608 subtitles, not for TTML one.
cue.cellResolution = currentItem.cellResolution;
cue.lineHeight = currentItem.lineHeight;
cue.linePadding = currentItem.linePadding;
cue.fontSize = currentItem.fontSize;
// Resolve images sources
if (cue.isd) {
_resolveImagesInContents(cue, cue.isd.contents);
}
return cue;
}
function _getCueInformationForNonHtml(currentItem, timeOffset) {
let cue = new Cue(currentItem.start - timeOffset, currentItem.end - timeOffset, currentItem.data);
cue.cueID = Utils.generateUuid();
return cue;
}
function manualCueProcessing(time) {
const activeTracks = _getManualActiveTracks();
if (activeTracks && activeTracks.length > 0) {
const track = activeTracks[0];
const cueData = tracksCueData.get(track);
if (!cueData) {
return;
}
const prevActiveCues = cueData.activeCues;
const newActiveCues = cueData.allCues.findCuesAtTime(time);
const cuesToExit = prevActiveCues.filter(cue => !newActiveCues.includes(cue));
const cuesToEnter = newActiveCues.filter(cue => !prevActiveCues.includes(cue));
// Exit cues that are no longer active
cuesToExit.forEach((cue) => {
if (settings.get().streaming.text.dispatchForManualRendering) {
_triggerCueExit(cue);
} else {
_removeManualCue(cue);
}
});
// Enter cues that are newly active
cuesToEnter.forEach((cue) => {
if (settings.get().streaming.text.dispatchForManualRendering) {
_triggerCueEnter(cue);
} else {
// eslint-disable-next-line no-undef
WebVTT.processCues(window, [cue], vttCaptionContainer, cue.cueID);
}
});
// Update the activeCues for this track
cueData.activeCues = newActiveCues;
}
}
function _removeManualCue(cue) {
if (vttCaptionContainer) {
const divs = vttCaptionContainer.childNodes;
for (let i = 0; i < divs.length; ++i) {
if (divs[i].id === cue.cueID) {
vttCaptionContainer.removeChild(divs[i]);
--i;
}
}
}
}
function disableManualTracks() {
const activeTracks = _getManualActiveTracks();
if (activeTracks && activeTracks.length > 0) {
const track = activeTracks[0];
const cueData = tracksCueData.get(track);
if (!cueData) {
return;
}
// Exit all currently active cues for this track
cueData.activeCues.forEach((cue) => {
if (settings.get().streaming.text.dispatchForManualRendering) {
_triggerCueExit(cue);
} else {
_removeManualCue(cue);
}
});
// Clear the activeCues for this track
cueData.activeCues = [];
}
}
function _getManualActiveTracks() {
const tracks = videoModel.getTextTracks();
const activeTracks = []
for (const track of tracks) {
if (track.manualMode === Constants.TEXT_SHOWING) {
activeTracks.push(track);
}
}
return activeTracks;
}
function getTrackByIdx(idx) {
return idx >= 0 && textTrackInfos[idx] ?
videoModel.getTextTrack(textTrackInfos[idx].kind, textTrackInfos[idx].id, textTrackInfos[idx].lang, textTrackInfos[idx].isTTML, textTrackInfos[idx].isEmbedded) : null;
}
function getCurrentTrackIdx() {
return currentTrackIdx;
}
function getTrackIdxForId(trackId) {
let idx = -1;
for (let i = 0; i < textTrackInfos.length; i++) {
if (textTrackInfos[i].id === trackId) {
idx = i;
break;
}
}
return idx;
}
function setCurrentTrackIdx(idx) {
if (idx === currentTrackIdx) {
return;
}
currentTrackIdx = idx;
const track = getTrackByIdx(currentTrackIdx);
setCueStyleOnTrack.call(this, track);
if (videoSizeCheckInterval) {
clearInterval(videoSizeCheckInterval);
videoSizeCheckInterval = null;
}
if (track && track.renderingType === 'html') {
checkVideoSize.call(this, track, true);
if (window.ResizeObserver) {
resizeObserver = new window.ResizeObserver(() => {
checkVideoSize.call(this, track, true);
});
resizeObserver.observe(videoModel.getElement());
} else {
videoSizeCheckInterval = setInterval(checkVideoSize.bind(this, track), 500);
}
}
}
function setCueStyleOnTrack(track) {
clearCaptionContainer.call(this);
if (track) {
if (track.renderingType === 'html') {
setNativeCueStyle.call(this);
} else {
removeNativeCueStyle.call(this);
}
} else {
removeNativeCueStyle.call(this);
}
}
function cueInRange(cue, start, end, strict = true) {
if (!cue) {
return false
}
return (isNaN(start) || (strict ? cue.startTime : cue.endTime) >= start) && (isNaN(end) || (strict ? cue.endTime : cue.startTime) <= end);
}
function _deleteTrackCues(track, start, end, strict = true) {
if (!track) {
return;
}
// Handle native cues
if (track.cues && track.cues.length > 0) {
const lastIdx = track.cues.length - 1;
for (let r = lastIdx; r >= 0; r--) {
if (cueInRange(track.cues[r], start, end, strict)) {
if (track.cues[r].onexit) {
track.cues[r].onexit();
}
track.removeCue(track.cues[r]);
}
}
}
// Handle manual cues using active cues tracking
const cueData = tracksCueData.get(track);
if (cueData) {
const currentActiveCues = cueData.activeCues;
const cuesToRemove = currentActiveCues.filter(cue =>
cue.startTime >= start && cue.endTime <= end
);
cuesToRemove.forEach(cue => {
if (settings.get().streaming.text.dispatchForManualRendering) {
_triggerCueExit(cue);
} else {
_removeManualCue(cue);
}
});
// Remove from active cues array
cueData.activeCues = currentActiveCues.filter(cue => !cuesToRemove.includes(cue));
}
}
function deleteCuesFromTrackIdx(trackIdx, start, end) {
const track = getTrackByIdx(trackIdx);
if (track) {
_deleteTrackCues(track, start, end);
}
}
function deleteAllTextTracks() {
const ln = nativeTexttracks ? nativeTexttracks.length : 0;
for (let i = 0; i < ln; i++) {
const track = getTrackByIdx(i);
if (track) {
_deleteTrackCues.call(this, track, streamInfo.start, streamInfo.start + streamInfo.duration, false);
}
}
nativeTexttracks = [];
textTrackInfos = [];
if (videoSizeCheckInterval) {
clearInterval(videoSizeCheckInterval);
videoSizeCheckInterval = null;
}
if (resizeObserver && videoModel) {
resizeObserver.unobserve(videoModel.getElement());
resizeObserver = null;
}
currentTrackIdx = -1;
clearCaptionContainer.call(this);
// Clear all track data
tracksCueData.clear();
}
/**
* Invalidate the cue window for all tracks, forcing an update of the cue window on the next
* call to {@link updateTextTrackWindow}.
*/
function invalidateCueWindow() {
for (const cueData of tracksCueData.values()) {
cueData.lastCueWindowUpdate = -Infinity;
}
}
/* Set native cue style to transparent background to avoid it being displayed. */
function setNativeCueStyle() {
let styleElement = document.getElementById('native-cue-style');
if (styleElement) {
return; //Already set
}
styleElement = document.createElement('style');
styleElement.id = 'native-cue-style';
document.head.appendChild(styleElement);
const stylesheet = styleElement.sheet;
const video = videoModel.getElement();
try {
if (video) {
if (video.id) {
stylesheet.insertRule('#' + video.id + '::cue {background: transparent}', 0);
} else if (video.classList.length !== 0) {
stylesheet.insertRule('.' + video.className + '::cue {background: transparent}', 0);
} else {
stylesheet.insertRule('video::cue {background: transparent}', 0);
}
}
} catch (e) {
logger.info('' + e.message);
}
}
/* Remove the extra cue style with transparent background for native cues. */
function removeNativeCueStyle() {
const styleElement = document.getElementById('native-cue-style');
if (styleElement) {
document.head.removeChild(styleElement);
}
}
function clearCaptionContainer() {
if (captionContainer) {
while (captionContainer.firstChild) {
captionContainer.removeChild(captionContainer.firstChild);
}
}
}
function setModeForTrackIdx(idx, mode) {
const track = getTrackByIdx(idx);
if (track && track.mode !== mode) {
track.mode = mode;
}
if (track && track.manualMode !== mode) {
track.manualMode = mode;
}
}
function getCurrentTextTrackInfo() {
return textTrackInfos[currentTrackIdx];
}
function getTextTrackInfos() {
return textTrackInfos
}
instance = {
addCaptions,
addTextTrackInfo,
createTracks,
deleteAllTextTracks,
deleteCuesFromTrackIdx,
disableManualTracks,
getCurrentTrackIdx,
getCurrentTextTrackInfo,
getStreamId,
getTextTrackInfos,
getTrackIdxForId,
initialize,
manualCueProcessing,
setCurrentTrackIdx,
setModeForTrackIdx,
updateTextTrackWindow,
invalidateCueWindow,
};
setup();
return instance;
}
/**
* @typedef {Object} TrackCueData
* @property {CueIntervalTree} allCues - All cues for this track, stored in an interval tree for efficient lookup
* @property {number} lastCueWindowUpdate - Timestamp of last cue window update (for native rendering only)
* @property {Array} activeCues - Currently active cues for this track (for manual rendering only)
*/
TextTracks.__dashjs_factory_name = 'TextTracks';
export default FactoryMaker.getClassFactory(TextTracks);