bigscreen-player
Version:
Simplified media playback for bigscreen devices.
387 lines (307 loc) • 11.5 kB
JavaScript
import { fromXML, generateISD, renderHTML } from 'smp-imsc';
import { f as findSegmentTemplate, L as LoadUrl, a as DebugTool, P as Plugins, U as Utils, D as DOMHelpers } from './main-a8416c78.js';
import 'tslib';
const SEGMENTS_BUFFER_SIZE = 3;
const LOAD_ERROR_COUNT_MAX = 3;
function IMSCSubtitles(mediaPlayer, autoStart, parentElement, mediaSources, defaultStyleOpts) {
let imscRenderOpts = transformStyleOptions(defaultStyleOpts);
let currentSegmentRendered = {};
let loadErrorCount = 0;
let segments = [];
let exampleSubtitlesElement;
let currentSubtitlesElement;
let updateInterval;
if (autoStart) {
start();
}
function hasOffset() {
const { presentationTimeOffsetInMilliseconds } = mediaSources.time();
return (
typeof presentationTimeOffsetInMilliseconds === "number" &&
isFinite(presentationTimeOffsetInMilliseconds) &&
presentationTimeOffsetInMilliseconds > 0
)
}
function calculateSegmentNumber() {
const segmentNumber = Math.floor(getCurrentTime() / mediaSources.currentSubtitlesSegmentLength());
// Add 1 as the PTO gives segment '0' relative to the presentation time.
// DASH segments use one-based indexing, so add 1 to the result of PTO.
// (Imagine PTO was 0)
if (hasOffset()) {
return segmentNumber + 1
}
return segmentNumber
}
function loadAllRequiredSegments() {
const segmentsToLoad = [];
const currentSegmentNumber = calculateSegmentNumber();
for (let offset = 0; offset < SEGMENTS_BUFFER_SIZE; offset++) {
const segmentNumber = currentSegmentNumber + offset;
const alreadyLoaded = segments.some((segment) => segment.number === segmentNumber);
if (!alreadyLoaded) {
segmentsToLoad.push(segmentNumber);
}
}
if (SEGMENTS_BUFFER_SIZE === segmentsToLoad.length) {
// This is to ensure when seeking to a point with no subtitles, don't leave previous subtitle displayed.
removeCurrentSubtitlesElement();
}
const segmentsUrlTemplate = mediaSources.currentSubtitlesSource();
const segmentsTemplate = findSegmentTemplate(segmentsUrlTemplate);
segmentsToLoad.forEach((segmentNumber) => {
loadSegment(segmentsUrlTemplate.replace(segmentsTemplate, segmentNumber), segmentNumber);
});
}
function loadSegment(url, segmentNumber) {
LoadUrl(url, {
timeout: mediaSources.subtitlesRequestTimeout(),
onLoad: (responseXML, responseText) => {
resetLoadErrorCount();
if (!responseXML && isSubtitlesWhole()) {
DebugTool.error("responseXML is invalid");
Plugins.interface.onSubtitlesXMLError({ cdn: mediaSources.currentSubtitlesCdn() });
stop();
return
}
try {
const preTrimTime = Date.now();
const xmlText = isSubtitlesWhole()
? responseText.replace(/^.*<\?xml[^?]+\?>/i, "")
: responseText.split(/<\?xml[^?]+\?>/i)[1] || responseText;
if (isSubtitlesWhole()) {
DebugTool.info(`XML trim duration: ${Date.now() - preTrimTime}`);
}
const preParseTime = Date.now();
const xml = fromXML(xmlText);
if (isSubtitlesWhole()) {
DebugTool.info(`XML parse duration: ${Date.now() - preParseTime}`);
}
const times = xml.getMediaTimeEvents();
segments.push({
xml: modifyStyling(xml),
times: times || [0],
previousSubtitleIndex: null,
number: segmentNumber,
});
if (segments.length > SEGMENTS_BUFFER_SIZE) {
pruneSegments();
}
} catch (error) {
error.name = "SubtitlesTransformError";
DebugTool.error(error);
Plugins.interface.onSubtitlesTransformError();
stop();
}
},
onError: ({ statusCode, ...rest } = {}) => {
DebugTool.error(`Failed to load subtitle data. Status code: ${statusCode}`);
loadErrorFailover({ statusCode, ...rest });
},
onTimeout: () => {
DebugTool.error("Loading subtitles timed out");
Plugins.interface.onSubtitlesTimeout({ cdn: mediaSources.currentSubtitlesCdn() });
stop();
},
});
}
function resetLoadErrorCount() {
loadErrorCount = 0;
}
function loadErrorLimit() {
loadErrorCount++;
if (loadErrorCount >= LOAD_ERROR_COUNT_MAX) {
resetLoadErrorCount();
return true
}
}
function loadErrorFailover(opts) {
const isWhole = isSubtitlesWhole();
if (isWhole || (!isWhole && loadErrorLimit())) {
stop();
segments = [];
mediaSources
.failoverSubtitles(opts)
.then(() => start())
.catch(() => DebugTool.info("No more CDNs available for subtitle failover"));
}
}
function pruneSegments() {
// Before sorting, check if we've gone back in time, so we know whether to prune from front or back of array
const seekedBack = segments[SEGMENTS_BUFFER_SIZE].number < segments[SEGMENTS_BUFFER_SIZE - 1].number;
segments.sort((someSegment, otherSegment) => someSegment.number - otherSegment.number);
if (seekedBack) {
segments.pop();
} else {
segments.splice(0, 1);
}
}
// Opts: { backgroundColour: string (css colour, hex), fontFamily: string , size: number, lineHeight: number }
function transformStyleOptions(opts) {
if (opts === undefined) return
const customStyles = {};
if (opts.backgroundColour) {
customStyles.spanBackgroundColorAdjust = { transparent: opts.backgroundColour };
}
if (opts.fontFamily) {
customStyles.fontFamily = opts.fontFamily;
}
if (opts.size > 0) {
customStyles.sizeAdjust = opts.size;
}
if (opts.lineHeight) {
customStyles.lineHeightAdjust = opts.lineHeight;
}
return customStyles
}
function removeCurrentSubtitlesElement() {
if (currentSubtitlesElement) {
DOMHelpers.safeRemoveElement(currentSubtitlesElement);
currentSubtitlesElement = undefined;
}
}
function removeExampleSubtitlesElement() {
if (exampleSubtitlesElement) {
DOMHelpers.safeRemoveElement(exampleSubtitlesElement);
exampleSubtitlesElement = undefined;
}
}
function getSegmentToRender(currentTime) {
let segment;
for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex++) {
for (let timesIndex = 0; timesIndex < segments[segmentIndex].times.length; timesIndex++) {
const lastOne = segments[segmentIndex].times.length === timesIndex + 1;
if (
currentTime >= segments[segmentIndex].times[timesIndex] &&
(lastOne || currentTime < segments[segmentIndex].times[timesIndex + 1]) &&
segments[segmentIndex].previousSubtitleIndex !== timesIndex &&
segments[segmentIndex].times[timesIndex] !== 0
) {
segment = segments[segmentIndex];
currentSegmentRendered = segments[segmentIndex];
segments[segmentIndex].previousSubtitleIndex = timesIndex;
break
}
}
}
return segment
}
function render(currentTime, xml) {
removeCurrentSubtitlesElement();
currentSubtitlesElement = document.createElement("div");
currentSubtitlesElement.id = "bsp_subtitles";
currentSubtitlesElement.style.position = "absolute";
parentElement.appendChild(currentSubtitlesElement);
renderSubtitle(
xml,
currentTime,
currentSubtitlesElement,
imscRenderOpts,
parentElement.clientHeight,
parentElement.clientWidth
);
}
function renderExample(exampleXmlString, styleOpts, safePosition = {}) {
const exampleXml = fromXML(exampleXmlString);
removeExampleSubtitlesElement();
const customStyleOptions = transformStyleOptions(styleOpts);
const exampleStyle = Utils.merge(imscRenderOpts, customStyleOptions);
exampleSubtitlesElement = document.createElement("div");
exampleSubtitlesElement.id = "subtitlesPreview";
exampleSubtitlesElement.style.position = "absolute";
const elementWidth = parentElement.clientWidth;
const elementHeight = parentElement.clientHeight;
const topPixels = ((safePosition.top || 0) / 100) * elementHeight;
const rightPixels = ((safePosition.right || 0) / 100) * elementWidth;
const bottomPixels = ((safePosition.bottom || 0) / 100) * elementHeight;
const leftPixels = ((safePosition.left || 0) / 100) * elementWidth;
const renderWidth = elementWidth - leftPixels - rightPixels;
const renderHeight = elementHeight - topPixels - bottomPixels;
exampleSubtitlesElement.style.top = `${topPixels}px`;
exampleSubtitlesElement.style.right = `${rightPixels}px`;
exampleSubtitlesElement.style.bottom = `${bottomPixels}px`;
exampleSubtitlesElement.style.left = `${leftPixels}px`;
parentElement.appendChild(exampleSubtitlesElement);
renderSubtitle(exampleXml, 1, exampleSubtitlesElement, exampleStyle, renderHeight, renderWidth);
}
function renderSubtitle(xml, currentTime, subsElement, styleOpts, renderHeight, renderWidth) {
try {
const isd = generateISD(xml, currentTime);
renderHTML(isd, subsElement, null, renderHeight, renderWidth, false, null, null, false, styleOpts);
} catch (error) {
error.name = "SubtitlesRenderError";
DebugTool.error(error);
Plugins.interface.onSubtitlesRenderError();
}
}
function modifyStyling(xml) {
if (!isSubtitlesWhole() && xml?.head?.styling) {
xml.head.styling.initials = defaultStyleOpts.initials;
}
return xml
}
function isSubtitlesWhole() {
const subtitlesUrl = mediaSources.currentSubtitlesSource();
if (typeof subtitlesUrl !== "string") {
return false
}
return findSegmentTemplate(subtitlesUrl) == null
}
function isValidTime(time) {
// A newly loaded video element reports currentTime as 0
return time > 0
}
function getCurrentTime() {
const presentationTimeInSeconds = mediaPlayer.getCurrentTime();
return hasOffset()
? presentationTimeInSeconds + mediaSources.time().presentationTimeOffsetInMilliseconds / 1000
: presentationTimeInSeconds
}
function start() {
stop();
const url = mediaSources.currentSubtitlesSource();
const isWhole = isSubtitlesWhole();
if (url && url !== "") {
if (isWhole && segments.length === 0) {
loadSegment(url);
}
updateInterval = setInterval(() => {
const time = getCurrentTime();
if (!isWhole && isValidTime(time)) {
loadAllRequiredSegments();
}
update(time);
}, 750);
}
}
function stop() {
clearInterval(updateInterval);
removeCurrentSubtitlesElement();
}
function update(currentTime) {
const segment = getSegmentToRender(currentTime);
if (segment) {
render(currentTime, segment.xml);
}
}
function customise(styleOpts, enabled) {
const customStyleOptions = transformStyleOptions(styleOpts);
imscRenderOpts = Utils.merge(imscRenderOpts, customStyleOptions);
if (enabled) {
render(getCurrentTime(), currentSegmentRendered && currentSegmentRendered.xml);
}
}
return {
start,
stop,
updatePosition: () => {},
customise,
renderExample,
clearExample: removeExampleSubtitlesElement,
tearDown: () => {
stop();
resetLoadErrorCount();
segments = undefined;
},
}
}
export { IMSCSubtitles as default };