bigscreen-player
Version:
Simplified media playback for bigscreen devices.
476 lines (403 loc) • 14 kB
JavaScript
import { D as DOMHelpers, a as DebugTool, P as Plugins, L as LoadUrl, T as TransportControlPosition } from './main-4e864739.js';
import 'tslib';
/**
* Safely checks if an attribute exists on an element.
* Browsers < DOM Level 2 do not have 'hasAttribute'
*
* The interesting case - can be null when it isn't there or "", but then can also return "" when there is an attribute with no value.
* For subs this is good enough. There should not be attributes without values.
* @param {Element} el HTML Element
* @param {String} attribute attribute to check for
*/
function hasAttribute(el, attribute) {
return !!el.getAttribute(attribute)
}
function hasNestedTime(element) {
return !hasAttribute(element, "begin") || !hasAttribute(element, "end")
}
function TimedText(timedPieceNode, toStyleFunc) {
const start = timeStampToSeconds(timedPieceNode.getAttribute("begin"));
const end = timeStampToSeconds(timedPieceNode.getAttribute("end"));
const _node = timedPieceNode;
let htmlElementNode;
function timeStampToSeconds(timeStamp) {
const timePieces = timeStamp.split(":");
let timeSeconds = parseFloat(timePieces.pop(), 10);
if (timePieces.length) {
timeSeconds += 60 * parseInt(timePieces.pop(), 10);
}
if (timePieces.length) {
timeSeconds += 60 * 60 * parseInt(timePieces.pop(), 10);
}
return timeSeconds
}
function removeFromDomIfExpired(time) {
if (time > end || time < start) {
DOMHelpers.safeRemoveElement(htmlElementNode);
return true
}
return false
}
function addToDom(parentNode) {
const node = htmlElementNode || generateHtmlElementNode();
parentNode.appendChild(node);
}
function generateHtmlElementNode(node) {
const source = node || _node;
let localName = source.localName || source.tagName;
// We lose line breaks with nested TimePieces, so this provides similar layout
const parentNodeLocalName =
(source.parentNode && source.parentNode.localName) || (source.parentNode && source.parentNode.tagName);
if (localName === "span" && parentNodeLocalName === "p" && hasNestedTime(source.parentNode)) {
localName = "p";
}
const html = document.createElement(localName);
const style = toStyleFunc(source);
if (style) {
html.setAttribute("style", style);
html.style.cssText = style;
}
if (localName === "p") {
html.style.margin = "0px";
}
for (let i = 0, j = source.childNodes.length; i < j; i++) {
const n = source.childNodes[i];
if (n.nodeType === 3) {
html.appendChild(document.createTextNode(n.data));
} else if (n.nodeType === 1) {
html.appendChild(generateHtmlElementNode(n));
}
}
if (!node) {
htmlElementNode = html;
}
return html
}
return {
start: start,
end: end,
// TODO: can we stop this from adding/removing itself from the DOM? Just expose the 'generateNode' function? OR just generate the node at creation and have a property...
removeFromDomIfExpired: removeFromDomIfExpired,
addToDom: addToDom,
}
}
function Transformer() {
const _styles = {};
const elementToStyleMap = [
{
attribute: "tts:color",
property: "color",
},
{
attribute: "tts:backgroundColor",
property: "text-shadow",
},
{
attribute: "tts:fontStyle",
property: "font-style",
},
{
attribute: "tts:textAlign",
property: "text-align",
},
];
/**
* Safely checks if an attribute exists on an element.
* Browsers < DOM Level 2 do not have 'hasAttribute'
*
* The interesting case - can be null when it isn't there or "", but then can also return "" when there is an attribute with no value.
* For subs this is good enough. There should not be attributes without values.
* @param {Element} el HTML Element
* @param {String} attribute attribute to check for
*/
const hasAttribute = (el, attribute) => !!el.getAttribute(attribute);
function hasNestedTime(element) {
return !hasAttribute(element, "begin") || !hasAttribute(element, "end")
}
function isEBUDistribution(metadata) {
return metadata === "urn:ebu:tt:distribution:2014-01" || metadata === "urn:ebu:tt:distribution:2018-04"
}
function rgbWithOpacity(value) {
if (DOMHelpers.isRGBA(value)) {
let opacity = parseInt(value.slice(7, 9), 16) / 255;
if (isNaN(opacity)) {
opacity = 1.0;
}
value = DOMHelpers.rgbaToRGB(value);
value += "; opacity: " + opacity + ";";
}
return value
}
function elementToStyle(el) {
const styles = _styles;
const inherit = el.getAttribute("style");
let stringStyle = "";
if (inherit) {
if (styles[inherit]) {
stringStyle = styles[inherit];
} else {
return false
}
}
for (let i = 0, j = elementToStyleMap.length; i < j; i++) {
const map = elementToStyleMap[i];
let value = el.getAttribute(map.attribute);
if (value === null || value === undefined) {
continue
}
if (map.conversion) {
value = map.conversion(value);
}
if (map.attribute === "tts:backgroundColor") {
value = rgbWithOpacity(value);
value += " 2px 2px 1px";
}
if (map.attribute === "tts:color") {
value = rgbWithOpacity(value);
}
stringStyle += map.property + ": " + value + "; ";
}
return stringStyle
}
function transformXML(xml) {
try {
// Use .getElementsByTagNameNS() when parsing XML as some implementations of .getElementsByTagName() will lowercase its argument before proceding
const conformsToStandardElements = Array.prototype.slice.call(
xml.getElementsByTagNameNS("urn:ebu:tt:metadata", "conformsToStandard")
);
const isEBUTTD =
conformsToStandardElements && conformsToStandardElements.some((node) => isEBUDistribution(node.textContent));
const captionValues = {
ttml: {
namespace: "http://www.w3.org/2006/10/ttaf1",
idAttribute: "id",
},
ebuttd: {
namespace: "http://www.w3.org/ns/ttml",
idAttribute: "xml:id",
},
};
const captionStandard = isEBUTTD ? captionValues.ebuttd : captionValues.ttml;
const styles = _styles;
const styleElements = xml.getElementsByTagNameNS(captionStandard.namespace, "style");
for (let i = 0; i < styleElements.length; i++) {
const se = styleElements[i];
const id = se.getAttribute(captionStandard.idAttribute);
const style = elementToStyle(se);
if (style) {
styles[id] = style;
}
}
const body = xml.getElementsByTagNameNS(captionStandard.namespace, "body")[0];
const s = elementToStyle(body);
const ps = xml.getElementsByTagNameNS(captionStandard.namespace, "p");
const items = [];
for (let k = 0, m = ps.length; k < m; k++) {
if (hasNestedTime(ps[k])) {
const tag = ps[k];
for (let index = 0; index < tag.childNodes.length; index++) {
if (hasAttribute(tag.childNodes[index], "begin") && hasAttribute(tag.childNodes[index], "end")) {
// TODO: rather than pass a function, can't we make timedText look after it's style from this point?
items.push(TimedText(tag.childNodes[index], elementToStyle));
}
}
} else {
items.push(TimedText(ps[k], elementToStyle));
}
}
return {
baseStyle: s,
subtitlesForTime: (time) => items.filter((subtitle) => subtitle.start < time && subtitle.end > time),
}
} catch (e) {
DebugTool.info("Error transforming captions : " + e);
Plugins.interface.onSubtitlesTransformError();
}
}
return {
transformXML: transformXML,
}
}
function Renderer(id, captionsXML, mediaPlayer) {
let transformedSubtitles;
let liveItems = [];
let interval = 0;
let outputElement;
outputElement = document.createElement("div");
outputElement.id = id;
transformedSubtitles = Transformer().transformXML(captionsXML);
start();
function render() {
return outputElement
}
function start() {
if (transformedSubtitles) {
interval = setInterval(() => update(), 750);
if (outputElement) {
outputElement.setAttribute("style", transformedSubtitles.baseStyle);
outputElement.style.cssText = transformedSubtitles.baseStyle;
outputElement.style.display = "block";
}
}
}
function stop() {
if (outputElement) {
outputElement.style.display = "none";
}
cleanOldCaptions(mediaPlayer.getDuration());
clearInterval(interval);
}
function update() {
try {
if (!mediaPlayer) {
stop();
}
const time = mediaPlayer.getCurrentTime();
updateCaptions(time);
confirmCaptionsRendered();
} catch (e) {
DebugTool.info("Exception while rendering subtitles: " + e);
Plugins.interface.onSubtitlesRenderError();
}
}
function confirmCaptionsRendered() {
if (outputElement && !outputElement.hasChildNodes() && liveItems.length > 0) {
Plugins.interface.onSubtitlesRenderError();
}
}
function updateCaptions(time) {
cleanOldCaptions(time);
addNewCaptions(time);
}
function cleanOldCaptions(time) {
const live = liveItems;
for (let i = live.length - 1; i >= 0; i--) {
if (live[i].removeFromDomIfExpired(time)) {
live.splice(i, 1);
}
}
}
function addNewCaptions(time) {
const live = liveItems;
const fresh = transformedSubtitles.subtitlesForTime(time);
liveItems = live.concat(fresh);
for (let i = 0, j = fresh.length; i < j; i++) {
// TODO: Probably start adding to the DOM here rather than calling through.
fresh[i].addToDom(outputElement);
}
}
return {
render: render,
start: start,
stop: stop,
}
}
function LegacySubtitles(mediaPlayer, autoStart, parentElement, mediaSources) {
const container = document.createElement("div");
let subtitlesRenderer;
if (autoStart) {
start();
}
function loadSubtitles() {
const url = mediaSources.currentSubtitlesSource();
if (url && url !== "") {
LoadUrl(url, {
timeout: mediaSources.subtitlesRequestTimeout(),
onLoad: (responseXML) => {
if (responseXML) {
createContainer(responseXML);
} else {
DebugTool.info("Error: responseXML is invalid.");
Plugins.interface.onSubtitlesXMLError({ cdn: mediaSources.currentSubtitlesCdn() });
}
},
onError: ({ statusCode, ...rest } = {}) => {
DebugTool.info(`Error loading subtitles data: ${statusCode}`);
mediaSources
.failoverSubtitles({ statusCode, ...rest })
.then(() => loadSubtitles())
.catch(() => DebugTool.info("Failed to load from subtitles file from all available CDNs"));
},
onTimeout: () => {
DebugTool.info("Request timeout loading subtitles");
Plugins.interface.onSubtitlesTimeout({ cdn: mediaSources.currentSubtitlesCdn() });
},
});
}
}
function createContainer(xml) {
container.id = "playerCaptionsContainer";
DOMHelpers.addClass(container, "playerCaptions");
const videoHeight = parentElement.clientHeight;
container.style.position = "absolute";
container.style.bottom = "0px";
container.style.right = "0px";
container.style.fontWeight = "bold";
container.style.textAlign = "center";
container.style.textShadow = "#161616 2px 2px 1px";
container.style.color = "#ebebeb";
if (videoHeight === 1080) {
container.style.width = "1824px";
container.style.fontSize = "63px";
container.style.paddingRight = "48px";
container.style.paddingLeft = "48px";
container.style.paddingBottom = "60px";
} else {
// Assume 720 if not 1080. Styling implementation could be cleaner, but this is a quick fix for legacy subtitles
container.style.width = "1216px";
container.style.fontSize = "42px";
container.style.paddingRight = "32px";
container.style.paddingLeft = "32px";
container.style.paddingBottom = "40px";
}
// TODO: We don't need this extra Div really... can we get rid of render() and use the passed in container?
subtitlesRenderer = Renderer("playerCaptions", xml, mediaPlayer);
container.appendChild(subtitlesRenderer.render());
parentElement.appendChild(container);
}
function start() {
if (subtitlesRenderer) {
subtitlesRenderer.start();
} else {
loadSubtitles();
}
}
function stop() {
if (subtitlesRenderer) {
subtitlesRenderer.stop();
}
}
function updatePosition(transportControlPosition) {
const classes = {
controlsVisible: TransportControlPosition.CONTROLS_ONLY,
controlsWithInfoVisible: TransportControlPosition.CONTROLS_WITH_INFO,
leftCarouselVisible: TransportControlPosition.LEFT_CAROUSEL,
bottomCarouselVisible: TransportControlPosition.BOTTOM_CAROUSEL,
};
for (const cssClassName in classes) {
// eslint-disable-next-line no-prototype-builtins
if (classes.hasOwnProperty(cssClassName)) {
// Allow multiple flags to be set at once
if ((classes[cssClassName] & transportControlPosition) === classes[cssClassName]) {
DOMHelpers.addClass(container, cssClassName);
} else {
DOMHelpers.removeClass(container, cssClassName);
}
}
}
}
function tearDown() {
stop();
DOMHelpers.safeRemoveElement(container);
}
return {
start,
stop,
updatePosition,
customise: () => {},
renderExample: () => {},
clearExample: () => {},
tearDown,
}
}
export { LegacySubtitles as default };