UNPKG

bigscreen-player

Version:

Simplified media playback for bigscreen devices.

1,513 lines (1,388 loc) 246 kB
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