bigscreen-player
Version:
Simplified media playback for bigscreen devices.
1,513 lines (1,388 loc) • 246 kB
JavaScript
import { __rest } from 'tslib';
/**
* Provides an enumeration of possible media states.
*/
const MediaState = {
/** Media is stopped and is not attempting to start. */
STOPPED: 0,
/** Media is paused. */
PAUSED: 1,
/** Media is playing successfully. */
PLAYING: 2,
/** Media is waiting for data (buffering). */
WAITING: 4,
/** Media has ended. */
ENDED: 5,
/** Media has thrown a fatal error. */
FATAL_ERROR: 6,
};
const LiveSupport = {
NONE: "none",
PLAYABLE: "playable",
RESTARTABLE: "restartable",
SEEKABLE: "seekable",
};
const PlaybackStrategy = {
MSE: "msestrategy",
NATIVE: "nativestrategy",
BASIC: "basicstrategy",
};
function getValues(obj) {
const values = [];
for (const key in obj) {
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
continue;
}
values.push(obj[key]);
}
return values;
}
var EntryCategory;
(function (EntryCategory) {
EntryCategory["METRIC"] = "metric";
EntryCategory["MESSAGE"] = "message";
EntryCategory["TRACE"] = "trace";
})(EntryCategory || (EntryCategory = {}));
const isMessage = (entry) => entry.category === EntryCategory.MESSAGE;
const isMetric = (entry) => entry.category === EntryCategory.METRIC;
const isTrace = (entry) => entry.category === EntryCategory.TRACE;
function isValid(data) {
const type = typeof data;
return (type === "boolean" ||
type === "number" ||
type === "string" ||
(type === "object" && Array.isArray(data) && data.every((element) => isValid(element))));
}
function isEqual(left, right) {
if (Array.isArray(left) && Array.isArray(right)) {
return left.length === right.length && left.every((element, index) => isEqual(element, right[index]));
}
return left === right;
}
function sortEntries(someEntry, otherEntry) {
return someEntry.sessionTime === otherEntry.sessionTime
? someEntry.currentElementTime - otherEntry.currentElementTime
: someEntry.sessionTime - otherEntry.sessionTime;
}
function concatArrays(someArray, otherArray) {
return [...someArray, ...otherArray];
}
const METRIC_ENTRY_THRESHOLD = 100;
class Chronicle {
constructor() {
this.sessionStartTime = Date.now();
this.currentElementTime = 0;
this.messages = [];
this.metrics = {};
this.traces = [];
this.listeners = { update: [], timeupdate: [] };
}
triggerUpdate(entry) {
this.listeners.update.forEach((callback) => callback(entry));
}
triggerTimeUpdate(seconds) {
this.listeners.timeupdate.forEach((callback) => callback(seconds));
}
timestamp(entry) {
return Object.assign(Object.assign({}, entry), { currentElementTime: this.currentElementTime, sessionTime: this.getSessionTime() });
}
pushMessage(message) {
const entry = this.timestamp(message);
this.messages.push(entry);
this.triggerUpdate(entry);
}
getCurrentElementTime() {
return this.currentElementTime;
}
setCurrentElementTime(seconds) {
this.currentElementTime = seconds;
this.triggerTimeUpdate(seconds);
}
getSessionTime() {
return Date.now() - this.sessionStartTime;
}
on(type, listener) {
this.listeners[type].push(listener);
}
off(type, listener) {
const index = this.listeners[type].indexOf(listener);
if (index === -1) {
return;
}
this.listeners[type].splice(index, 1);
}
retrieve() {
const metrics = getValues(this.metrics).reduce(concatArrays, []);
return [...this.traces, ...metrics, ...this.messages].sort(sortEntries);
}
size() {
return (this.messages.length +
this.traces.length +
getValues(this.metrics).reduce((sumSoFar, metricsForKey) => sumSoFar + metricsForKey.length, 0));
}
appendMetric(kind, data) {
if (!isValid(data)) {
throw new TypeError(`A metric value can only be a primitive type, or an array of any depth containing primitive types. Got ${typeof data}`);
}
const latest = this.getLatestMetric(kind);
if (latest && isEqual(latest.data, data)) {
return;
}
if (this.metrics[kind] == null) {
this.metrics[kind] = [];
}
const metricsForKey = this.metrics[kind];
if (metricsForKey.length + 1 === METRIC_ENTRY_THRESHOLD) {
this.trace("error", new Error(`Metric ${kind} exceeded ${METRIC_ENTRY_THRESHOLD}. Consider a more selective sample, or not storing history.`));
}
const metric = this.timestamp({ kind, data, category: EntryCategory.METRIC });
metricsForKey.push(metric);
this.triggerUpdate(metric);
}
setMetric(kind, data) {
this.metrics[kind] = [];
this.appendMetric(kind, data);
}
getLatestMetric(kind) {
var _a;
if (!((_a = this.metrics[kind]) === null || _a === void 0 ? void 0 : _a.length)) {
return null;
}
const metricsForKey = this.metrics[kind];
return metricsForKey[metricsForKey.length - 1];
}
debug(message) {
this.pushMessage({ category: EntryCategory.MESSAGE, kind: "debug", data: message });
}
info(message) {
this.pushMessage({ category: EntryCategory.MESSAGE, kind: "info", data: message });
}
trace(kind, data) {
const entry = this.timestamp({ kind, data, category: EntryCategory.TRACE });
this.traces.push(entry);
this.triggerUpdate(entry);
}
warn(message) {
this.pushMessage({ category: EntryCategory.MESSAGE, kind: "warning", data: message });
}
}
function addClass(el, className) {
if (el.classList) {
el.classList.add(className);
}
else {
el.className += ` ${className}`;
}
}
function removeClass(el, className) {
if (el.classList) {
el.classList.remove(className);
}
else {
el.className = el.className.replace(new RegExp(`(^|\\b)${className.split(" ").join("|")}(\\b|$)`, "gi"), " ");
}
}
function hasClass(el, className) {
return el.classList ? el.classList.contains(className) : new RegExp(`(^| )${className}( |$)`, "gi").test(el.className);
}
function isRGBA(rgbaString) {
return new RegExp("^#([A-Fa-f0-9]{8})$").test(rgbaString);
}
/**
* Checks that the string is an RGBA tuple and returns a RGB Tripple.
* A string that isn't an RGBA tuple will be returned to the caller.
*/
function rgbaToRGB(rgbaString) {
return isRGBA(rgbaString) ? rgbaString.slice(0, 7) : rgbaString;
}
/**
* Safely removes an element from the DOM, simply doing
* nothing if the node is detached (Has no parent).
* @param el The Element to remove
*/
function safeRemoveElement(el) {
if (el && el.parentNode) {
el.parentNode.removeChild(el);
}
}
var DOMHelpers = {
addClass,
removeClass,
hasClass,
rgbaToRGB,
isRGBA,
safeRemoveElement,
};
let appElement;
let logBox;
let logContainer;
let staticContainer;
let staticBox;
function init() {
logBox = document.createElement("div");
logContainer = document.createElement("span");
staticBox = document.createElement("div");
staticContainer = document.createElement("span");
if (appElement === undefined) {
appElement = document.body;
}
logBox.id = "logBox";
logBox.style.position = "absolute";
logBox.style.width = "63%";
logBox.style.left = "5%";
logBox.style.top = "15%";
logBox.style.bottom = "25%";
logBox.style.backgroundColor = "#1D1D1D";
logBox.style.opacity = "0.9";
logBox.style.overflow = "hidden";
staticBox.id = "staticBox";
staticBox.style.position = "absolute";
staticBox.style.width = "30%";
staticBox.style.right = "1%";
staticBox.style.top = "15%";
staticBox.style.bottom = "25%";
staticBox.style.backgroundColor = "#1D1D1D";
staticBox.style.opacity = "0.9";
staticBox.style.overflow = "hidden";
logContainer.id = "logContainer";
logContainer.style.color = "#ffffff";
logContainer.style.fontSize = "11pt";
logContainer.style.position = "absolute";
logContainer.style.bottom = "1%";
logContainer.style.left = "1%";
logContainer.style.wordWrap = "break-word";
logContainer.style.whiteSpace = "pre-line";
staticContainer.id = "staticContainer";
staticContainer.style.color = "#ffffff";
staticContainer.style.fontSize = "11pt";
staticContainer.style.wordWrap = "break-word";
staticContainer.style.left = "1%";
staticContainer.style.whiteSpace = "pre-line";
logBox.appendChild(logContainer);
staticBox.appendChild(staticContainer);
appElement.appendChild(logBox);
appElement.appendChild(staticBox);
}
function setRootElement(root) {
if (root) {
appElement = root;
}
}
function renderDynamicLogs(dynamic) {
if (logContainer)
logContainer.textContent = dynamic.join("\n");
}
function renderStaticLogs(staticLogs) {
staticLogs.forEach((entry) => renderStaticLog(entry));
}
function render({ dynamic: dynamicLogs, static: staticLogs }) {
renderDynamicLogs(dynamicLogs);
renderStaticLogs(staticLogs);
}
function renderStaticLog(entry) {
if (entry == null) {
return;
}
const { id, key, value } = entry;
const existingElement = document.querySelector(`#${id}`);
const text = `${key}: ${value}`;
if (existingElement == null) {
createNewStaticElement(entry);
return;
}
if (existingElement.textContent === text) {
return;
}
existingElement.textContent = text;
}
function createNewStaticElement({ id, key, value }) {
const staticLog = document.createElement("div");
staticLog.id = id;
staticLog.style.paddingBottom = "1%";
staticLog.style.borderBottom = "1px solid white";
staticLog.textContent = `${key}: ${value}`;
staticContainer === null || staticContainer === void 0 ? void 0 : staticContainer.appendChild(staticLog);
}
function tearDown() {
DOMHelpers.safeRemoveElement(logBox);
DOMHelpers.safeRemoveElement(staticBox);
appElement = undefined;
staticContainer = undefined;
logContainer = undefined;
logBox = undefined;
}
var DebugView = {
init,
setRootElement,
render,
tearDown,
};
const invertedMediaState = {
0: "STOPPED",
1: "PAUSED",
2: "PLAYING",
4: "WAITING",
5: "ENDED",
6: "FATAL_ERROR",
};
const DYNAMIC_ENTRY_LIMIT = 29;
function zeroPadHMS(time) {
return `${time < 10 ? "0" : ""}${time}`;
}
function zeroPadMs(milliseconds) {
return `${milliseconds < 100 ? "0" : ""}${milliseconds < 10 ? "0" : ""}${milliseconds}`;
}
function formatDate(value) {
const hours = value.getUTCHours();
const mins = value.getUTCMinutes();
const secs = value.getUTCSeconds();
return `${zeroPadHMS(hours)}:${zeroPadHMS(mins)}:${zeroPadHMS(secs)}`;
}
class DebugViewController {
constructor() {
this.isVisible = false;
this.shouldRender = false;
this.filters = [];
this.dynamicEntries = [];
this.latestMetricByKey = {};
}
isMediaState(metric) {
const { kind } = metric;
const mediaStateMetrics = ["ended", "paused", "playback-rate", "ready-state", "seeking"];
return mediaStateMetrics.includes(kind);
}
mergeMediaState(entry) {
const prevEntry = this.latestMetricByKey["media-element-state"] == null
? { category: "union", kind: "media-element-state", data: {} }
: this.latestMetricByKey["media-element-state"];
const { sessionTime, currentElementTime, kind: metricKind, data: metricData } = entry;
return Object.assign(Object.assign({}, prevEntry), { sessionTime,
currentElementTime, data: Object.assign(Object.assign({}, prevEntry.data), { [metricKind]: metricData }) });
}
isAudioQuality(metric) {
const { kind } = metric;
return ["audio-max-quality", "audio-download-quality", "audio-playback-quality"].includes(kind);
}
isVideoQuality(metric) {
const { kind } = metric;
return ["video-max-quality", "video-download-quality", "video-playback-quality"].includes(kind);
}
mergeVideoQuality(entry) {
const { sessionTime, currentElementTime, kind: metricKind, data: metricData } = entry;
const prevEntry = this.latestMetricByKey["video-quality"] == null
? { category: "union", kind: "video-quality", data: {} }
: this.latestMetricByKey["video-quality"];
const keyForKind = {
"video-max-quality": "max",
"video-download-quality": "download",
"video-playback-quality": "playback",
};
return Object.assign(Object.assign({}, prevEntry), { sessionTime,
currentElementTime, data: Object.assign(Object.assign({}, prevEntry.data), { [keyForKind[metricKind]]: metricData }) });
}
mergeAudioQuality(entry) {
const { sessionTime, currentElementTime, kind: metricKind, data: metricData } = entry;
const prevEntry = this.latestMetricByKey["audio-quality"] == null
? { category: "union", kind: "audio-quality", data: {} }
: this.latestMetricByKey["audio-quality"];
const keyForKind = {
"audio-max-quality": "max",
"audio-download-quality": "download",
"audio-playback-quality": "playback",
};
return Object.assign(Object.assign({}, prevEntry), { sessionTime,
currentElementTime, data: Object.assign(Object.assign({}, prevEntry.data), { [keyForKind[metricKind]]: metricData }) });
}
mergeMaxBitrate(entry) {
const { sessionTime, currentElementTime, kind: metricKind, data: [, bitrate], } = entry;
const prevEntry = this.latestMetricByKey["max-bitrate"] == null
? { category: "union", kind: "max-bitrate", data: { audio: 0, video: 0 } }
: this.latestMetricByKey["max-bitrate"];
const keyForKind = {
"audio-max-quality": "audio",
"video-max-quality": "video",
};
return Object.assign(Object.assign({}, prevEntry), { sessionTime,
currentElementTime, data: Object.assign(Object.assign({}, prevEntry.data), { [keyForKind[metricKind]]: bitrate }) });
}
cacheEntry(entry) {
const { category, kind } = entry;
switch (category) {
case EntryCategory.METRIC:
if (this.isMediaState(entry)) {
this.cacheStaticEntry(this.mergeMediaState(entry));
return;
}
if (kind === "audio-max-quality" || kind === "video-max-quality") {
this.cacheStaticEntry(this.mergeMaxBitrate(entry));
}
if (this.isVideoQuality(entry)) {
this.cacheStaticEntry(this.mergeVideoQuality(entry));
return;
}
if (this.isAudioQuality(entry)) {
this.cacheStaticEntry(this.mergeAudioQuality(entry));
return;
}
return this.cacheStaticEntry(entry);
case EntryCategory.MESSAGE:
case EntryCategory.TRACE:
this.cacheDynamicEntry(entry);
if (this.dynamicEntries.length >= DYNAMIC_ENTRY_LIMIT) {
this.dynamicEntries = this.dynamicEntries.slice(-DYNAMIC_ENTRY_LIMIT);
}
break;
}
}
cacheStaticEntry(entry) {
var _a;
const latestSessionTimeSoFar = (_a = this.latestMetricByKey[entry.kind]) === null || _a === void 0 ? void 0 : _a.sessionTime;
if (typeof latestSessionTimeSoFar === "number" && latestSessionTimeSoFar > entry.sessionTime) {
return;
}
this.latestMetricByKey[entry.kind] = entry;
}
cacheDynamicEntry(entry) {
if (entry.category === "time") {
this.cacheTimestamp(entry);
return;
}
this.dynamicEntries.push(entry);
}
cacheTimestamp(entry) {
const lastDynamicEntry = this.dynamicEntries[this.dynamicEntries.length - 1];
if (lastDynamicEntry == null || lastDynamicEntry.category !== "time") {
this.dynamicEntries.push(entry);
return;
}
this.dynamicEntries[this.dynamicEntries.length - 1] = entry;
}
serialiseDynamicEntry(entry) {
let formattedData;
const { category } = entry;
switch (category) {
case EntryCategory.MESSAGE:
formattedData = this.serialiseMessage(entry);
break;
case "time":
formattedData = this.serialiseTime(entry);
break;
case EntryCategory.TRACE:
formattedData = this.serialiseTrace(entry);
break;
}
const sessionTime = new Date(entry.sessionTime);
const formatedSessionTime = `${formatDate(sessionTime)}.${zeroPadMs(sessionTime.getUTCMilliseconds())}`;
return `${formatedSessionTime} - ${formattedData}`;
}
serialiseMessage(message) {
const { kind, data } = message;
switch (kind) {
case "debug":
return `Debug: ${data}`;
case "info":
return `Info: ${data}`;
case "warning":
return `Warning: ${data}`;
}
}
serialiseTime(time) {
const { currentElementTime } = time;
return `Video time: ${currentElementTime.toFixed(2)}`;
}
serialiseTrace(trace) {
var _a;
const { currentElementTime, kind, data } = trace;
switch (kind) {
case "apicall": {
const { functionName, functionArgs } = data;
const argsPart = functionArgs.length === 0 ? "" : ` with args [${functionArgs.join(", ")}]`;
return `Called '${functionName}${argsPart}'`;
}
case "buffered-ranges": {
const buffered = data.buffered.map(([start, end]) => `${start.toFixed(2)} - ${end.toFixed(2)}`).join(", ");
return `Buffered ${data.kind}: [${buffered}] at current time ${currentElementTime.toFixed(2)}`;
}
case "error":
return `${(_a = data.name) !== null && _a !== void 0 ? _a : "Error"}: ${data.message}`;
case "event": {
const { eventType, eventTarget } = data;
return `Event: '${eventType}' from ${eventTarget}`;
}
case "gap": {
const { from, to } = data;
return `Gap from ${from} to ${to}`;
}
case "session-start":
return `Playback session started at ${new Date(data).toISOString().replace("T", " ")}`;
case "session-end":
return `Playback session ended at ${new Date(data).toISOString().replace("T", " ")}`;
case "source-loaded": {
const { transferFormat, manifestType, availabilityStartTimeInMilliseconds, presentationTimeOffsetInMilliseconds, timeShiftBufferDepthInMilliseconds, } = data;
let logMessage = `Loaded ${manifestType} ${transferFormat} source.`;
if (availabilityStartTimeInMilliseconds > 0) {
logMessage += ` AST: ${new Date(availabilityStartTimeInMilliseconds).toString()}`;
}
if (timeShiftBufferDepthInMilliseconds > 0) {
logMessage += ` Time shift [s]: ${timeShiftBufferDepthInMilliseconds / 1000}`;
}
if (presentationTimeOffsetInMilliseconds > 0) {
logMessage += ` PTO [s]: ${presentationTimeOffsetInMilliseconds / 1000}.`;
}
return logMessage;
}
case "quota-exceeded": {
const { bufferLevel, time } = data;
return `Quota exceeded with buffer level ${bufferLevel} at chunk start time ${time}`;
}
case "state-change":
return `Event: ${invertedMediaState[data]}`;
}
}
serialiseStaticEntry(entry) {
const { kind } = entry;
const parsedKey = kind.replace(/-/g, " ");
const parsedValue = this.serialiseMetric(entry);
return parsedValue == null ? null : { id: kind, key: parsedKey, value: parsedValue };
}
serialiseMetric({ kind, data }) {
var _a, _b, _c, _d, _e, _f;
if (typeof data !== "object") {
return data;
}
if (kind === "media-element-state") {
const parts = [];
const isWaiting = typeof data["ready-state"] === "number" && data["ready-state"] <= 2;
if (!isWaiting && !data.paused && !data.seeking) {
parts.push(data["playback-rate"] === 0 ? "halted at rate 0" : `playing at rate ${(_a = data["playback-rate"]) === null || _a === void 0 ? void 0 : _a.toFixed(2)}`);
}
if (isWaiting) {
parts.push("waiting");
}
if (data.paused) {
parts.push("paused");
}
if (data.seeking) {
parts.push("seeking");
}
if (data.ended) {
parts.push("ended");
}
return parts.join(", ");
}
if (kind === "seekable-range") {
const [start, end] = data;
return `${formatDate(new Date(start * 1000))} - ${formatDate(new Date(end * 1000))}`;
}
if (kind === "initial-playback-time") {
const [seconds, timeline] = data;
return `${seconds}s ${timeline}`;
}
if (kind === "audio-quality" || kind === "video-quality") {
const [maxQuality] = (_b = data.max) !== null && _b !== void 0 ? _b : [];
const [downloadQuality, downloadBitrate] = (_c = data.download) !== null && _c !== void 0 ? _c : [];
const [playbackQuality, playbackBitrate] = (_d = data.playback) !== null && _d !== void 0 ? _d : [];
const playbackPart = `${((playbackBitrate !== null && playbackBitrate !== void 0 ? playbackBitrate : 0) / 1000).toFixed(0)} kbps (${playbackQuality !== null && playbackQuality !== void 0 ? playbackQuality : 0}/${maxQuality !== null && maxQuality !== void 0 ? maxQuality : 0})`;
if (playbackQuality === downloadQuality) {
return playbackPart;
}
return `${playbackPart} - downloading ${((downloadBitrate !== null && downloadBitrate !== void 0 ? downloadBitrate : 0) / 1000).toFixed(0)} kbps`;
}
if (kind === "max-bitrate") {
if (data.audio === 0) {
return null;
}
const bitratePart = ((((_e = data.audio) !== null && _e !== void 0 ? _e : 0) + ((_f = data.video) !== null && _f !== void 0 ? _f : 0)) / 1000).toFixed(0);
return `${bitratePart} kbps`;
}
return data.join(", ");
}
render() {
DebugView.render({
static: getValues(this.latestMetricByKey).map((entry) => this.serialiseStaticEntry(entry)),
dynamic: this.dynamicEntries.map((entry) => this.serialiseDynamicEntry(entry)),
});
}
setFilters(filters) {
this.filters = filters;
}
addTime({ currentElementTime, sessionTime }) {
this.cacheTimestamp({ currentElementTime, sessionTime, category: "time" });
this.shouldRender = true;
}
addEntries(entries) {
for (const entry of entries) {
if (!this.filters.every((filter) => filter(entry))) {
continue;
}
this.cacheEntry(entry);
}
this.shouldRender = true;
}
hideView() {
clearInterval(this.renderInterval);
DebugView.tearDown();
this.isVisible = false;
}
showView() {
DebugView.setRootElement(this.rootElement);
DebugView.init();
this.renderInterval = setInterval(() => {
if (this.shouldRender) {
this.render();
this.shouldRender = false;
}
}, 250);
this.isVisible = true;
}
setRootElement(el) {
DebugView.setRootElement(el);
}
}
function isError(obj) {
return obj != null && typeof obj === "object" && "name" in obj && "message" in obj;
}
const LogLevels = {
ERROR: 0,
WARN: 1,
INFO: 2,
DEBUG: 3,
};
function shouldDisplayMediaElemenEvent(entry) {
return (!isTrace(entry) ||
entry.kind !== "event" ||
entry.data.eventTarget !== "MediaElement" ||
["paused", "playing", "seeking", "seeked", "waiting"].includes(entry.data.eventType));
}
function createDebugTool() {
let chronicle = new Chronicle();
let currentLogLevel = LogLevels.INFO;
let viewController = new DebugViewController();
function init() {
chronicle = new Chronicle();
viewController = new DebugViewController();
setLogLevel(LogLevels.INFO);
chronicle.trace("session-start", Date.now());
}
function tearDown() {
if (viewController.isVisible) {
hide();
}
chronicle.trace("session-end", Date.now());
}
function getDebugLogs() {
return chronicle.retrieve();
}
function setLogLevel(newLogLevel) {
if (typeof newLogLevel !== "number") {
return;
}
viewController.setFilters(newLogLevel === LogLevels.DEBUG ? [] : [shouldDisplayMediaElemenEvent]);
currentLogLevel = newLogLevel;
}
function setRootElement(element) {
viewController.setRootElement(element);
}
function updateElementTime(seconds) {
chronicle.setCurrentElementTime(seconds);
}
function apicall(functionName, functionArgs = []) {
chronicle.trace("apicall", { functionName, functionArgs });
}
function buffered(kind, buffered) {
chronicle.trace("buffered-ranges", { kind, buffered });
}
function debug(...parts) {
if (currentLogLevel < LogLevels.DEBUG) {
return;
}
chronicle.debug(parts.join(" "));
}
function error(...parts) {
if (currentLogLevel < LogLevels.ERROR) {
return;
}
const { name, message } = parts.length === 1 && isError(parts[0]) ? parts[0] : new Error(parts.join(" "));
chronicle.trace("error", { name, message });
}
function event(eventType, eventTarget = "unknown") {
chronicle.trace("event", { eventTarget, eventType });
}
function gap(from, to) {
chronicle.trace("gap", { from, to });
}
function quotaExceeded(bufferLevel, time) {
chronicle.trace("quota-exceeded", { bufferLevel, time });
}
function info(...parts) {
if (currentLogLevel < LogLevels.INFO) {
return;
}
chronicle.info(parts.join(" "));
}
function sourceLoaded(sourceInfo) {
chronicle.trace("source-loaded", sourceInfo);
}
function statechange(value) {
chronicle.trace("state-change", value);
}
function warn(...parts) {
if (currentLogLevel < LogLevels.WARN) {
return;
}
chronicle.warn(parts.join(" "));
}
function dynamicMetric(kind, data) {
chronicle.appendMetric(kind, data);
}
function staticMetric(kind, data) {
chronicle.setMetric(kind, data);
}
function handleHistoryUpdate(change) {
viewController.addEntries([change]);
}
function handleTimeUpdate(seconds) {
viewController.addTime({ currentElementTime: seconds, sessionTime: chronicle.getSessionTime() });
}
function hide() {
viewController.hideView();
chronicle.off("update", handleHistoryUpdate);
chronicle.off("timeupdate", handleTimeUpdate);
}
function show() {
viewController.showView();
viewController.addEntries(chronicle.retrieve());
viewController.addTime({
currentElementTime: chronicle.getCurrentElementTime(),
sessionTime: chronicle.getSessionTime(),
});
chronicle.on("update", handleHistoryUpdate);
chronicle.on("timeupdate", handleTimeUpdate);
}
function toggleVisibility() {
const toggle = viewController.isVisible ? hide : show;
toggle();
}
return {
logLevels: LogLevels,
init,
tearDown,
getDebugLogs,
setLogLevel,
updateElementTime,
apicall,
buffered,
debug,
error,
event,
gap,
quotaExceeded,
info,
sourceLoaded,
statechange,
warn,
dynamicMetric,
staticMetric,
hide,
show,
setRootElement,
toggleVisibility,
};
}
const DebugTool = createDebugTool();
const ManifestType = {
STATIC: "static",
DYNAMIC: "dynamic",
};
function AllowedMediaTransitions(mediaplayer) {
const player = mediaplayer;
const MediaPlayerState = {
EMPTY: "EMPTY", // No source set
STOPPED: "STOPPED", // Source set but no playback
BUFFERING: "BUFFERING", // Not enough data to play, waiting to download more
PLAYING: "PLAYING", // Media is playing
PAUSED: "PAUSED", // Media is paused
COMPLETE: "COMPLETE", // Media has reached its end point
ERROR: "ERROR", // An error occurred
};
function canBePaused() {
const pausableStates = [MediaPlayerState.BUFFERING, MediaPlayerState.PLAYING];
return pausableStates.indexOf(player.getState()) !== -1
}
function canBeStopped() {
const unstoppableStates = [MediaPlayerState.EMPTY, MediaPlayerState.ERROR];
const stoppable = unstoppableStates.indexOf(player.getState()) === -1;
return stoppable
}
function canBeginSeek() {
const unseekableStates = [MediaPlayerState.EMPTY, MediaPlayerState.ERROR];
const state = player.getState();
const seekable = state ? unseekableStates.indexOf(state) === -1 : false;
return seekable
}
function canResume() {
return player.getState() === MediaPlayerState.PAUSED || player.getState() === MediaPlayerState.BUFFERING
}
return {
canBePaused: canBePaused,
canBeStopped: canBeStopped,
canBeginSeek: canBeginSeek,
canResume: canResume,
}
}
function LiveGlitchCurtain(parentElement) {
let curtain = document.createElement("div");
curtain.id = "liveGlitchCurtain";
curtain.style.display = "none";
curtain.style.position = "absolute";
curtain.style.top = 0;
curtain.style.left = 0;
curtain.style.right = 0;
curtain.style.bottom = 0;
curtain.style.backgroundColor = "#3c3c3c";
return {
showCurtain: () => {
curtain.style.display = "block";
parentElement.appendChild(curtain);
},
hideCurtain: () => {
curtain.style.display = "none";
},
tearDown: () => {
DOMHelpers.safeRemoveElement(curtain);
},
}
}
const STATE$1 = {
EMPTY: "EMPTY", // No source set
STOPPED: "STOPPED", // Source set but no playback
BUFFERING: "BUFFERING", // Not enough data to play, waiting to download more
PLAYING: "PLAYING", // Media is playing
PAUSED: "PAUSED", // Media is paused
COMPLETE: "COMPLETE", // Media has reached its end point
ERROR: "ERROR", // An error occurred
};
const EVENT = {
STOPPED: "stopped", // Event fired when playback is stopped
BUFFERING: "buffering", // Event fired when playback has to suspend due to buffering
PLAYING: "playing", // Event fired when starting (or resuming) playing of the media
PAUSED: "paused", // Event fired when media playback pauses
COMPLETE: "complete", // Event fired when media playback has reached the end of the media
ERROR: "error", // Event fired when an error condition occurs
STATUS: "status", // Event fired regularly during play
METADATA: "metadata", // Event fired when media element loaded the init segment(s)
SENTINEL_ENTER_BUFFERING: "sentinel-enter-buffering", // Event fired when a sentinel has to act because the device has started buffering but not reported it
SENTINEL_EXIT_BUFFERING: "sentinel-exit-buffering", // Event fired when a sentinel has to act because the device has finished buffering but not reported it
SENTINEL_PAUSE: "sentinel-pause", // Event fired when a sentinel has to act because the device has failed to pause when expected
SENTINEL_PLAY: "sentinel-play", // Event fired when a sentinel has to act because the device has failed to play when expected
SENTINEL_SEEK: "sentinel-seek", // Event fired when a sentinel has to act because the device has failed to seek to the correct location
SENTINEL_COMPLETE: "sentinel-complete", // Event fired when a sentinel has to act because the device has completed the media but not reported it
SENTINEL_PAUSE_FAILURE: "sentinel-pause-failure", // Event fired when the pause sentinel has failed twice, so it is giving up
SENTINEL_SEEK_FAILURE: "sentinel-seek-failure", // Event fired when the seek sentinel has failed twice, so it is giving up
SEEK_ATTEMPTED: "seek-attempted", // Event fired when a device using a seekfinishedemitevent modifier sets the source
SEEK_FINISHED: "seek-finished", // Event fired when a device using a seekfinishedemitevent modifier has seeked successfully
};
const TYPE = {
VIDEO: "video",
AUDIO: "audio",
LIVE_VIDEO: "live-video",
LIVE_AUDIO: "live-audio",
};
function unpausedEventCheck(event) {
return event != null && event.state && event.type !== "status" ? event.state !== STATE$1.PAUSED : undefined
}
var MediaPlayerBase = {
STATE: STATE$1,
EVENT,
TYPE,
unpausedEventCheck,
};
function LegacyPlayerAdapter(mediaSources, playbackElement, isUHD, player) {
const manifestType = mediaSources.time().manifestType;
const setSourceOpts = {
disableSentinels:
!!isUHD && manifestType === ManifestType.DYNAMIC && window.bigscreenPlayer?.overrides?.liveUhdDisableSentinels,
disableSeekSentinel: !!window.bigscreenPlayer?.overrides?.disableSeekSentinel,
};
const mediaPlayer = player;
const transitions = new AllowedMediaTransitions(mediaPlayer);
let isEnded = false;
let duration = 0;
let eventCallbacks = [];
let errorCallback;
let timeUpdateCallback;
let currentTime;
let isPaused;
let hasStartTime;
let handleErrorOnExitingSeek;
let delayPauseOnExitSeek;
let pauseOnExitSeek;
let exitingSeek;
let targetSeekToTime;
let liveGlitchCurtain;
mediaPlayer.addEventCallback(this, eventHandler);
function eventHandler(event) {
const handleEvent = {
[MediaPlayerBase.EVENT.PLAYING]: onPlaying,
[MediaPlayerBase.EVENT.PAUSED]: onPaused,
[MediaPlayerBase.EVENT.BUFFERING]: onBuffering,
[MediaPlayerBase.EVENT.SEEK_ATTEMPTED]: onSeekAttempted,
[MediaPlayerBase.EVENT.SEEK_FINISHED]: onSeekFinished,
[MediaPlayerBase.EVENT.STATUS]: onTimeUpdate,
[MediaPlayerBase.EVENT.COMPLETE]: onEnded,
[MediaPlayerBase.EVENT.ERROR]: onError,
};
if (Object.prototype.hasOwnProperty.call(handleEvent, event.type)) {
handleEvent[event.type].call(this, event);
} else {
DebugTool.info(`${getSelection()} Event:${event.type}`);
}
}
function onPlaying(event) {
// Guard against a playing event being fired when currentTime is NaN
if (parseInt(event.currentTime) !== 0 && !isNaN(event.currentTime)) {
currentTime = event.currentTime;
}
isPaused = false;
isEnded = false;
duration = duration || event.duration;
publishMediaState(MediaState.PLAYING);
}
function onPaused(_event) {
isPaused = true;
publishMediaState(MediaState.PAUSED);
}
function onBuffering(_event) {
isEnded = false;
publishMediaState(MediaState.WAITING);
}
function onTimeUpdate(event) {
DebugTool.updateElementTime(event.currentTime);
isPaused = false;
// Note: Multiple consecutive CDN failover logic
// A newly loaded video element will always report a 0 time update
// This is slightly unhelpful if we want to continue from a later point but consult currentTime as the source of truth.
if (parseInt(event.currentTime) !== 0) {
currentTime = event.currentTime;
}
// Must publish this time update before checkSeekSucceded - which could cause a pause event
// This is a device specific event ordering issue.
publishTimeUpdate();
if ((handleErrorOnExitingSeek || delayPauseOnExitSeek) && exitingSeek) {
checkSeekSucceeded(event.seekableRange.start, event.currentTime);
}
}
function onEnded() {
isPaused = true;
isEnded = true;
publishMediaState(MediaState.ENDED);
}
function onError(error) {
if (handleErrorOnExitingSeek && exitingSeek) {
restartMediaPlayer();
} else {
const mediaError = {
code: error.code || 0,
message: error.message || "unknown",
};
publishError(mediaError);
}
}
function onSeekAttempted() {
if (requiresLiveCurtain()) {
const doNotForceBeginPlaybackToEndOfWindow = {
forceBeginPlaybackToEndOfWindow: false,
};
const streaming = window.bigscreenPlayer || {
overrides: doNotForceBeginPlaybackToEndOfWindow,
};
const overrides = streaming.overrides || doNotForceBeginPlaybackToEndOfWindow;
const shouldShowCurtain =
manifestType === ManifestType.DYNAMIC && (hasStartTime || overrides.forceBeginPlaybackToEndOfWindow);
if (shouldShowCurtain) {
liveGlitchCurtain = new LiveGlitchCurtain(playbackElement);
liveGlitchCurtain.showCurtain();
}
}
}
function onSeekFinished() {
if (requiresLiveCurtain() && liveGlitchCurtain) {
liveGlitchCurtain.hideCurtain();
}
}
function publishMediaState(mediaState) {
eventCallbacks.forEach((callbackObj) => callbackObj.callback.call(callbackObj.thisArg, mediaState));
}
function publishError(mediaError) {
if (errorCallback) {
errorCallback(mediaError);
}
}
function publishTimeUpdate() {
if (timeUpdateCallback) {
timeUpdateCallback();
}
}
function setupExitSeekWorkarounds(mimeType) {
handleErrorOnExitingSeek = manifestType === ManifestType.DYNAMIC && mimeType === "application/dash+xml";
const deviceFailsPlayAfterPauseOnExitSeek = window.bigscreenPlayer?.overrides?.pauseOnExitSeek;
delayPauseOnExitSeek = handleErrorOnExitingSeek || deviceFailsPlayAfterPauseOnExitSeek;
}
function checkSeekSucceeded(seekableRangeStart, currentTime) {
const SEEK_TOLERANCE = 30;
const clampedSeekToTime = Math.max(seekableRangeStart, targetSeekToTime);
const successfullySeeked = Math.abs(currentTime - clampedSeekToTime) < SEEK_TOLERANCE;
if (successfullySeeked) {
if (pauseOnExitSeek) {
// Delay call to pause until seek has completed
// successfully for scenarios which can error upon exiting seek.
mediaPlayer.pause();
pauseOnExitSeek = false;
}
exitingSeek = false;
}
}
// Dash live streams can error on exiting seek when the start of the
// seekable range has overtaken the point where the stream was paused
// Workaround - reset the media player then do a fresh beginPlaybackFrom()
function restartMediaPlayer() {
exitingSeek = false;
pauseOnExitSeek = false;
const source = mediaPlayer.getSource();
const mimeType = mediaPlayer.getMimeType();
reset();
mediaPlayer.initialiseMedia("video", source, mimeType, playbackElement, setSourceOpts);
mediaPlayer.beginPlaybackFrom(currentTime || 0);
}
function requiresLiveCurtain() {
return !!window.bigscreenPlayer?.overrides?.showLiveCurtain
}
function reset() {
if (transitions.canBeStopped()) {
mediaPlayer.stop();
}
mediaPlayer.reset();
}
return {
transitions,
addEventCallback: (thisArg, callback) => {
eventCallbacks.push({
thisArg,
callback,
});
},
removeEventCallback: (callback) => {
const index = eventCallbacks.find((callbackObj) => callbackObj.callback === callback);
if (index !== -1) {
eventCallbacks.splice(index, 1);
}
},
addErrorCallback: (thisArg, newErrorCallback) => {
errorCallback = (event) => newErrorCallback.call(thisArg, event);
},
addTimeUpdateCallback: (thisArg, newTimeUpdateCallback) => {
timeUpdateCallback = () => newTimeUpdateCallback.call(thisArg);
},
load: (mimeType, presentationTimeInSeconds) => {
setupExitSeekWorkarounds(mimeType);
isPaused = false;
hasStartTime = presentationTimeInSeconds || presentationTimeInSeconds === 0;
mediaPlayer.initialiseMedia("video", mediaSources.currentSource(), mimeType, playbackElement, setSourceOpts);
if (
typeof mediaPlayer.beginPlaybackFrom === "function" &&
(manifestType === ManifestType.STATIC || hasStartTime)
) {
// currentTime = 0 is interpreted as play from live point by many devices
const startTimeInSeconds =
manifestType === ManifestType.DYNAMIC && presentationTimeInSeconds === 0 ? 0.1 : presentationTimeInSeconds;
currentTime = startTimeInSeconds || 0;
mediaPlayer.beginPlaybackFrom(currentTime);
} else {
mediaPlayer.beginPlayback();
}
},
play: () => {
isPaused = false;
if (delayPauseOnExitSeek && exitingSeek) {
pauseOnExitSeek = false;
} else {
if (isEnded) {
mediaPlayer.playFrom && mediaPlayer.playFrom(0);
} else if (transitions.canResume()) {
mediaPlayer.resume && mediaPlayer.resume();
} else {
mediaPlayer.playFrom && mediaPlayer.playFrom(currentTime);
}
}
},
pause: () => {
if (delayPauseOnExitSeek && exitingSeek && transitions.canBePaused()) {
pauseOnExitSeek = true;
} else {
mediaPlayer.pause();
}
},
isPaused: () => isPaused,
isEnded: () => isEnded,
getDuration: () => duration,
getPlayerElement: () => mediaPlayer.getPlayerElement && mediaPlayer.getPlayerElement(),
getSeekableRange: () => {
if (manifestType === ManifestType.STATIC) {
return {
start: 0,
end: duration,
}
}
return typeof mediaPlayer.getSeekableRange === "function" ? mediaPlayer.getSeekableRange() : null
},
setPlaybackRate: (rate) => {
if (typeof mediaPlayer.setPlaybackRate === "function") {
mediaPlayer.setPlaybackRate(rate);
}
},
getPlaybackRate: () => {
if (typeof mediaPlayer.getPlaybackRate === "function") {
return mediaPlayer.getPlaybackRate()
}
return 1
},
getCurrentTime: () => currentTime,
setCurrentTime: (presentationTimeInSeconds) => {
isEnded = false;
currentTime = presentationTimeInSeconds;
if (handleErrorOnExitingSeek || delayPauseOnExitSeek) {
targetSeekToTime = presentationTimeInSeconds;
exitingSeek = true;
pauseOnExitSeek = isPaused;
}
mediaPlayer.playFrom && mediaPlayer.playFrom(presentationTimeInSeconds);
if (isPaused && !delayPauseOnExitSeek && typeof mediaPlayer.pause === "function") {
mediaPlayer.pause();
}
},
getStrategy: () => window.bigscreenPlayer?.playbackStrategy?.match(/.+(?=strategy)/g)[0].toUpperCase(),
reset,
tearDown: () => {
mediaPlayer.removeAllEventCallbacks();
pauseOnExitSeek = false;
exitingSeek = false;
pauseOnExitSeek = false;
delayPauseOnExitSeek = false;
isPaused = true;
isEnded = false;
if (liveGlitchCurtain) {
liveGlitchCurtain.tearDown();
liveGlitchCurtain = undefined;
}
eventCallbacks = [];
errorCallback = undefined;
timeUpdateCallback = undefined;
},
}
}
const STATE = {
STOPPED: 0,
PLAYING: 1,
PAUSED: 2,
CONNECTING: 3,
BUFFERING: 4,
FINISHED: 5,
ERROR: 6,
};
function Cehtml() {
let eventCallbacks = [];
let state = MediaPlayerBase.STATE.EMPTY;
let mediaElement;
let updateInterval;
let mediaType;
let source;
let mimeType;
let deferSeekingTo;
let range;
let postBufferingState;
let seekFinished;
let count;
let timeoutHappened;
let disableSentinels;
let sentinelSeekTime;
let seekSentinelTolerance;
let sentinelInterval;
let sentinelIntervalNumber;
let timeAtLastSentinelInterval;
let sentinelTimeIsNearEnd;
let timeHasAdvanced;
const sentinelLimits = {
pause: {
maximumAttempts: 2,
successEvent: MediaPlayerBase.EVENT.SENTINEL_PAUSE,
failureEvent: MediaPlayerBase.EVENT.SENTINEL_PAUSE_FAILURE,
currentAttemptCount: 0,
},
seek: {
maximumAttempts: 2,
successEvent: MediaPlayerBase.EVENT.SENTINEL_SEEK,
failureEvent: MediaPlayerBase.EVENT.SENTINEL_SEEK_FAILURE,
currentAttemptCount: 0,
},
};
function addEventCallback(thisArg, callback) {
eventCallbacks.push({
thisArg,
callback,
});
}
function removeEventCallback(callback) {
const index = eventCallbacks.findIndex((callbackObj) => callbackObj.callback === callback);
if (index !== -1) {
eventCallbacks.splice(index, 1);
}
}
function removeAllEventCallbacks() {
eventCallbacks = [];
}
function emitEvent(eventType, eventLabels) {
const event = {
type: eventType,
currentTime: getCurrentTime(),
seekableRange: getSeekableRange(),
duration: getDuration(),
url: getSource(),
mimeType: getMimeType(),
state: getState(),
};
if (eventLabels) {
for (const key in eventLabels) {
if (eventLabels.hasOwnProperty(key)) {
event[key] = eventLabels[key];
}
}
}
eventCallbacks.forEach((callbackObj) => callbackObj.callback.call(callbackObj.thisArg, event));
}
function getClampedTime(seconds) {
const CLAMP_OFFSET_FROM_END_OF_RANGE = 1.1;
const range = getSeekableRange();
const nearToEnd = Math.max(range.end - CLAMP_OFFSET_FROM_END_OF_RANGE, range.start);
if (seconds < range.start) {
return range.start
} else if (seconds > nearToEnd) {
return nearToEnd
} else {
return seconds
}
}
function isLiveMedia() {
return mediaType === MediaPlayerBase.TYPE.LIVE_VIDEO || mediaType === MediaPlayerBase.TYPE.LIVE_AUDIO
}
function getSource() {
return source
}
function getMimeType() {
return mimeType
}
function getState() {
return state
}
function setSeekSentinelTolerance() {
const ON_DEMAND_SEEK_SENTINEL_TOLERANCE = 15;
const LIVE_SEEK_SENTINEL_TOLERANCE = 30;
seekSentinelTolerance = ON_DEMAND_SEEK_SENTINEL_TOLERANCE;
if (isLiveMedia()) {
seekSentinelTolerance = LIVE_SEEK_SENTINEL_TOLERANCE;
}
}
function initialiseMedia(type, url, mediaMimeType, sourceContainer, opts) {
opts = opts || {};
disableSentinels = opts.disableSentinels;
mediaType = type;
source = url;
mimeType = mediaMimeType;
emitSeekAttempted();
if (getState() === MediaPlayerBase.STATE.EMPTY) {
timeAtLastSentinelInterval = 0;
setSeekSentinelTolerance();
createElement();
addElementToDOM();
mediaElement.data = source;
registerEventHandlers();
toStopped();
} else {
toError("Cannot set source unless in the '" + MediaPlayerBase.STATE.EMPTY + "' state");
}
}
function resume() {
postBufferingState = MediaPlayerBase.STATE.PLAYING;
switch (getState()) {
case MediaPlayerBase.STATE.PLAYING:
case MediaPlayerBase.STATE.BUFFERING:
break
case MediaPlayerBase.STATE.PAUSED:
mediaElement.play(1);
toPlaying();
break
default:
toError("Cannot resume while in the '" + getState() + "' state");
break
}
}
function playFrom(seconds) {
postBufferingState = MediaPlayerBase.STATE.PLAYING;
sentinelLimits.seek.currentAttemptCount = 0;
switch (getState()) {
case MediaPlayerBase.STATE.BUFFERING:
deferSeekingTo = seconds;
break
case MediaPlayerBase.STATE.COMPLETE:
toBuffering();
mediaElement.stop();
playAndSetDeferredSeek(seconds);
break
case MediaPlayerBase.STATE.PLAYING:
toBuffering();
const seekResult = seekTo(seconds);
if (seekResult === false) {
toPlaying();
}
break
case MediaPlayerBase.STATE.PAUSED:
toBuffering();
seekTo(seconds);
mediaElement.play(1);
break
default:
toError("Cannot playFrom while in the '" + getState() + "' state");
break
}
}
function getDuration() {
switch (getState()) {
case MediaPlayerBase.STATE.STOPPED:
case MediaPlayerBase.STATE.ERROR:
return undefined
default:
if (isLiveMedia()) {
return Infinity
}
return getMediaDuration()
}
}
function beginPlayback() {
postBufferingState = MediaPlayerBase.STATE.PLAYING;
switch (getState()) {
case MediaPlayerBase.STATE.STOPPED:
toBuffering();
mediaElement.play(1);
break
default:
toError("Cannot beginPlayback while in the '" + getState() + "' state");
break
}
}
function beginPlaybackFrom(seconds) {
postBufferingState = MediaPlayerBase.STATE.PLAYING;
sentinelLimits.seek.currentAttemptCount = 0;
switch (getState()) {
case MediaPlayerBase.STATE.STOPPED:
// Seeking past 0 requires calling play first when media has not been loaded
toBuffering();
playAn