UNPKG

bigscreen-player

Version:

Simplified media playback for bigscreen devices.

8,628 lines 249 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();
        playAndSetDeferredSeek(seconds);
        break

      default:
        toError("Cannot beginPlayback while in the '" + getState() + "' state");
        break
    }
  }

  function pause() {
    postBufferingState = MediaPlayerBase.STATE.PAUSED;
    switch (getState()) {
      case MediaPlayerBase.STATE.BUFFERING:
      case MediaPlayerBase.STATE.PAUSED:
        break

      case MediaPlayerBase.STATE.PLAYING:
        mediaElement.play(0);
        toPaused();
        break

      default:
        toError("Cannot pause while in the '" + getState() + "' state");
        break
    }
  }

  function stop() {
    switch (getState()) {
      case MediaPlayerBase.STATE.STOPPED:
        break

      case MediaPlayerBase.STATE.BUFFERING:
      case MediaPlayerBase.STATE.PLAYING:
      case MediaPlayerBase.STATE.PAUSED:
      case MediaPlayerBase.STATE.COMPLETE:
        sentinelSeekTime = undefined;
        if (mediaElement.stop) {
          mediaElement.stop();
          toStopped();
        } else {
          toError("mediaElement.stop is not a function : failed to stop the media player");
        }
        break

      default:
        toError("Cannot stop while in the '" + getState() + "' state");
        break
    }
  }

  function reset() {
    switch (getState()) {
      case MediaPlayerBase.STATE.EMPTY:
        break

      case MediaPlayerBase.STATE.STOPPED:
      case MediaPlayerBase.STATE.ERROR:
        toEmpty();
        break

      default:
        toError("Cannot reset while in the '" + getState() + "' state");
        break
    }
  }

  function getCurrentTime() {
    switch (getState()) {
      case MediaPlayerBase.STATE.STOPPED:
      case MediaPlayerBase.STATE.ERROR:
        break

      case MediaPlayerBase.STATE.COMPLETE:
        if (range) {
          return range.end
        }
        break

      default:
        if (mediaElement) {
          return mediaElement.playPosition / 1000
        }
        break
    }
    return undefined
  }

  function getSeekableRange() {
    switch (getState()) {
      case MediaPlayerBase.STATE.STOPPED:
      case MediaPlayerBase.STATE.ERROR:
        break

      default:
        return range
    }
    return undefined
  }

  function getMediaDuration() {
    if (range) {
      return range.end
    }
    return undefined
  }

  function getPlayerElement() {
    return mediaElement
  }

  function onFinishedBuffering() {
    cacheRange();

    if (getState() !== MediaPlayerBase.STATE.BUFFERING) {
      return
    }

    if (waitingToSeek()) {
      toBuffering();
      performDeferredSeek();
    } else if (waitingToPause()) {
      toPaused();
      mediaElement.play(0);
    } else {
      toPlaying();
    }
  }

  function onDeviceError() {
    reportError("Media element error code: " + mediaElement.error);
  }

  function onDeviceBuffering() {
    if (getState() === MediaPlayerBase.STATE.PLAYING) {
      toBuffering();
    }
  }

  function onEndOfMedia() {
    if (getState() !== MediaPlayerBase.STATE.COMPLETE) {
      toComplete();
    }
  }

  function emitSeekAttempted() {
    if (getState() === MediaPlayerBase.STATE.EMPTY) {
      emitEvent(MediaPlayerBase.EVENT.SEEK_ATTEMPTED);
      seekFinished = false;
    }

    count = 0;
    timeoutHappened = false;
    if (window.bigscreenPlayer && window.bigscreenPlayer.overrides && window.bigscreenPlayer.overrides.restartTimeout) {
      setTimeout(() => {
        timeoutHappened = true;
      }, window.bigscreenPlayer.overrides.restartTimeout);
    } else {
      timeoutHappened = true;
    }
  }

  function emitSeekFinishedAtCorrectStartingPoint() {
    let isAtCorrectStartingPoint = Math.abs(getCurrentTime() - sentinelSeekTime) <= seekSentinelTolerance;

    if (sentinelSeekTime === undefined) {
      isAtCorrectStartingPoint = true;
    }

    const isPlayingAtCorrectTime = getState() === MediaPlayerBase.STATE.PLAYING && isAtCorrectStartingPoint;

    if (isPlayingAtCorrectTime && count >= 5 && timeoutHappened && !seekFinished) {
      emitEvent(MediaPlayerBase.EVENT.SEEK_FINISHED);
      seekFinished = true;
    } else if (isPlayingAtCorrectTime) {
      count++;
    } else {
      count = 0;
    }
  }

  function onStatus() {
    if (getState() === MediaPlayerBase.STATE.PLAYING) {
      emitEvent(MediaPlayerBase.EVENT.STATUS);
    }

    emitSeekFinishedAtCorrectStartingPoint();
  }

  function createElement() {
    mediaElement = document.createElement("object", "mediaPlayer");
    mediaElement.type = mimeType;
    mediaElement.style.position = "absolute";
    mediaElement.style.top = "0px";
    mediaElement.style.left = "0px";
    mediaElement.style.width = "100%";
    mediaElement.style.height = "100%";
  }

  function registerEventHandlers() {
    const DEVICE_UPDATE_PERIOD_MS = 500;

    mediaElement.onPlayStateChange = () => {
      switch (mediaElement.playState) {
        case STATE.STOPPED:
          break
        case STATE.PLAYING:
          onFinishedBuffering();
          break
        case STATE.PAUSED:
          break
        case STATE.CONNECTING:
          break
        case STATE.BUFFERING:
          onDeviceBuffering();
          break
        case STATE.FINISHED:
          onEndOfMedia();
          break
        case STATE.ERROR:
          onDeviceError();
          break
      }
    };

    updateInterval = setInterval(() => onStatus(), DEVICE_UPDATE_PERIOD_MS);
  }

  function addElementToDOM() {
    const body = document.getElementsByTagName("body")[0];
    body.insertBefore(mediaElement, body.firstChild);
  }

  function cacheRange() {
    if (mediaElement) {
      range = {
        start: 0,
        end: mediaElement.playTime / 1000,
      };
    }
  }

  function playAndSetDeferredSeek(seconds) {
    mediaElement.play(1);
    if (seconds > 0) {
      deferSeekingTo = seconds;
    }
  }

  function waitingToSeek() {
    return deferSeekingTo !== undefined
  }

  function performDeferredSeek() {
    seekTo(deferSeekingTo);
    deferSeekingTo = undefined;
  }

  function seekTo(seconds) {
    const clampedTime = getClampedTime(seconds);

    if (clampedTime !== seconds) {
      DebugTool.info(
        "playFrom " +
          seconds +
          " clamped to " +
          clampedTime +
          " - seekable range is { start: " +
          range.start +
          ", end: " +
          range.end +
          " }"
      );
    }

    sentinelSeekTime = clampedTime;
    return mediaElement.seek(clampedTime * 1000)
  }

  function waitingToPause() {
    return postBufferingState === MediaPlayerBase.STATE.PAUSED
  }

  function wipe() {
    mediaType = undefined;
    source = undefined;
    mimeType = undefined;
    sentinelSeekTime = undefined;
    range = undefined;

    if (mediaElement) {
      clearInterval(updateInterval);
      clearSentinels();
      destroyMediaElement();
    }
  }

  function destroyMediaElement() {
    delete mediaElement.onPlayStateChange;
    DOMHelpers.safeRemoveElement(mediaElement);
    mediaElement = undefined;
  }

  function reportError(errorMessage) {
    DebugTool.info(errorMessage);
    emitEvent(MediaPlayerBase.EVENT.ERROR, { errorMessage: errorMessage });
  }

  function toStopped() {
    state = MediaPlayerBase.STATE.STOPPED;
    emitEvent(MediaPlayerBase.EVENT.STOPPED);
    if (sentinelInterval) {
      clearSentinels();
    }
  }

  function toBuffering() {
    state = MediaPlayerBase.STATE.BUFFERING;
    emitEvent(MediaPlayerBase.EVENT.BUFFERING);
    setSentinels([exitBufferingSentinel]);
  }

  function toPlaying() {
    state = MediaPlayerBase.STATE.PLAYING;
    emitEvent(MediaPlayerBase.EVENT.PLAYING);
    setSentinels([shouldBeSeekedSentinel, enterCompleteSentinel, enterBufferingSentinel]);
  }

  function toPaused() {
    state = MediaPlayerBase.STATE.PAUSED;
    emitEvent(MediaPlayerBase.EVENT.PAUSED);
    setSentinels([shouldBePausedSentinel, shouldBeSeekedSentinel]);
  }

  function toComplete() {
    state = MediaPlayerBase.STATE.COMPLETE;
    emitEvent(MediaPlayerBase.EVENT.COMPLETE);
    clearSentinels();
  }

  function toEmpty() {
    wipe();
    state = MediaPlayerBase.STATE.EMPTY;
  }

  function toError(errorMessage) {
    wipe();
    state = MediaPlayerBase.STATE.ERROR;
    reportError(errorMessage);
  }

  function isNearToEnd(seconds) {
    return getDuration() - seconds <= 1
  }

  function setSentinels(sentinels) {
    if (disableSentinels) {
      return
    }

    sentinelLimits.pause.currentAttemptCount = 0;
    timeAtLastSentinelInterval = getCurrentTime();
    clearSentinels();
    sentinelIntervalNumber = 0;
    sentinelInterval = setInterval(() => {
      const newTime = getCurrentTime();
      sentinelIntervalNumber++;

      timeHasAdvanced = newTime ? newTime > timeAtLastSentinelInterval + 0.2 : false;
      sentinelTimeIsNearEnd = isNearToEnd(newTime || timeAtLastSentinelInterval);

      for (let i = 0; i < sentinels.length; i++) {
        const sentinelActionPerformed = sentinels[i].call(this);
        if (sentinelActionPerformed) {
          break
        }
      }

      timeAtLastSentinelInterval = newTime;
    }, 1100);
  }

  function clearSentinels() {
    clearInterval(sentinelInterval);
  }

  function enterBufferingSentinel() {
    const sentinelBufferingRequired = !timeHasAdvanced && !sentinelTimeIsNearEnd && sentinelIntervalNumber > 1;

    if (sentinelBufferingRequired) {
      emitEvent(MediaPlayerBase.EVENT.SENTINEL_ENTER_BUFFERING);
      toBuffering();
    }

    return sentinelBufferingRequired
  }

  function exitBufferingSentinel() {
    const sentinelExitBufferingRequired = timeHasAdvanced;

    if (sentinelExitBufferingRequired) {
      emitEvent(MediaPlayerBase.EVENT.SENTINEL_EXIT_BUFFERING);
      onFinishedBuffering();
    }

    return sentinelExitBufferingRequired
  }

  function shouldBeSeekedSentinel() {
    if (sentinelSeekTime === undefined) {
      return false
    }

    const currentTime = getCurrentTime();
    const clampedSentinelSeekTime = getClampedTime(sentinelSeekTime);
    const sentinelSeekRequired = Math.abs(clampedSentinelSeekTime - currentTime) > seekSentinelTolerance;

    let sentinelActionTaken = false;

    if (sentinelSeekRequired) {
      const mediaElement = mediaElement;

      sentinelActionTaken = nextSentinelAttempt(sentinelLimits.seek, () => {
        mediaElement.seek(clampedSentinelSeekTime * 1000);
      });
    } else if (sentinelIntervalNumber < 3) {
      sentinelSeekTime = currentTime;
    } else {
      sentinelSeekTime = undefined;
    }
    return sentinelActionTaken
  }

  function shouldBePausedSentinel() {
    const sentinelPauseRequired = timeHasAdvanced;
    let sentinelActionTaken = false;

    if (sentinelPauseRequired) {
      const mediaElement = mediaElement;

      sentinelActionTaken = nextSentinelAttempt(sentinelLimits.pause, () => {
        mediaElement.play(0);
      });
    }
    return sentinelActionTaken
  }

  function enterCompleteSentinel() {
    const sentinelCompleteRequired = !timeHasAdvanced && sentinelTimeIsNearEnd;

    if (sentinelCompleteRequired) {
      emitEvent(MediaPlayerBase.EVENT.SENTINEL_COMPLETE);
      onEndOfMedia();
    }

    return sentinelCompleteRequired
  }

  function nextSentinelAttempt(sentinelInfo, attemptFn) {
    let currentAttemptCount, maxAttemptCount;

    sentinelInfo.currentAttemptCount += 1;
    currentAttemptCount = sentinelInfo.currentAttemptCount;
    maxAttemptCount = sentinelInfo.maximumAttempts;

    if (currentAttemptCount === maxAttemptCount + 1) {
      emitEvent(sentinelInfo.failureEvent);
    }

    if (currentAttemptCount <= maxAttemptCount) {
      attemptFn();
      emitEvent(sentinelInfo.successEvent);
      return true
    }

    return false
  }

  return {
    addEventCallback: addEventCallback,
    removeEventCallback: removeEventCallback,
    removeAllEventCallbacks: removeAllEventCallbacks,
    initialiseMedia: initialiseMedia,
    resume: resume,
    playFrom: playFrom,
    beginPlayback: beginPlayback,
    beginPlaybackFrom: beginPlaybackFrom,
    pause: pause,
    stop: stop,
    reset: reset,
    getSource: getSource,
    getMimeType: getMimeType,
    getSeekableRange: getSeekableRange,
    getMediaDuration: getMediaDuration,
    getState: getState,
    getPlayerElement: getPlayerElement,
    getDuration: getDuration,
  }
}

function handlePlayPromise(playPromise) {
    if (!playPromise || typeof playPromise.catch !== "function")
        return;
    playPromise.catch((error) => {
        if (error && error.name === "AbortError") {
            return;
        }
        throw error;
    });
}

function Html5() {
  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,
    },
  };

  let eventCallbacks = [];
  let state = MediaPlayerBase.STATE.EMPTY;

  let mediaElement;
  let sourceElement;

  let trustZeroes = false;
  let ignoreNextPauseEvent = false;
  let nearEndOfMedia;
  let readyToPlayFrom;

  let mediaType;
  let source;
  let mimeType;

  let postBufferingState;
  let targetSeekTime;
  let seekFinished;

  let count;
  let timeoutHappened;

  let disableSentinels;
  let disableSeekSentinel;
  let hasSentinelTimeChangedWithinTolerance;
  let enterBufferingSentinelAttemptCount;
  let sentinelSeekTime;
  let seekSentinelTolerance;
  let sentinelInterval;
  let sentinelIntervalNumber;
  let lastSentinelTime;

  let cachedSeekableRange;
  let readyToCache = true;

  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 getDuration() {
    switch (getState()) {
      case MediaPlayerBase.STATE.STOPPED:
      case MediaPlayerBase.STATE.ERROR:
        return undefined
      default:
        if (isLiveMedia()) {
          return Infinity
        }
        return getMediaDuration()
    }
  }

  function getSource() {
    return source
  }

  function getMimeType() {
    return mimeType
  }

  function getState() {
    return state
  }

  function isLiveMedia() {
    return mediaType === MediaPlayerBase.TYPE.LIVE_VIDEO || mediaType === MediaPlayerBase.TYPE.LIVE_AUDIO
  }

  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 generateSourceElement(url, mimeType) {
    const sourceElement = document.createElement("source");

    sourceElement.src = url;
    sourceElement.type = mimeType;
    return sourceElement
  }

  function appendChildElement(to, el) {
    to.appendChild(el);
  }

  function prependChildElement(to, el) {
    if (to.childNodes.length > 0) {
      to.insertBefore(el, to.childNodes[0]);
    } else {
      to.appendChild(el);
    }
  }

  function toStopped() {
    state = MediaPlayerBase.STATE.STOPPED;
    emitEvent(MediaPlayerBase.EVENT.STOPPED);
    setSentinels([]);
  }

  function enterBufferingSentinel() {
    let sentinelShouldFire = !hasSentinelTimeChangedWithinTolerance && !nearEndOfMedia;

    if (getCurrentTime() === 0) {
      sentinelShouldFire = trustZeroes && sentinelShouldFire;
    }

    if (enterBufferingSentinelAttemptCount === undefined) {
      enterBufferingSentinelAttemptCount = 0;
    }

    if (sentinelShouldFire) {
      enterBufferingSentinelAttemptCount++;
    } else {
      enterBufferingSentinelAttemptCount = 0;
    }

    if (enterBufferingSentinelAttemptCount === 1) {
      sentinelShouldFire = false;
    }

    if (sentinelShouldFire) {
      emitEvent(MediaPlayerBase.EVENT.SENTINEL_ENTER_BUFFERING);
      toBuffering();
      /* Resetting the sentinel attempt count to zero means that the sentinel will only fire once
       even if multiple iterations result in the same conditions.
       This should not be needed as the second iteration, when the enter buffering sentinel is fired
       will cause the media player to go into the buffering state. The enter buffering sentinel is not fired
       when in buffering state
       */
      enterBufferingSentinelAttemptCount = 0;
      return true
    }

    return false
  }

  function exitBufferingSentinel() {
    function fireExitBufferingSentinel() {
      emitEvent(MediaPlayerBase.EVENT.SENTINEL_EXIT_BUFFERING);
      exitBuffering();
      return true
    }

    if (readyToPlayFrom && mediaElement.paused) {
      return fireExitBufferingSentinel()
    }

    if (hasSentinelTimeChangedWithinTolerance) {
      return fireExitBufferingSentinel()
    }

    return false
  }

  function shouldBeSeekedSentinel() {
    if (sentinelSeekTime === undefined || disableSeekSentinel) {
      return false
    }

    const currentTime = getCurrentTime();
    let sentinelActionTaken = false;

    if (Math.abs(currentTime - sentinelSeekTime) > seekSentinelTolerance) {
      sentinelActionTaken = nextSentinelAttempt(sentinelLimits.seek, () => {
        mediaElement.currentTime = sentinelSeekTime;
      });
    } else if (sentinelIntervalNumber < 3) {
      sentinelSeekTime = currentTime;
    } else {
      sentinelSeekTime = undefined;
    }

    return sentinelActionTaken
  }

  function shouldBePausedSentinel() {
    let sentinelActionTaken = false;

    if (hasSentinelTimeChangedWithinTolerance) {
      sentinelActionTaken = nextSentinelAttempt(sentinelLimits.pause, () => {
        pauseMediaElement();
      });
    }

    return sentinelActionTaken
  }

  function nextSentinelAttempt(sentinelInfo, attemptFn) {
    let currentAttemptCount, maxAttemptCount;

    sentinelInfo.currentAttemptCount += 1;
    currentAttemptCount = sentinelInfo.currentAttemptCount;
    maxAttemptCount = sentinelInfo.maximumAttempts;

    if (currentAttemptCount === maxAttemptCount + 1) {
      emitEvent(sentinelInfo.failureEvent);
    }

    if (currentAttemptCount <= maxAttemptCount) {
      attemptFn();
      emitEvent(sentinelInfo.successEvent);
      return true
    }

    return false
  }

  function endOfMediaSentinel() {
    if (!hasSentinelTimeChangedWithinTolerance && nearEndOfMedia) {
      emitEvent(MediaPlayerBase.EVENT.SENTINEL_COMPLETE);
      onEndOfMedia();
      return true
    }
    return false
  }

  function clearSentinels() {
    clearInterval(sentinelInterval);
  }

  function setSentinels(sentinels) {
    if (disableSentinels) {
      return
    }

    clearSentinels();
    sentinelIntervalNumber = 0;
    lastSentinelTime = getCurrentTime();
    sentinelInterval = setInterval(() => {
      sentinelIntervalNumber += 1;
      const newTime = getCurrentTime();

      hasSentinelTimeChangedWithinTolerance = Math.abs(newTime - lastSentinelTime) > 0.2;
      nearEndOfMedia = getDuration() - (newTime || lastSentinelTime) <= 1;
      lastSentinelTime = newTime;

      for (let i = 0; i < sentinels.length; i++) {
        const sentinelActivated = sentinels[i].call();

        if (getCurrentTime() > 0) {
          trustZeroes = false;
        }

        if (sentinelActivated) {
          break
        }
      }
    }, 1100);
  }

  function reportError(errorString, mediaError) {
    DebugTool.info("HTML5 Media Player error: " + errorString);
    emitEvent(MediaPlayerBase.EVENT.ERROR, mediaError);
  }

  function toBuffering() {
    state = MediaPlayerBase.STATE.BUFFERING;
    emitEvent(MediaPlayerBase.EVENT.BUFFERING);
    setSentinels([exitBufferingSentinel]);
  }

  function toComplete() {
    state = MediaPlayerBase.STATE.COMPLETE;
    emitEvent(MediaPlayerBase.EVENT.COMPLETE);
    setSentinels([]);
  }

  function toEmpty() {
    wipe();
    state = MediaPlayerBase.STATE.EMPTY;
  }

  function toError(errorMessage) {
    wipe();
    state = MediaPlayerBase.STATE.ERROR;
    reportError(errorMessage);
  }

  function isReadyToPlayFrom() {
    if (readyToPlayFrom !== undefined) {
      return readyToPlayFrom
    }
    return false
  }

  function getMediaDuration() {
    if (mediaElement && isReadyToPlayFrom()) {
      return mediaElement.duration
    }

    return undefined
  }

  function getCachedSeekableRange() {
    if (readyToCache) {
      cacheSeekableRange();
    }

    return cachedSeekableRange
  }

  function cacheSeekableRange() {
    readyToCache = false;
    setTimeout(function () {
      readyToCache = true;
    }, 250);

    cachedSeekableRange = getElementSeekableRange();
  }

  function getElementSeekableRange() {
    if (mediaElement) {
      if (isReadyToPlayFrom() && mediaElement.seekable && mediaElement.seekable.length > 0) {
        return {
          start: mediaElement.seekable.start(0),
          end: mediaElement.seekable.end(0),
        }
      } else if (mediaElement.duration !== undefined) {
        return {
          start: 0,
          end: mediaElement.duration,
        }
      }
    }
  }

  function getSeekableRange() {
    if (window.bigscreenPlayer.overrides && window.bigscreenPlayer.overrides.cacheSeekableRange) {
      return getCachedSeekableRange()
    } else {
      return getElementSeekableRange()
    }
  }

  function onFinishedBuffering() {
    exitBuffering();
  }

  function pauseMediaElement() {
    mediaElement.pause();
    ignoreNextPauseEvent = true;
  }

  function onPause() {
    if (ignoreNextPauseEvent) {
      ignoreNextPauseEvent = false;
      return
    }

    if (getState() !== MediaPlayerBase.STATE.PAUSED) {
      toPaused();
    }
  }

  function onError() {
    reportError("Media element error code: " + mediaElement.error.code, {
      code: mediaElement.error.code,
      message: mediaElement.error.message,
    });
  }

  function onSourceError() {
    reportError("Media source element error");
  }

  function onDeviceBuffering() {
    if (getState() === MediaPlayerBase.STATE.PLAYING) {
      toBuffering();
    }
  }

  function onEndOfMedia() {
    toComplete();
  }

  function emitSeekAttempted() {
    if (getState() === MediaPlayerBase.STATE.EMPTY) {
      emitEvent(MediaPlayerBase.EVENT.SEEK_ATTEMPTED);
      seekFinished = false;
    }

    count = 0;
    timeoutHappened = false;
    if (window.bigscreenPlayer.overrides && window.bigscreenPlayer.overrides.restartTimeout) {
      setTimeout(() => {
        timeoutHappened = true;
      }, window.bigscreenPlayer.overrides.restartTimeout);
    } else {
      timeoutHappened = true;
    }
  }

  function emitSeekFinishedAtCorrectStartingPoint() {
    let isAtCorrectStartingPoint = Math.abs(getCurrentTime() - sentinelSeekTime) <= seekSentinelTolerance;

    if (sentinelSeekTime === undefined) {
      isAtCorrectStartingPoint = true;
    }

    const isPlayingAtCorrectTime = getState() === MediaPlayerBase.STATE.PLAYING && isAtCorrectStartingPoint;

    if (isPlayingAtCorrectTime && count >= 5 && timeoutHappened && !seekFinished) {
      emitEvent(MediaPlayerBase.EVENT.SEEK_FINISHED);
      seekFinished = true;
    } else if (isPlayingAtCorrectTime) {
      count++;
    } else {
      count = 0;
    }
  }

  function onStatus() {
    if (getState() === MediaPlayerBase.STATE.PLAYING) {
      emitEvent(MediaPlayerBase.EVENT.STATUS);
    }

    emitSeekFinishedAtCorrectStartingPoint();
  }

  function onMetadata() {
    metadataLoaded();

    emitEvent(MediaPlayerBase.EVENT.METADATA);
  }

  function exitBuffering() {
    metadataLoaded();

    if (getState() !== MediaPlayerBase.STATE.BUFFERING) {
      return
    } else if (postBufferingState === MediaPlayerBase.STATE.PAUSED) {
      toPaused();
    } else {
      toPlaying();
    }
  }

  function metadataLoaded() {
    readyToPlayFrom = true;

    if (waitingToPlayFrom()) {
      deferredPlayFrom();
    }
  }

  function playFromIfReady() {
    if (isReadyToPlayFrom()) {
      if (waitingToPlayFrom()) {
        deferredPlayFrom();
      }
    }
  }

  function waitingToPlayFrom() {
    return targetSeekTime !== undefined
  }

  function deferredPlayFrom() {
    if (window.bigscreenPlayer.overrides && window.bigscreenPlayer.overrides.deferredPlayback) {
      handlePlayPromise(mediaElement.play());
      seekTo(targetSeekTime);
    } else {
      seekTo(targetSeekTime);
      handlePlayPromise(mediaElement.play());
    }

    if (postBufferingState === MediaPlayerBase.STATE.PAUSED) {
      pauseMediaElement();
    }
    targetSeekTime = undefined;
  }

  function seekTo(seconds) {
    const clampedTime = getClampedTimeForPlayFrom(seconds);

    mediaElement.currentTime = clampedTime;
    sentinelSeekTime = clampedTime;
  }

  function getCurrentTime() {
    switch (getState()) {
      case MediaPlayerBase.STATE.STOPPED:
      case MediaPlayerBase.STATE.ERROR:
        return
      default:
        if (mediaElement) {
          return mediaElement.currentTime
        }
    }
  }

  /**
   * Time (in seconds) compared to current time within which seeking has no effect.
   * @constant {Number}
   */
  const CURRENT_TIME_TOLERANCE = 1;

  /**
   * Check whether a time value is near to the current media play time.
   * @param {Number} seconds The time value to test, in seconds from the start of the media
   * @protected
   */
  function isNearToCurrentTime(seconds) {
    const currentTime = getCurrentTime();
    const targetTime = getClampedTime(seconds);

    return Math.abs(currentTime - targetTime) <= CURRENT_TIME_TOLERANCE
  }

  /**
   * Clamp a time value so it does not exceed the current range.
   * Clamps to near the end instead of the end itself to allow for devices that cannot seek to the very end of the media.
   * @param {Number} seconds The time value to clamp in seconds from the start of the media
   * @protected
   */
  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 getClampedTimeForPlayFrom(seconds) {
    return getClampedTime(seconds)
  }

  function wipe() {
    mediaType = undefined;
    source = undefined;
    mimeType = undefined;
    targetSeekTime = undefined;
    sentinelSeekTime = undefined;

    clearSentinels();
    destroyMediaElement();

    readyToPlayFrom = false;
  }

  function destroyMediaElement() {
    if (mediaElement) {
      mediaElement.removeEventListener("canplay", onFinishedBuffering, false);
      mediaElement.removeEventListener("seeked", onFinishedBuffering, false);
      mediaElement.removeEventListener("playing", onFinishedBuffering, false);
      mediaElement.removeEventListener("error", onError, false);
      mediaElement.removeEventListener("ended", onEndOfMedia, false);
      mediaElement.removeEventListener("waiting", onDeviceBuffering, false);
      mediaElement.removeEventListener("timeupdate", onStatus, false);
      mediaElement.removeEventListener("loadedmetadata", onMetadata, false);
      mediaElement.removeEventListener("pause", onPause, false);
      sourceElement.removeEventListener("error", onSourceError, false);

      DOMHelpers.safeRemoveElement(sourceElement);
      unloadMediaSrc();
      DOMHelpers.safeRemoveElement(mediaElement);

      mediaElement = null;
      sourceElement = null;
    }
  }

  function unloadMediaSrc() {
    if (window.bigscreenPlayer.overrides && window.bigscreenPlayer.overrides.disableMediaSourceUnload) {
      return
    }
    // Reset source as advised by HTML5 video spec, section 4.8.10.15:
    // http://www.w3.org/TR/2011/WD-html5-20110405/video.html#best-practices-for-authors-using-media-elements
    mediaElement.removeAttribute("src");
    mediaElement.load();
  }

  function toPaused() {
    state = MediaPlayerBase.STATE.PAUSED;
    emitEvent(MediaPlayerBase.EVENT.PAUSED);
    setSentinels([shouldBeSeekedSentinel, shouldBePausedSentinel]);
  }

  function toPlaying() {
    state = MediaPlayerBase.STATE.PLAYING;
    emitEvent(MediaPlayerBase.EVENT.PLAYING);
    setSentinels([endOfMediaSentinel, shouldBeSeekedSentinel, enterBufferingSentinel]);
  }

  return {
    addEventCallback: (thisArg, callback) => {
      eventCallbacks.push({
        thisArg,
        callback,
      });
    },

    removeEventCallback: (callback) => {
      const index = eventCallbacks.findIndex((callbackObj) => callbackObj.callback === callback);

      if (index !== -1) {
        eventCallbacks.splice(index, 1);
      }
    },

    removeAllEventCallbacks: () => {
      eventCallbacks = [];
    },

    initialiseMedia: (type, url, mediaMimeType, sourceContainer, opts) => {
      opts = opts || {};
      disableSentinels = opts.disableSentinels;
      disableSeekSentinel = opts.disableSeekSentinel;
      mediaType = type;
      source = url;
      mimeType = mediaMimeType;

      emitSeekAttempted();

      if (getState() === MediaPlayerBase.STATE.EMPTY) {
        let idSuffix = "Video";

        if (mediaType === MediaPlayerBase.TYPE.AUDIO || mediaType === MediaPlayerBase.TYPE.LIVE_AUDIO) {
          idSuffix = "Audio";
        }

        setSeekSentinelTolerance();

        mediaElement = document.createElement(idSuffix.toLowerCase(), "mediaPlayer" + idSuffix);
        mediaElement.autoplay = false;
        mediaElement.style.position = "absolute";
        mediaElement.style.top = "0px";
        mediaElement.style.left = "0px";
        mediaElement.style.width = "100%";
        mediaElement.style.height = "100%";

        mediaElement.addEventListener("canplay", onFinishedBuffering, false);
        mediaElement.addEventListener("seeked", onFinishedBuffering, false);
        mediaElement.addEventListener("playing", onFinishedBuffering, false);
        mediaElement.addEventListener("error", onError, false);
        mediaElement.addEventListener("ended", onEndOfMedia, false);
        mediaElement.addEventListener("waiting", onDeviceBuffering, false);
        mediaElement.addEventListener("timeupdate", onStatus, false);
        mediaElement.addEventListener("loadedmetadata", onMetadata, false);
        mediaElement.addEventListener("pause", onPause, false);

        prependChildElement(sourceContainer, mediaElement);

        sourceElement = generateSourceElement(url, mimeType);
        sourceElement.addEventListener("error", onSourceError, false);

        mediaElement.preload = "auto";
        appendChildElement(mediaElement, sourceElement);

        mediaElement.load();

        toStopped();
      } else {
        toError("Cannot set source unless in the '" + MediaPlayerBase.STATE.EMPTY + "' state");
      }
    },

    setPlaybackRate: (rate) => {
      mediaElement.playbackRate = rate;
    },

    getPlaybackRate: () => mediaElement.playbackRate,

    playFrom: (seconds) => {
      postBufferingState = MediaPlayerBase.STATE.PLAYING;
      targetSeekTime = seconds;
      sentinelLimits.seek.currentAttemptCount = 0;

      switch (getState()) {
        case MediaPlayerBase.STATE.PAUSED:
        case MediaPlayerBase.STATE.COMPLETE:
          trustZeroes = true;
          toBuffering();
          playFromIfReady();
          break

        case MediaPlayerBase.STATE.BUFFERING:
          playFromIfReady();
          break

        case MediaPlayerBase.STATE.PLAYING:
          trustZeroes = true;
          toBuffering();
          targetSeekTime = getClampedTimeForPlayFrom(seconds);
          if (isNearToCurrentTime(targetSeekTime)) {
            targetSeekTime = undefined;
            toPlaying();
          } else {
            playFromIfReady();
          }
          break

        default:
          toError("Cannot playFrom while in the '" + getState() + "' state");
          break
      }
    },

    beginPlayback: () => {
      postBufferingState = MediaPlayerBase.STATE.PLAYING;
      sentinelSeekTime = undefined;

      switch (getState()) {
        case MediaPlayerBase.STATE.STOPPED:
          trustZeroes = true;
          toBuffering();
          handlePlayPromise(mediaElement.play());
          break

        default:
          toError("Cannot beginPlayback while in the '" + getState() + "' state");
          break
      }
    },

    beginPlaybackFrom: (seconds) => {
      postBufferingState = MediaPlayerBase.STATE.PLAYING;
      targetSeekTime = seconds;
      sentinelLimits.seek.currentAttemptCount = 0;

      switch (getState()) {
        case MediaPlayerBase.STATE.STOPPED:
          trustZeroes = true;
          toBuffering();
          playFromIfReady();
          break

        default:
          toError("Cannot beginPlaybackFrom while in the '" + getState() + "' state");
          break
      }
    },

    pause: () => {
      postBufferingState = MediaPlayerBase.STATE.PAUSED;
      switch (getState()) {
        case MediaPlayerBase.STATE.PAUSED:
          break

        case MediaPlayerBase.STATE.BUFFERING:
          sentinelLimits.pause.currentAttemptCount = 0;
          if (isReadyToPlayFrom()) {
            // If we are not ready to playFrom, then calling pause would seek to the start of media, which we might not want.
            pauseMediaElement();
          }
          break

        case MediaPlayerBase.STATE.PLAYING:
          sentinelLimits.pause.currentAttemptCount = 0;
          pauseMediaElement();
          toPaused();
          break

        default:
          toError("Cannot pause while in the '" + getState() + "' state");
          break
      }
    },

    resume: () => {
      postBufferingState = MediaPlayerBase.STATE.PLAYING;
      switch (getState()) {
        case MediaPlayerBase.STATE.PLAYING:
          break

        case MediaPlayerBase.STATE.BUFFERING:
          if (isReadyToPlayFrom()) {
            // If we are not ready to playFrom, then calling play would seek to the start of media, which we might not want.
            handlePlayPromise(mediaElement.play());
          }
          break

        case MediaPlayerBase.STATE.PAUSED:
          handlePlayPromise(mediaElement.play());
          toPlaying();
          break

        default:
          toError("Cannot resume while in the '" + getState() + "' state");
          break
      }
    },

    stop: () => {
      switch (getState()) {
        case MediaPlayerBase.STATE.STOPPED:
          break

        case MediaPlayerBase.STATE.BUFFERING:
        case MediaPlayerBase.STATE.PLAYING:
        case MediaPlayerBase.STATE.PAUSED:
        case MediaPlayerBase.STATE.COMPLETE:
          pauseMediaElement();
          toStopped();
          break

        default:
          toError("Cannot stop while in the '" + getState() + "' state");
          break
      }
    },

    reset: () => {
      switch (getState()) {
        case MediaPlayerBase.STATE.EMPTY:
          break

        case MediaPlayerBase.STATE.STOPPED:
        case MediaPlayerBase.STATE.ERROR:
          toEmpty();
          break

        default:
          toError("Cannot reset while in the '" + getState() + "' state");
          break
      }
    },

    getSeekableRange: () => {
      switch (getState()) {
        case MediaPlayerBase.STATE.STOPPED:
        case MediaPlayerBase.STATE.ERROR:
          break

        default:
          return getSeekableRange()
      }
      return undefined
    },

    getState: () => state,
    getPlayerElement: () => mediaElement,
    getSource: getSource,
    getMimeType: getMimeType,
    getCurrentTime: getCurrentTime,
    getDuration: getDuration,
    toPaused: toPaused,
    toPlaying: toPlaying,
  }
}

function SamsungMaple() {
  const playerPlugin = document.getElementById("playerPlugin");

  let state = MediaPlayerBase.STATE.EMPTY;
  let deferSeekingTo = null;
  let postBufferingState = null;
  let tryingToPause = false;
  let currentTimeKnown = false;

  let mediaType;
  let source;
  let mimeType;

  let range;
  let currentTime;

  let eventCallbacks = [];

  function initialiseMedia(type, url, mediaMimeType) {
    if (getState() === MediaPlayerBase.STATE.EMPTY) {
      mediaType = type;
      source = url;
      mimeType = mediaMimeType;
      _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:
        break

      case MediaPlayerBase.STATE.BUFFERING:
        if (tryingToPause) {
          tryingToPause = false;
          toPlaying();
        }
        break

      case MediaPlayerBase.STATE.PAUSED:
        playerPlugin.Resume();
        toPlaying();
        break

      default:
        _toError("Cannot resume while in the '" + getState() + "' state");
        break
    }
  }

  function playFrom(seconds) {
    postBufferingState = MediaPlayerBase.STATE.PLAYING;

    const seekingTo = range ? _getClampedTimeForPlayFrom(seconds) : seconds;

    switch (getState()) {
      case MediaPlayerBase.STATE.BUFFERING:
        deferSeekingTo = seekingTo;
        break

      case MediaPlayerBase.STATE.PLAYING:
        _toBuffering();
        if (!currentTimeKnown) {
          deferSeekingTo = seekingTo;
        } else if (_isNearToCurrentTime(seekingTo)) {
          toPlaying();
        } else {
          _seekToWithFailureStateTransition(seekingTo);
        }
        break

      case MediaPlayerBase.STATE.PAUSED:
        _toBuffering();
        if (!currentTimeKnown) {
          deferSeekingTo = seekingTo;
        } else if (_isNearToCurrentTime(seekingTo)) {
          playerPlugin.Resume();
          toPlaying();
        } else {
          _seekToWithFailureStateTransition(seekingTo);
          playerPlugin.Resume();
        }
        break

      case MediaPlayerBase.STATE.COMPLETE:
        playerPlugin.Stop();
        _setDisplayFullScreenForVideo();
        playerPlugin.ResumePlay(_wrappedSource(), seekingTo);
        _toBuffering();
        break

      default:
        _toError("Cannot playFrom while in the '" + getState() + "' state");
        break
    }
  }

  function beginPlayback() {
    postBufferingState = MediaPlayerBase.STATE.PLAYING;
    switch (getState()) {
      case MediaPlayerBase.STATE.STOPPED:
        _toBuffering();
        _setDisplayFullScreenForVideo();
        playerPlugin.Play(_wrappedSource());
        break

      default:
        _toError("Cannot beginPlayback while in the '" + getState() + "' state");
        break
    }
  }

  function beginPlaybackFrom(seconds) {
    postBufferingState = MediaPlayerBase.STATE.PLAYING;

    const seekingTo = range ? _getClampedTimeForPlayFrom(seconds) : seconds;

    switch (getState()) {
      case MediaPlayerBase.STATE.STOPPED:
        _setDisplayFullScreenForVideo();
        playerPlugin.ResumePlay(_wrappedSource(), seekingTo);
        _toBuffering();
        break

      default:
        _toError("Cannot beginPlayback while in the '" + getState() + "' state");
        break
    }
  }

  function pause() {
    postBufferingState = MediaPlayerBase.STATE.PAUSED;
    switch (getState()) {
      case MediaPlayerBase.STATE.BUFFERING:
      case MediaPlayerBase.STATE.PAUSED:
        break

      case MediaPlayerBase.STATE.PLAYING:
        _tryPauseWithStateTransition();
        break

      default:
        _toError("Cannot pause while in the '" + getState() + "' state");
        break
    }
  }

  function stop() {
    switch (getState()) {
      case MediaPlayerBase.STATE.STOPPED:
        break

      case MediaPlayerBase.STATE.BUFFERING:
      case MediaPlayerBase.STATE.PLAYING:
      case MediaPlayerBase.STATE.PAUSED:
      case MediaPlayerBase.STATE.COMPLETE:
        _stopPlayer();
        _toStopped();
        break

      default:
        _toError("Cannot stop while in the '" + getState() + "' state");
        break
    }
  }

  function reset() {
    switch (getState()) {
      case MediaPlayerBase.STATE.EMPTY:
        break

      case MediaPlayerBase.STATE.STOPPED:
      case MediaPlayerBase.STATE.ERROR:
        _toEmpty();
        break

      default:
        _toError("Cannot reset while in the '" + getState() + "' state");
        break
    }
  }

  function getSource() {
    return source
  }

  function getMimeType() {
    return mimeType
  }

  function getCurrentTime() {
    if (getState() === MediaPlayerBase.STATE.STOPPED) {
      return undefined
    } else {
      return currentTime
    }
  }

  function getSeekableRange() {
    return range
  }

  function getDuration() {
    if (range) {
      return range.end
    }
    return undefined
  }

  function getState() {
    return state
  }

  function getPlayerElement() {
    return playerPlugin
  }

  function toPlaying() {
    state = MediaPlayerBase.STATE.PLAYING;
    _emitEvent(MediaPlayerBase.EVENT.PLAYING);
  }

  function toPaused() {
    state = MediaPlayerBase.STATE.PAUSED;
    _emitEvent(MediaPlayerBase.EVENT.PAUSED);
  }

  function _toStopped() {
    currentTime = 0;
    range = undefined;
    state = MediaPlayerBase.STATE.STOPPED;
    _emitEvent(MediaPlayerBase.EVENT.STOPPED);
  }

  function _toBuffering() {
    state = MediaPlayerBase.STATE.BUFFERING;
    _emitEvent(MediaPlayerBase.EVENT.BUFFERING);
  }

  function _toComplete() {
    state = MediaPlayerBase.STATE.COMPLETE;
    _emitEvent(MediaPlayerBase.EVENT.COMPLETE);
  }

  function _toEmpty() {
    _wipe();
    state = MediaPlayerBase.STATE.EMPTY;
  }

  function _toError(errorMessage) {
    _wipe();
    state = MediaPlayerBase.STATE.ERROR;
    _reportError(errorMessage);
    throw new Error("ApiError: " + errorMessage)
  }

  function _onFinishedBuffering() {
    if (getState() !== MediaPlayerBase.STATE.BUFFERING) {
      return
    }

    if (deferSeekingTo === null) {
      if (postBufferingState === MediaPlayerBase.STATE.PAUSED) {
        _tryPauseWithStateTransition();
      } else {
        toPlaying();
      }
    }
  }

  function _onDeviceError(message) {
    _reportError(message);
  }

  function _onDeviceBuffering() {
    if (getState() === MediaPlayerBase.STATE.PLAYING) {
      _toBuffering();
    }
  }

  function _onEndOfMedia() {
    _toComplete();
  }

  function _stopPlayer() {
    playerPlugin.Stop();
    currentTimeKnown = false;
  }

  function _tryPauseWithStateTransition() {
    let success = _isSuccessCode(playerPlugin.Pause());
    if (success) {
      toPaused();
    }

    tryingToPause = !success;
  }

  function _onStatus() {
    let state = getState();
    if (state === MediaPlayerBase.STATE.PLAYING) {
      _emitEvent(MediaPlayerBase.EVENT.STATUS);
    }
  }

  function _onMetadata() {
    range = {
      start: 0,
      end: playerPlugin.GetDuration() / 1000,
    };

    _emitEvent(MediaPlayerBase.EVENT.METADATA);
  }

  function _onCurrentTime(timeInMillis) {
    currentTime = timeInMillis / 1000;
    _onStatus();
    currentTimeKnown = true;

    if (deferSeekingTo !== null) {
      _deferredSeek();
    }

    if (tryingToPause) {
      _tryPauseWithStateTransition();
    }
  }

  function _deferredSeek() {
    const clampedTime = _getClampedTimeForPlayFrom(deferSeekingTo);
    const isNearCurrentTime = _isNearToCurrentTime(clampedTime);

    if (isNearCurrentTime) {
      toPlaying();
      deferSeekingTo = null;
    } else {
      const seekResult = _seekTo(clampedTime);

      if (seekResult) {
        deferSeekingTo = null;
      }
    }
  }

  function _getClampedTimeForPlayFrom(seconds) {
    const clampedTime = getClampedTime(seconds);

    if (clampedTime !== seconds) {
      DebugTool.info(
        "playFrom " +
          seconds +
          " clamped to " +
          clampedTime +
          " - seekable range is { start: " +
          range.start +
          ", end: " +
          range.end +
          " }"
      );
    }
    return clampedTime
  }

  function _onWindowHide() {
    stop();
  }

  function _registerEventHandlers() {
    window.SamsungMapleOnRenderError = () => _onDeviceError("Media element emitted OnRenderError");
    playerPlugin.OnRenderError = "SamsungMapleOnRenderError";

    window.SamsungMapleOnConnectionFailed = () => _onDeviceError("Media element emitted OnConnectionFailed");
    playerPlugin.OnConnectionFailed = "SamsungMapleOnConnectionFailed";

    window.SamsungMapleOnNetworkDisconnected = () => _onDeviceError("Media element emitted OnNetworkDisconnected");
    playerPlugin.OnNetworkDisconnected = "SamsungMapleOnNetworkDisconnected";

    window.SamsungMapleOnStreamNotFound = () => _onDeviceError("Media element emitted OnStreamNotFound");
    playerPlugin.OnStreamNotFound = "SamsungMapleOnStreamNotFound";

    window.SamsungMapleOnAuthenticationFailed = () => _onDeviceError("Media element emitted OnAuthenticationFailed");
    playerPlugin.OnAuthenticationFailed = "SamsungMapleOnAuthenticationFailed";

    window.SamsungMapleOnRenderingComplete = () => _onEndOfMedia();
    playerPlugin.OnRenderingComplete = "SamsungMapleOnRenderingComplete";

    window.SamsungMapleOnBufferingStart = () => _onDeviceBuffering();
    playerPlugin.OnBufferingStart = "SamsungMapleOnBufferingStart";

    window.SamsungMapleOnBufferingComplete = () => _onFinishedBuffering();
    playerPlugin.OnBufferingComplete = "SamsungMapleOnBufferingComplete";

    window.SamsungMapleOnStreamInfoReady = () => _onMetadata();
    playerPlugin.OnStreamInfoReady = "SamsungMapleOnStreamInfoReady";

    window.SamsungMapleOnCurrentPlayTime = (timeInMillis) => _onCurrentTime(timeInMillis);
    playerPlugin.OnCurrentPlayTime = "SamsungMapleOnCurrentPlayTime";

    window.addEventListener("hide", _onWindowHide, false);
    window.addEventListener("unload", _onWindowHide, false);
  }

  function _unregisterEventHandlers() {
    const eventHandlers = [
      "SamsungMapleOnRenderError",
      "SamsungMapleOnRenderingComplete",
      "SamsungMapleOnBufferingStart",
      "SamsungMapleOnBufferingComplete",
      "SamsungMapleOnStreamInfoReady",
      "SamsungMapleOnCurrentPlayTime",
      "SamsungMapleOnConnectionFailed",
      "SamsungMapleOnNetworkDisconnected",
      "SamsungMapleOnStreamNotFound",
      "SamsungMapleOnAuthenticationFailed",
    ];

    for (let i = 0; i < eventHandlers.length; i++) {
      const handler = eventHandlers[i];
      const hook = handler.substring("SamsungMaple".length);

      playerPlugin[hook] = undefined;
      delete window[handler];
    }

    window.removeEventListener("hide", _onWindowHide, false);
    window.removeEventListener("unload", _onWindowHide, false);
  }

  function _wipe() {
    _stopPlayer();
    mediaType = undefined;
    source = undefined;
    mimeType = undefined;
    currentTime = undefined;
    range = undefined;
    deferSeekingTo = null;
    tryingToPause = false;
    currentTimeKnown = false;
    _unregisterEventHandlers();
  }

  function _seekTo(seconds) {
    const offset = seconds - getCurrentTime();
    const success = _isSuccessCode(_jump(offset));

    if (success) {
      currentTime = seconds;
    }

    return success
  }

  function _seekToWithFailureStateTransition(seconds) {
    const success = _seekTo(seconds);

    if (!success) {
      toPlaying();
    }
  }

  function _jump(offsetSeconds) {
    if (offsetSeconds > 0) {
      return playerPlugin.JumpForward(offsetSeconds)
    } else {
      return playerPlugin.JumpBackward(Math.abs(offsetSeconds))
    }
  }

  function _isHlsMimeType() {
    const mime = mimeType.toLowerCase();
    return mime === "application/vnd.apple.mpegurl" || mime === "application/x-mpegurl"
  }

  function _wrappedSource() {
    let wrappedSource = source;

    if (_isHlsMimeType()) {
      wrappedSource += "|COMPONENT=HLS";
    }

    return wrappedSource
  }

  function _reportError(errorMessage) {
    DebugTool.info(errorMessage);
    _emitEvent(MediaPlayerBase.EVENT.ERROR, { errorMessage: errorMessage });
  }

  function _setDisplayFullScreenForVideo() {
    if (mediaType === MediaPlayerBase.TYPE.VIDEO) {
      const dimensions = _getScreenSize();
      playerPlugin.SetDisplayArea(0, 0, dimensions.width, dimensions.height);
    }
  }

  function _getScreenSize() {
    let w, h;

    if (typeof window.innerWidth === "number") {
      w = window.innerWidth;
      h = window.innerHeight;
    } else {
      const d = document.documentElement || document.body;

      h = d.clientHeight || d.offsetHeight;
      w = d.clientWidth || d.offsetWidth;
    }

    return {
      width: w,
      height: h,
    }
  }

  function _isSuccessCode(code) {
    const samsung2010ErrorCode = -1;
    return code && code !== samsung2010ErrorCode
  }

  /**
   * @constant {Number} Time (in seconds) compared to current time within which seeking has no effect.
   * On a sample device (Samsung FoxP 2013), seeking by two seconds worked 90% of the time, but seeking
   * by 2.5 seconds was always seen to work.
   */
  const CURRENT_TIME_TOLERANCE = 2.5;

  function _isNearToCurrentTime(seconds) {
    const currentTime = getCurrentTime();
    const targetTime = getClampedTime(seconds);

    return Math.abs(currentTime - targetTime) <= CURRENT_TIME_TOLERANCE
  }

  function getClampedTime(seconds) {
    const range = getSeekableRange();
    const CLAMP_OFFSET_FROM_END_OF_RANGE = 1.1;
    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 _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));
  }

  return {
    addEventCallback: (thisArg, callback) => {
      eventCallbacks.push({ thisArg, callback });
    },

    removeEventCallback: (callback) => {
      const index = eventCallbacks.findIndex((callbackObj) => callbackObj.callback === callback);

      if (index !== -1) {
        eventCallbacks.splice(index, 1);
      }
    },

    removeAllEventCallbacks: () => {
      eventCallbacks = [];
    },
    initialiseMedia: initialiseMedia,
    playFrom: playFrom,
    beginPlayback: beginPlayback,
    beginPlaybackFrom: beginPlaybackFrom,
    resume: resume,
    pause: pause,
    stop: stop,
    reset: reset,
    getSeekableRange: getSeekableRange,
    getState: getState,
    getPlayerElement: getPlayerElement,
    getSource: getSource,
    getMimeType: getMimeType,
    getCurrentTime: getCurrentTime,
    getDuration: getDuration,
    toPaused: toPaused,
    toPlaying: toPlaying,
  }
}

/**
 * @preserve Copyright (c) 2017-present British Broadcasting Corporation. All rights reserved.
 * @license See https://github.com/fmtvp/tal/blob/master/LICENSE for full licence
 */

function SamsungStreaming() {
  let state = MediaPlayerBase.STATE.EMPTY;
  let currentPlayer;
  let deferSeekingTo = null;
  let nextSeekingTo = null;
  let postBufferingState = null;
  let tryingToPause = false;
  let currentTimeKnown = false;
  let updatingTime = false;
  let lastWindowRanged = false;

  let mediaType;
  let source;
  let mimeType;

  let range;
  let currentTime;

  let eventCallbacks = [];

  let playerPlugin;
  let tvmwPlugin;
  let originalSource;

  try {
    _registerSamsungPlugins();
  } catch (ignoreErr) {}

  const PlayerEventCodes = {
    CONNECTION_FAILED: 1,
    AUTHENTICATION_FAILED: 2,
    STREAM_NOT_FOUND: 3,
    NETWORK_DISCONNECTED: 4,
    NETWORK_SLOW: 5,
    RENDER_ERROR: 6,
    RENDERING_START: 7,
    RENDERING_COMPLETE: 8,
    STREAM_INFO_READY: 9,
    DECODING_COMPLETE: 10,
    BUFFERING_START: 11,
    BUFFERING_COMPLETE: 12,
    BUFFERING_PROGRESS: 13,
    CURRENT_PLAYBACK_TIME: 14,
    AD_START: 15,
    AD_END: 16,
    RESOLUTION_CHANGED: 17,
    BITRATE_CHANGED: 18,
    SUBTITLE: 19,
    CUSTOM: 20,
  };

  const PlayerEmps = {
    Player: 0,
    StreamingPlayer: 1,
  };

  /**
   * @constant {Number} Time (in seconds) compared to current time within which seeking has no effect.
   * Jumping to time lower than 3s causes error in PlayFrom60 on HLS live - player jumps to previous chunk.
   * Value set to 4s to be ahead of potential wrong player jumps.
   */
  const CURRENT_TIME_TOLERANCE = 4;
  const CLAMP_OFFSET_FROM_END_OF_LIVE_RANGE = 10;
  const CLAMP_OFFSET_FROM_START_OF_RANGE = 1.1;
  const CLAMP_OFFSET_FROM_END_OF_RANGE = 1.1;
  const RANGE_UPDATE_TOLERANCE = 8;
  const RANGE_END_TOLERANCE = 100;

  function initialiseMedia(type, url, mediaMimeType) {
    if (getState() === MediaPlayerBase.STATE.EMPTY) {
      mediaType = type;
      source = url;
      mimeType = mediaMimeType;
      _registerEventHandlers();
      _toStopped();

      if (_isHlsMimeType()) {
        _openStreamingPlayerPlugin();
        if (_isLiveMedia()) {
          source += "|HLSSLIDING|COMPONENT=HLS";
        } else {
          source += "|COMPONENT=HLS";
        }
      } else {
        _openPlayerPlugin();
      }

      _initPlayer(source);
    } else {
      _toError("Cannot set source unless in the '" + MediaPlayerBase.STATE.EMPTY + "' state");
    }
  }

  function resume() {
    postBufferingState = MediaPlayerBase.STATE.PLAYING;
    switch (getState()) {
      case MediaPlayerBase.STATE.PLAYING:
        break

      case MediaPlayerBase.STATE.BUFFERING:
        if (tryingToPause) {
          tryingToPause = false;
          toPlaying();
        }
        break

      case MediaPlayerBase.STATE.PAUSED:
        playerPlugin.Execute("Resume");
        toPlaying();
        break

      default:
        _toError("Cannot resume while in the '" + getState() + "' state");
        break
    }
  }

  function playFrom(seconds) {
    postBufferingState = MediaPlayerBase.STATE.PLAYING;
    const seekingTo = range ? _getClampedTimeForPlayFrom(seconds) : seconds;

    switch (getState()) {
      case MediaPlayerBase.STATE.BUFFERING:
        // deferSeekingTo = seekingTo;
        nextSeekingTo = seekingTo;
        break

      case MediaPlayerBase.STATE.PLAYING:
        _toBuffering();
        if (!currentTimeKnown) {
          deferSeekingTo = seekingTo;
        } else if (_isNearToCurrentTime(seekingTo)) {
          toPlaying();
        } else {
          _seekToWithFailureStateTransition(seekingTo);
        }
        break

      case MediaPlayerBase.STATE.PAUSED:
        _toBuffering();
        if (!currentTimeKnown) {
          deferSeekingTo = seekingTo;
        } else if (_isNearToCurrentTime(seekingTo)) {
          playerPlugin.Execute("Resume");
          toPlaying();
        } else {
          _seekToWithFailureStateTransition(seekingTo);
          playerPlugin.Execute("Resume");
        }
        break

      case MediaPlayerBase.STATE.COMPLETE:
        playerPlugin.Execute("Stop");
        _initPlayer(source);
        playerPlugin.Execute("StartPlayback", seekingTo);
        _toBuffering();
        break

      default:
        _toError("Cannot playFrom while in the '" + getState() + "' state");
        break
    }
  }

  function beginPlayback() {
    postBufferingState = MediaPlayerBase.STATE.PLAYING;
    switch (getState()) {
      case MediaPlayerBase.STATE.STOPPED:
        _toBuffering();
        playerPlugin.Execute("StartPlayback");
        break

      default:
        _toError("Cannot beginPlayback while in the '" + getState() + "' state");
        break
    }
  }

  function beginPlaybackFrom(seconds) {
    postBufferingState = MediaPlayerBase.STATE.PLAYING;
    let seekingTo = getSeekableRange() ? _getClampedTimeForPlayFrom(seconds) : seconds;

    // StartPlayback from near start of range causes spoiler defect
    if (seekingTo < CLAMP_OFFSET_FROM_START_OF_RANGE && _isLiveMedia()) {
      seekingTo = CLAMP_OFFSET_FROM_START_OF_RANGE;
    } else {
      seekingTo = parseInt(Math.floor(seekingTo), 10);
    }

    switch (getState()) {
      case MediaPlayerBase.STATE.STOPPED:
        playerPlugin.Execute("StartPlayback", seekingTo);
        _toBuffering();
        break

      default:
        _toError("Cannot beginPlayback while in the '" + getState() + "' state");
        break
    }
  }

  function pause() {
    postBufferingState = MediaPlayerBase.STATE.PAUSED;
    switch (getState()) {
      case MediaPlayerBase.STATE.BUFFERING:
      case MediaPlayerBase.STATE.PAUSED:
        break

      case MediaPlayerBase.STATE.PLAYING:
        _tryPauseWithStateTransition();
        break

      default:
        _toError("Cannot pause while in the '" + getState() + "' state");
        break
    }
  }

  function stop() {
    switch (getState()) {
      case MediaPlayerBase.STATE.STOPPED:
        break

      case MediaPlayerBase.STATE.BUFFERING:
      case MediaPlayerBase.STATE.PLAYING:
      case MediaPlayerBase.STATE.PAUSED:
      case MediaPlayerBase.STATE.COMPLETE:
        _stopPlayer();
        _toStopped();
        break

      default:
        _toError("Cannot stop while in the '" + getState() + "' state");
        break
    }
  }

  function reset() {
    switch (getState()) {
      case MediaPlayerBase.STATE.EMPTY:
        break

      case MediaPlayerBase.STATE.STOPPED:
      case MediaPlayerBase.STATE.ERROR:
        _toEmpty();
        break

      default:
        _toError("Cannot reset while in the '" + getState() + "' state");
        break
    }
  }

  function getSource() {
    return source
  }

  function getMimeType() {
    return mimeType
  }

  function getCurrentTime() {
    if (getState() === MediaPlayerBase.STATE.STOPPED) {
      return undefined
    } else {
      return currentTime
    }
  }

  function getSeekableRange() {
    switch (getState()) {
      case MediaPlayerBase.STATE.STOPPED:
      case MediaPlayerBase.STATE.ERROR:
        return undefined

      default:
        return range
    }
  }

  function getDuration() {
    if (range) {
      return range.end
    }

    return undefined
  }

  function getState() {
    return state
  }

  function getPlayerElement() {
    return playerPlugin
  }

  function toPlaying() {
    if (_isHlsMimeType() && _isLiveMedia() && !updatingTime) {
      _updateRange();
    }

    state = MediaPlayerBase.STATE.PLAYING;
    _emitEvent(MediaPlayerBase.EVENT.PLAYING);
  }

  function toPaused() {
    state = MediaPlayerBase.STATE.PAUSED;
    _emitEvent(MediaPlayerBase.EVENT.PAUSED);
  }

  function _toStopped() {
    currentTime = 0;
    range = undefined;
    state = MediaPlayerBase.STATE.STOPPED;
    _emitEvent(MediaPlayerBase.EVENT.STOPPED);
  }

  function _toBuffering() {
    state = MediaPlayerBase.STATE.BUFFERING;
    _emitEvent(MediaPlayerBase.EVENT.BUFFERING);
  }

  function _toComplete() {
    state = MediaPlayerBase.STATE.COMPLETE;
    _emitEvent(MediaPlayerBase.EVENT.COMPLETE);
  }

  function _toEmpty() {
    _wipe();
    state = MediaPlayerBase.STATE.EMPTY;
  }

  function _toError(errorMessage) {
    _wipe();
    state = MediaPlayerBase.STATE.ERROR;
    _reportError(errorMessage);
    throw new Error("ApiError: " + errorMessage)
  }

  function _registerSamsungPlugins() {
    playerPlugin = document.getElementById("sefPlayer");
    tvmwPlugin = document.getElementById("pluginObjectTVMW");
    originalSource = tvmwPlugin.GetSource();
    window.addEventListener(
      "hide",
      () => {
        stop();
        tvmwPlugin.SetSource(originalSource);
      },
      false
    );
  }

  function _getClampedTime(seconds) {
    const range = getSeekableRange();
    const offsetFromEnd = _getClampOffsetFromConfig();
    const nearToEnd = Math.max(range.end - offsetFromEnd, range.start);

    if (seconds < range.start) {
      return range.start
    } else if (seconds > nearToEnd) {
      return nearToEnd
    } else {
      return seconds
    }
  }

  function _openPlayerPlugin() {
    if (currentPlayer !== undefined) {
      playerPlugin.Close();
    }

    playerPlugin.Open("Player", "1.010", "Player");
    currentPlayer = PlayerEmps.Player;
  }

  function _isLiveRangeOutdated() {
    const time = Math.floor(currentTime);

    if (time % 8 === 0 && !updatingTime && lastWindowRanged !== time) {
      lastWindowRanged = time;
      return true
    } else {
      return false
    }
  }

  function _openStreamingPlayerPlugin() {
    if (currentPlayer !== undefined) {
      playerPlugin.Close();
    }

    playerPlugin.Open("StreamingPlayer", "1.0", "StreamingPlayer");
    currentPlayer = PlayerEmps.StreamingPlayer;
  }

  function _closePlugin() {
    playerPlugin.Close();
    currentPlayer = undefined;
  }

  function _initPlayer(source) {
    const result = playerPlugin.Execute("InitPlayer", source);

    if (result !== 1) {
      _toError("Failed to initialize video: " + source);
    }
  }

  function _onFinishedBuffering() {
    if (getState() !== MediaPlayerBase.STATE.BUFFERING) {
      return
    }

    if (!_isInitialBufferingFinished() && nextSeekingTo !== null) {
      deferSeekingTo = nextSeekingTo;
      nextSeekingTo = null;
    }

    if (deferSeekingTo === null) {
      if (postBufferingState === MediaPlayerBase.STATE.PAUSED) {
        _tryPauseWithStateTransition();
      } else {
        toPlaying();
      }
    }
  }

  function _onDeviceError(message) {
    _reportError(message);
  }

  function _onDeviceBuffering() {
    if (getState() === MediaPlayerBase.STATE.PLAYING) {
      _toBuffering();
    }
  }

  function _onEndOfMedia() {
    _toComplete();
  }

  function _stopPlayer() {
    playerPlugin.Execute("Stop");
    currentTimeKnown = false;
  }

  function _tryPauseWithStateTransition() {
    let success = playerPlugin.Execute("Pause");
    success = success && success !== -1;

    if (success) {
      toPaused();
    }

    tryingToPause = !success;
  }

  function _onStatus() {
    const state = getState();

    if (state === MediaPlayerBase.STATE.PLAYING) {
      _emitEvent(MediaPlayerBase.EVENT.STATUS);
    }
  }

  function _updateRange() {
    if (_isHlsMimeType() && _isLiveMedia()) {
      const playingRange = playerPlugin.Execute("GetPlayingRange").split("-");

      range = {
        start: Math.floor(playingRange[0]),
        end: Math.floor(playingRange[1]),
      };

      // don't call range for the next 8 seconds
      updatingTime = true;
      setTimeout(() => {
        updatingTime = false;
      }, RANGE_UPDATE_TOLERANCE * 1000);
    } else {
      const duration = playerPlugin.Execute("GetDuration") / 1000;
      range = {
        start: 0,
        end: duration,
      };
    }
  }

  function _onCurrentTime(timeInMillis) {
    currentTime = timeInMillis / 1000;
    _onStatus();
    currentTimeKnown = true;

    // [optimisation] do not call player API periodically in HLS live
    // - calculate range manually when possible
    // - do not calculate range if player API was called less than RANGE_UPDATE_TOLERANCE seconds ago
    if (_isLiveMedia() && _isLiveRangeOutdated()) {
      range.start += 8;
      range.end += 8;
    }

    if (nextSeekingTo !== null) {
      deferSeekingTo = nextSeekingTo;
      nextSeekingTo = null;
    }

    if (deferSeekingTo !== null) {
      _deferredSeek();
    }

    if (tryingToPause) {
      _tryPauseWithStateTransition();
    }
  }

  function _deferredSeek() {
    const clampedTime = _getClampedTimeForPlayFrom(deferSeekingTo);
    const isNearCurrentTime = _isNearToCurrentTime(clampedTime);

    if (isNearCurrentTime) {
      toPlaying();
      deferSeekingTo = null;
    } else {
      const seekResult = _seekTo(clampedTime);

      if (seekResult) {
        deferSeekingTo = null;
      }
    }
  }

  function _getClampedTimeForPlayFrom(seconds) {
    if (currentPlayer === PlayerEmps.StreamingPlayer && !updatingTime) {
      _updateRange();
    }

    const clampedTime = _getClampedTime(seconds);

    if (clampedTime !== seconds) {
      DebugTool.info(
        "playFrom " +
          seconds +
          " clamped to " +
          clampedTime +
          " - seekable range is { start: " +
          range.start +
          ", end: " +
          range.end +
          " }"
      );
    }

    return clampedTime
  }

  function _getClampOffsetFromConfig() {
    if (_isLiveMedia()) {
      return CLAMP_OFFSET_FROM_END_OF_LIVE_RANGE
    } else {
      return CLAMP_OFFSET_FROM_END_OF_RANGE
    }
  }

  function _registerEventHandlers() {
    playerPlugin.OnEvent = (eventType, param1) => {
      switch (eventType) {
        case PlayerEventCodes.STREAM_INFO_READY:
          _updateRange();
          _emitEvent(MediaPlayerBase.EVENT.METADATA);
          break

        case PlayerEventCodes.CURRENT_PLAYBACK_TIME:
          if (range && _isLiveMedia()) {
            const seconds = Math.floor(param1 / 1000);

            // jump to previous current time if PTS out of range occurs
            if (seconds > range.end + RANGE_END_TOLERANCE) {
              playFrom(currentTime);
              break
              // call GetPlayingRange() on SEF emp if current time is out of range
            } else if (!_isCurrentTimeInRangeTolerance(seconds)) {
              _updateRange();
            }
          }

          _onCurrentTime(param1);
          break

        case PlayerEventCodes.BUFFERING_START:
        case PlayerEventCodes.BUFFERING_PROGRESS:
          _onDeviceBuffering();
          break

        case PlayerEventCodes.BUFFERING_COMPLETE:
          // For live HLS, don't update the range more than once every 8 seconds
          if (!updatingTime) {
            _updateRange();
          }

          // [optimisation] if Stop() is not called after RENDERING_COMPLETE then player sends periodically BUFFERING_COMPLETE and RENDERING_COMPLETE
          // ignore BUFFERING_COMPLETE if player is already in COMPLETE state
          if (getState() !== MediaPlayerBase.STATE.COMPLETE) {
            _onFinishedBuffering();
          }
          break

        case PlayerEventCodes.RENDERING_COMPLETE:
          // [optimisation] if Stop() is not called after RENDERING_COMPLETE then player sends periodically BUFFERING_COMPLETE and RENDERING_COMPLETE
          // ignore RENDERING_COMPLETE if player is already in COMPLETE state
          if (getState() !== MediaPlayerBase.STATE.COMPLETE) {
            _onEndOfMedia();
          }
          break

        case PlayerEventCodes.CONNECTION_FAILED:
          _onDeviceError("Media element emitted OnConnectionFailed");
          break

        case PlayerEventCodes.NETWORK_DISCONNECTED:
          _onDeviceError("Media element emitted OnNetworkDisconnected");
          break

        case PlayerEventCodes.AUTHENTICATION_FAILED:
          _onDeviceError("Media element emitted OnAuthenticationFailed");
          break

        case PlayerEventCodes.RENDER_ERROR:
          _onDeviceError("Media element emitted OnRenderError");
          break

        case PlayerEventCodes.STREAM_NOT_FOUND:
          _onDeviceError("Media element emitted OnStreamNotFound");
          break
      }
    };

    window.addEventListener("hide", _onWindowHide, false);
    window.addEventListener("unload", _onWindowHide, false);
  }

  function _onWindowHide() {
    stop();
  }

  function _unregisterEventHandlers() {
    playerPlugin.OnEvent = undefined;
    window.removeEventListener("hide", _onWindowHide, false);
    window.removeEventListener("unload", _onWindowHide, false);
  }

  function _wipe() {
    _stopPlayer();
    _closePlugin();
    _unregisterEventHandlers();

    mediaType = undefined;
    source = undefined;
    mimeType = undefined;
    currentTime = undefined;
    range = undefined;
    deferSeekingTo = null;
    nextSeekingTo = null;
    tryingToPause = false;
    currentTimeKnown = false;
    updatingTime = false;
    lastWindowRanged = false;
  }

  function _seekTo(seconds) {
    const offset = seconds - getCurrentTime();
    const success = _jump(offset);

    if (success === 1) {
      currentTime = seconds;
    }

    return success
  }

  function _seekToWithFailureStateTransition(seconds) {
    const success = _seekTo(seconds);

    if (success !== 1) {
      toPlaying();
    }
  }

  function _jump(offsetSeconds) {
    let result;

    if (offsetSeconds > 0) {
      result = playerPlugin.Execute("JumpForward", offsetSeconds);
      return result
    } else {
      result = playerPlugin.Execute("JumpBackward", Math.abs(offsetSeconds));
      return result
    }
  }

  function _isHlsMimeType() {
    const mime = mimeType.toLowerCase();
    return mime === "application/vnd.apple.mpegurl" || mime === "application/x-mpegurl"
  }

  function _isCurrentTimeInRangeTolerance(seconds) {
    if (seconds > range.end + RANGE_UPDATE_TOLERANCE) {
      return false
    } else if (seconds < range.start - RANGE_UPDATE_TOLERANCE) {
      return false
    } else {
      return true
    }
  }

  function _isInitialBufferingFinished() {
    if (currentTime === undefined || currentTime === 0) {
      return false
    } else {
      return true
    }
  }

  function _reportError(errorMessage) {
    DebugTool.info(errorMessage);
    _emitEvent(MediaPlayerBase.EVENT.ERROR, { errorMessage: errorMessage });
  }

  function _isNearToCurrentTime(seconds) {
    const currentTime = getCurrentTime();
    const targetTime = _getClampedTime(seconds);

    return Math.abs(currentTime - targetTime) <= CURRENT_TIME_TOLERANCE
  }

  function _isLiveMedia() {
    return mediaType === MediaPlayerBase.TYPE.LIVE_VIDEO || mediaType === MediaPlayerBase.TYPE.LIVE_AUDIO
  }

  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));
  }

  return {
    addEventCallback: (thisArg, callback) => {
      eventCallbacks.push({ thisArg, callback });
    },

    removeEventCallback: (callback) => {
      const index = eventCallbacks.findIndex((callbackObj) => callbackObj.callback === callback);

      if (index !== -1) {
        eventCallbacks.splice(index, 1);
      }
    },

    removeAllEventCallbacks: () => {
      eventCallbacks = [];
    },

    initialiseMedia: initialiseMedia,
    playFrom: playFrom,
    beginPlayback: beginPlayback,
    beginPlaybackFrom: beginPlaybackFrom,
    resume: resume,
    pause: pause,
    stop: stop,
    reset: reset,
    getSeekableRange: getSeekableRange,
    getState: getState,
    getPlayerElement: getPlayerElement,
    getSource: getSource,
    getMimeType: getMimeType,
    getCurrentTime: getCurrentTime,
    getDuration: getDuration,
    toPaused: toPaused,
    toPlaying: toPlaying,
  }
}

/**
 * @preserve Copyright (c) 2017-present British Broadcasting Corporation. All rights reserved.
 * @license See https://github.com/fmtvp/tal/blob/master/LICENSE for full licence
 */

function SamsungStreaming2015() {
  let state = MediaPlayerBase.STATE.EMPTY;
  let currentPlayer;
  let deferSeekingTo = null;
  let nextSeekingTo = null;
  let postBufferingState = null;
  let tryingToPause = false;
  let currentTimeKnown = false;
  let updatingTime = false;
  let lastWindowRanged = false;

  let mediaType;
  let source;
  let mimeType;

  let range;
  let currentTime;

  let eventCallbacks = [];

  let playerPlugin;
  let tvmwPlugin;
  let originalSource;

  try {
    _registerSamsungPlugins();
  } catch (ignoreErr) {}

  const PlayerEventCodes = {
    CONNECTION_FAILED: 1,
    AUTHENTICATION_FAILED: 2,
    STREAM_NOT_FOUND: 3,
    NETWORK_DISCONNECTED: 4,
    NETWORK_SLOW: 5,
    RENDER_ERROR: 6,
    RENDERING_START: 7,
    RENDERING_COMPLETE: 8,
    STREAM_INFO_READY: 9,
    DECODING_COMPLETE: 10,
    BUFFERING_START: 11,
    BUFFERING_COMPLETE: 12,
    BUFFERING_PROGRESS: 13,
    CURRENT_PLAYBACK_TIME: 14,
    AD_START: 15,
    AD_END: 16,
    RESOLUTION_CHANGED: 17,
    BITRATE_CHANGED: 18,
    SUBTITLE: 19,
    CUSTOM: 20,
  };

  const PlayerEmps = {
    Player: 0,
    StreamingPlayer: 1,
  };

  /**
   * @constant {Number} Time (in seconds) compared to current time within which seeking has no effect.
   * Jumping to time lower than 3s causes error in PlayFrom60 on HLS live - player jumps to previous chunk.
   * Value set to 4s to be ahead of potential wrong player jumps.
   */
  const CURRENT_TIME_TOLERANCE = 4;
  const CLAMP_OFFSET_FROM_END_OF_LIVE_RANGE = 10;
  const CLAMP_OFFSET_FROM_START_OF_RANGE = 1.1;
  const CLAMP_OFFSET_FROM_END_OF_RANGE = 1.1;
  const RANGE_UPDATE_TOLERANCE = 8;
  const RANGE_END_TOLERANCE = 100;

  function initialiseMedia(type, url, mediaMimeType) {
    if (this.getState() === MediaPlayerBase.STATE.EMPTY) {
      mediaType = type;
      source = url;
      mimeType = mediaMimeType;
      _registerEventHandlers();
      _toStopped();

      if (_isHlsMimeType()) {
        source += "|COMPONENT=HLS";
      }
      _openPlayerPlugin();

      _initPlayer(source);
    } else {
      _toError("Cannot set source unless in the '" + MediaPlayerBase.STATE.EMPTY + "' state");
    }
  }

  function resume() {
    postBufferingState = MediaPlayerBase.STATE.PLAYING;
    switch (getState()) {
      case MediaPlayerBase.STATE.PLAYING:
        break

      case MediaPlayerBase.STATE.BUFFERING:
        if (tryingToPause) {
          tryingToPause = false;
          toPlaying();
        }
        break

      case MediaPlayerBase.STATE.PAUSED:
        playerPlugin.Execute("Resume");
        toPlaying();
        break

      default:
        _toError("Cannot resume while in the '" + getState() + "' state");
        break
    }
  }

  function playFrom(seconds) {
    postBufferingState = MediaPlayerBase.STATE.PLAYING;
    const seekingTo = range ? _getClampedTimeForPlayFrom(seconds) : seconds;

    switch (getState()) {
      case MediaPlayerBase.STATE.BUFFERING:
        // deferSeekingTo = seekingTo;
        nextSeekingTo = seekingTo;
        break

      case MediaPlayerBase.STATE.PLAYING:
        _toBuffering();
        if (!currentTimeKnown) {
          deferSeekingTo = seekingTo;
        } else if (_isNearToCurrentTime(seekingTo)) {
          toPlaying();
        } else {
          _seekToWithFailureStateTransition(seekingTo);
        }
        break

      case MediaPlayerBase.STATE.PAUSED:
        _toBuffering();
        if (!currentTimeKnown) {
          deferSeekingTo = seekingTo;
        } else if (_isNearToCurrentTime(seekingTo)) {
          playerPlugin.Execute("Resume");
          toPlaying();
        } else {
          _seekToWithFailureStateTransition(seekingTo);
          playerPlugin.Execute("Resume");
        }
        break

      case MediaPlayerBase.STATE.COMPLETE:
        playerPlugin.Execute("Stop");
        _initPlayer(source);
        playerPlugin.Execute("StartPlayback", seekingTo);
        _toBuffering();
        break

      default:
        _toError("Cannot playFrom while in the '" + getState() + "' state");
        break
    }
  }

  function beginPlayback() {
    postBufferingState = MediaPlayerBase.STATE.PLAYING;
    switch (getState()) {
      case MediaPlayerBase.STATE.STOPPED:
        _toBuffering();
        playerPlugin.Execute("StartPlayback");
        break

      default:
        _toError("Cannot beginPlayback while in the '" + getState() + "' state");
        break
    }
  }

  function beginPlaybackFrom(seconds) {
    postBufferingState = MediaPlayerBase.STATE.PLAYING;
    let seekingTo = getSeekableRange() ? _getClampedTimeForPlayFrom(seconds) : seconds;

    // StartPlayback from near start of range causes spoiler defect
    if (seekingTo < CLAMP_OFFSET_FROM_START_OF_RANGE && _isLiveMedia()) {
      seekingTo = CLAMP_OFFSET_FROM_START_OF_RANGE;
    } else {
      seekingTo = parseInt(Math.floor(seekingTo), 10);
    }

    switch (getState()) {
      case MediaPlayerBase.STATE.STOPPED:
        playerPlugin.Execute("StartPlayback", seekingTo);
        _toBuffering();
        break

      default:
        _toError("Cannot beginPlayback while in the '" + getState() + "' state");
        break
    }
  }

  function pause() {
    postBufferingState = MediaPlayerBase.STATE.PAUSED;
    switch (getState()) {
      case MediaPlayerBase.STATE.BUFFERING:
      case MediaPlayerBase.STATE.PAUSED:
        break

      case MediaPlayerBase.STATE.PLAYING:
        _tryPauseWithStateTransition();
        break

      default:
        _toError("Cannot pause while in the '" + getState() + "' state");
        break
    }
  }

  function stop() {
    switch (getState()) {
      case MediaPlayerBase.STATE.STOPPED:
        break

      case MediaPlayerBase.STATE.BUFFERING:
      case MediaPlayerBase.STATE.PLAYING:
      case MediaPlayerBase.STATE.PAUSED:
      case MediaPlayerBase.STATE.COMPLETE:
        _stopPlayer();
        _toStopped();
        break

      default:
        _toError("Cannot stop while in the '" + getState() + "' state");
        break
    }
  }

  function reset() {
    switch (getState()) {
      case MediaPlayerBase.STATE.EMPTY:
        break

      case MediaPlayerBase.STATE.STOPPED:
      case MediaPlayerBase.STATE.ERROR:
        _toEmpty();
        break

      default:
        _toError("Cannot reset while in the '" + getState() + "' state");
        break
    }
  }

  function getSource() {
    return source
  }

  function getMimeType() {
    return mimeType
  }

  function getCurrentTime() {
    if (getState() === MediaPlayerBase.STATE.STOPPED) {
      return undefined
    } else {
      return currentTime
    }
  }

  function getSeekableRange() {
    switch (getState()) {
      case MediaPlayerBase.STATE.STOPPED:
      case MediaPlayerBase.STATE.ERROR:
        break

      default:
        return range
    }

    return undefined
  }

  function getDuration() {
    if (range) {
      return range.end
    }

    return undefined
  }

  function getState() {
    return state
  }

  function getPlayerElement() {
    return playerPlugin
  }

  function toPlaying() {
    if (_isHlsMimeType() && _isLiveMedia() && !updatingTime) {
      _updateRange();
    }

    state = MediaPlayerBase.STATE.PLAYING;
    _emitEvent(MediaPlayerBase.EVENT.PLAYING);
  }

  function toPaused() {
    state = MediaPlayerBase.STATE.PAUSED;
    _emitEvent(MediaPlayerBase.EVENT.PAUSED);
  }

  function _toStopped() {
    currentTime = 0;
    range = undefined;
    state = MediaPlayerBase.STATE.STOPPED;
    _emitEvent(MediaPlayerBase.EVENT.STOPPED);
  }

  function _toBuffering() {
    state = MediaPlayerBase.STATE.BUFFERING;
    _emitEvent(MediaPlayerBase.EVENT.BUFFERING);
  }

  function _toComplete() {
    state = MediaPlayerBase.STATE.COMPLETE;
    _emitEvent(MediaPlayerBase.EVENT.COMPLETE);
  }

  function _toEmpty() {
    _wipe();
    state = MediaPlayerBase.STATE.EMPTY;
  }

  function _toError(errorMessage) {
    _wipe();
    state = MediaPlayerBase.STATE.ERROR;
    _reportError(errorMessage);
    throw new Error("ApiError: " + errorMessage)
  }

  function _registerSamsungPlugins() {
    playerPlugin = document.getElementById("sefPlayer");
    tvmwPlugin = document.getElementById("pluginObjectTVMW");
    originalSource = tvmwPlugin.GetSource();
    window.addEventListener(
      "hide",
      () => {
        stop();
        tvmwPlugin.SetSource(originalSource);
      },
      false
    );
  }

  function _getClampedTime(seconds) {
    const range = getSeekableRange();
    const offsetFromEnd = _getClampOffsetFromConfig();
    const nearToEnd = Math.max(range.end - offsetFromEnd, range.start);

    if (seconds < range.start) {
      return range.start
    } else if (seconds > nearToEnd) {
      return nearToEnd
    } else {
      return seconds
    }
  }

  function _openPlayerPlugin() {
    if (currentPlayer !== undefined) {
      playerPlugin.Close();
    }

    playerPlugin.Open("Player", "1.010", "Player");
    currentPlayer = PlayerEmps.Player;
  }

  function _isLiveRangeOutdated() {
    const time = Math.floor(currentTime);

    if (time % 8 === 0 && !updatingTime && lastWindowRanged !== time) {
      lastWindowRanged = time;
      return true
    } else {
      return false
    }
  }

  function _closePlugin() {
    playerPlugin.Close();
    currentPlayer = undefined;
  }

  function _initPlayer(source) {
    const result = playerPlugin.Execute("InitPlayer", source);

    if (result !== 1) {
      _toError("Failed to initialize video: " + source);
    }
  }

  function _onFinishedBuffering() {
    if (getState() !== MediaPlayerBase.STATE.BUFFERING) {
      return
    }

    if (!_isInitialBufferingFinished() && nextSeekingTo !== null) {
      deferSeekingTo = nextSeekingTo;
      nextSeekingTo = null;
    }

    if (deferSeekingTo === null) {
      if (postBufferingState === MediaPlayerBase.STATE.PAUSED) {
        _tryPauseWithStateTransition();
      } else {
        toPlaying();
      }
    }
  }

  function _onDeviceError(message) {
    _reportError(message);
  }

  function _onDeviceBuffering() {
    if (getState() === MediaPlayerBase.STATE.PLAYING) {
      _toBuffering();
    }
  }

  function _onEndOfMedia() {
    _toComplete();
  }

  function _stopPlayer() {
    playerPlugin.Execute("Stop");
    currentTimeKnown = false;
  }

  function _tryPauseWithStateTransition() {
    let success = playerPlugin.Execute("Pause");
    success = success && success !== -1;

    if (success) {
      toPaused();
    }

    tryingToPause = !success;
  }

  function _onStatus() {
    const state = getState();

    if (state === MediaPlayerBase.STATE.PLAYING) {
      _emitEvent(MediaPlayerBase.EVENT.STATUS);
    }
  }

  function _updateRange() {
    if (_isHlsMimeType() && _isLiveMedia()) {
      const playingRange = playerPlugin.Execute("GetLiveDuration").split("|");

      range = {
        start: Math.floor(playingRange[0] / 1000),
        end: Math.floor(playingRange[1] / 1000),
      };

      // don't call range for the next 8 seconds
      updatingTime = true;
      setTimeout(() => {
        updatingTime = false;
      }, RANGE_UPDATE_TOLERANCE * 1000);
    } else {
      const duration = playerPlugin.Execute("GetDuration") / 1000;
      range = {
        start: 0,
        end: duration,
      };
    }
  }

  function _onCurrentTime(timeInMillis) {
    currentTime = timeInMillis / 1000;
    _onStatus();
    currentTimeKnown = true;

    // [optimisation] do not call player API periodically in HLS live
    // - calculate range manually when possible
    // - do not calculate range if player API was called less than RANGE_UPDATE_TOLERANCE seconds ago
    if (_isLiveMedia() && _isLiveRangeOutdated()) {
      range.start += 8;
      range.end += 8;
    }

    if (nextSeekingTo !== null) {
      deferSeekingTo = nextSeekingTo;
      nextSeekingTo = null;
    }

    if (deferSeekingTo !== null) {
      _deferredSeek();
    }

    if (tryingToPause) {
      _tryPauseWithStateTransition();
    }
  }

  function _deferredSeek() {
    const clampedTime = _getClampedTimeForPlayFrom(deferSeekingTo);
    const isNearCurrentTime = _isNearToCurrentTime(clampedTime);

    if (isNearCurrentTime) {
      toPlaying();
      deferSeekingTo = null;
    } else {
      const seekResult = _seekTo(clampedTime);
      if (seekResult) {
        deferSeekingTo = null;
      }
    }
  }

  function _getClampedTimeForPlayFrom(seconds) {
    if (_isHlsMimeType() && _isLiveMedia() && !updatingTime) {
      _updateRange();
    }

    const clampedTime = _getClampedTime(seconds);

    if (clampedTime !== seconds) {
      DebugTool.info(
        "playFrom " +
          seconds +
          " clamped to " +
          clampedTime +
          " - seekable range is { start: " +
          range.start +
          ", end: " +
          range.end +
          " }"
      );
    }

    return clampedTime
  }

  function _getClampOffsetFromConfig() {
    if (_isLiveMedia()) {
      return CLAMP_OFFSET_FROM_END_OF_LIVE_RANGE
    } else {
      return CLAMP_OFFSET_FROM_END_OF_RANGE
    }
  }

  function _registerEventHandlers() {
    playerPlugin.OnEvent = (eventType, param1) => {
      switch (eventType) {
        case PlayerEventCodes.STREAM_INFO_READY:
          _updateRange();
          _emitEvent(MediaPlayerBase.EVENT.METADATA);
          break

        case PlayerEventCodes.CURRENT_PLAYBACK_TIME:
          if (range && _isLiveMedia()) {
            const seconds = Math.floor(param1 / 1000);

            // jump to previous current time if PTS out of range occurs
            if (seconds > range.end + RANGE_END_TOLERANCE) {
              playFrom(currentTime);
              break
              // call GetPlayingRange() on SEF emp if current time is out of range
            } else if (!_isCurrentTimeInRangeTolerance(seconds)) {
              _updateRange();
            }
          }
          _onCurrentTime(param1);
          break

        case PlayerEventCodes.BUFFERING_START:
        case PlayerEventCodes.BUFFERING_PROGRESS:
          _onDeviceBuffering();
          break

        case PlayerEventCodes.BUFFERING_COMPLETE:
          // For live HLS, don't update the range more than once every 8 seconds
          if (!updatingTime) {
            _updateRange();
          }

          // [optimisation] if Stop() is not called after RENDERING_COMPLETE then player sends periodically BUFFERING_COMPLETE and RENDERING_COMPLETE
          // ignore BUFFERING_COMPLETE if player is already in COMPLETE state
          if (getState() !== MediaPlayerBase.STATE.COMPLETE) {
            _onFinishedBuffering();
          }
          break

        case PlayerEventCodes.RENDERING_COMPLETE:
          // [optimisation] if Stop() is not called after RENDERING_COMPLETE then player sends periodically BUFFERING_COMPLETE and RENDERING_COMPLETE
          // ignore RENDERING_COMPLETE if player is already in COMPLETE state
          if (getState() !== MediaPlayerBase.STATE.COMPLETE) {
            _onEndOfMedia();
          }
          break

        case PlayerEventCodes.CONNECTION_FAILED:
          _onDeviceError("Media element emitted OnConnectionFailed");
          break

        case PlayerEventCodes.NETWORK_DISCONNECTED:
          _onDeviceError("Media element emitted OnNetworkDisconnected");
          break

        case PlayerEventCodes.AUTHENTICATION_FAILED:
          _onDeviceError("Media element emitted OnAuthenticationFailed");
          break

        case PlayerEventCodes.RENDER_ERROR:
          _onDeviceError("Media element emitted OnRenderError");
          break

        case PlayerEventCodes.STREAM_NOT_FOUND:
          _onDeviceError("Media element emitted OnStreamNotFound");
          break
      }
    };

    window.addEventListener("hide", _onWindowHide, false);
    window.addEventListener("unload", _onWindowHide, false);
  }

  function _onWindowHide() {
    stop();
  }

  function _unregisterEventHandlers() {
    playerPlugin.OnEvent = undefined;
    window.removeEventListener("hide", _onWindowHide, false);
    window.removeEventListener("unload", _onWindowHide, false);
  }

  function _wipe() {
    _stopPlayer();
    _closePlugin();
    _unregisterEventHandlers();

    mediaType = undefined;
    source = undefined;
    mimeType = undefined;
    currentTime = undefined;
    range = undefined;
    deferSeekingTo = null;
    nextSeekingTo = null;
    tryingToPause = false;
    currentTimeKnown = false;
    updatingTime = false;
    lastWindowRanged = false;
  }

  function _seekTo(seconds) {
    const offset = seconds - getCurrentTime();
    const success = _jump(offset);

    if (success === 1) {
      currentTime = seconds;
    }

    return success
  }

  function _seekToWithFailureStateTransition(seconds) {
    const success = _seekTo(seconds);

    if (success !== 1) {
      toPlaying();
    }
  }

  function _jump(offsetSeconds) {
    let result;

    if (offsetSeconds > 0) {
      result = playerPlugin.Execute("JumpForward", offsetSeconds);
      return result
    } else {
      result = playerPlugin.Execute("JumpBackward", Math.abs(offsetSeconds));
      return result
    }
  }

  function _isHlsMimeType() {
    const mime = mimeType.toLowerCase();
    return mime === "application/vnd.apple.mpegurl" || mime === "application/x-mpegurl"
  }

  function _isCurrentTimeInRangeTolerance(seconds) {
    if (seconds > range.end + RANGE_UPDATE_TOLERANCE) {
      return false
    } else if (seconds < range.start - RANGE_UPDATE_TOLERANCE) {
      return false
    } else {
      return true
    }
  }

  function _isInitialBufferingFinished() {
    if (currentTime === undefined || currentTime === 0) {
      return false
    } else {
      return true
    }
  }

  function _reportError(errorMessage) {
    DebugTool.info(errorMessage);
    _emitEvent(MediaPlayerBase.EVENT.ERROR, { errorMessage: errorMessage });
  }

  function _isNearToCurrentTime(seconds) {
    const currentTime = getCurrentTime();
    const targetTime = _getClampedTime(seconds);

    return Math.abs(currentTime - targetTime) <= CURRENT_TIME_TOLERANCE
  }

  function _isLiveMedia() {
    return mediaType === MediaPlayerBase.TYPE.LIVE_VIDEO || mediaType === MediaPlayerBase.TYPE.LIVE_AUDIO
  }

  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));
  }

  return {
    addEventCallback: (thisArg, callback) => {
      eventCallbacks.push({
        thisArg,
        callback,
      });
    },

    removeEventCallback: (callback) => {
      const index = eventCallbacks.findIndex((callbackObj) => callbackObj.callback === callback);

      if (index !== -1) {
        eventCallbacks.splice(index, 1);
      }
    },

    removeAllEventCallbacks: () => {
      eventCallbacks = [];
    },

    initialiseMedia: initialiseMedia,
    playFrom: playFrom,
    beginPlayback: beginPlayback,
    beginPlaybackFrom: beginPlaybackFrom,
    resume: resume,
    pause: pause,
    stop: stop,
    reset: reset,
    getSeekableRange: getSeekableRange,
    getState: getState,
    getPlayerElement: getPlayerElement,
    getSource: getSource,
    getMimeType: getMimeType,
    getCurrentTime: getCurrentTime,
    getDuration: getDuration,
    toPaused: toPaused,
    toPlaying: toPlaying,
  }
}

function None() {
  throw new Error("Cannot create a none live support player")
}

function PlayableLivePlayer(mediaPlayer) {
  return {
    initialiseMedia: (mediaType, sourceUrl, mimeType, sourceContainer, opts) => {
      const liveMediaType =
        mediaType === MediaPlayerBase.TYPE.AUDIO ? MediaPlayerBase.TYPE.LIVE_AUDIO : MediaPlayerBase.TYPE.LIVE_VIDEO;

      mediaPlayer.initialiseMedia(liveMediaType, sourceUrl, mimeType, sourceContainer, opts);
    },

    beginPlayback: () => mediaPlayer.beginPlayback(),
    stop: () => mediaPlayer.stop(),
    reset: () => mediaPlayer.reset(),
    getState: () => mediaPlayer.getState(),
    getSource: () => mediaPlayer.getSource(),
    getMimeType: () => mediaPlayer.getMimeType(),
    getPlayerElement: () => mediaPlayer.getPlayerElement(),

    addEventCallback: (thisArg, callback) => mediaPlayer.addEventCallback(thisArg, callback),
    removeEventCallback: (callback) => mediaPlayer.removeEventCallback(callback),
    removeAllEventCallbacks: () => mediaPlayer.removeAllEventCallbacks(),
  }
}

function RestartableLivePlayer(mediaPlayer) {
  return {
    beginPlayback: () => {
      if (
        window.bigscreenPlayer &&
        window.bigscreenPlayer.overrides &&
        window.bigscreenPlayer.overrides.forceBeginPlaybackToEndOfWindow
      ) {
        mediaPlayer.beginPlaybackFrom(Infinity);
      } else {
        mediaPlayer.beginPlayback();
      }
    },

    beginPlaybackFrom: (presentationTimeInSeconds) => {
      mediaPlayer.beginPlaybackFrom(presentationTimeInSeconds);
    },

    initialiseMedia: (mediaType, sourceUrl, mimeType, sourceContainer, opts) => {
      const mediaSubType =
        mediaType === MediaPlayerBase.TYPE.AUDIO ? MediaPlayerBase.TYPE.LIVE_AUDIO : MediaPlayerBase.TYPE.LIVE_VIDEO;

      mediaPlayer.initialiseMedia(mediaSubType, sourceUrl, mimeType, sourceContainer, opts);
    },
    stop: () => mediaPlayer.stop(),
    reset: () => mediaPlayer.reset(),
    getState: () => mediaPlayer.getState(),
    getSource: () => mediaPlayer.getSource(),
    getMimeType: () => mediaPlayer.getMimeType(),
    getPlayerElement: () => mediaPlayer.getPlayerElement(),
    addEventCallback: (thisArg, callback) => mediaPlayer.addEventCallback(thisArg, callback),
    removeEventCallback: (callback) => mediaPlayer.removeEventCallback(callback),
    removeAllEventCallbacks: () => mediaPlayer.removeAllEventCallbacks(),
  }
}

const AUTO_RESUME_WINDOW_START_CUSHION_SECONDS = 8;
const FOUR_MINUTES = 4 * 60;
function isSeekableRange(obj) {
    return (obj != null &&
        typeof obj === "object" &&
        "start" in obj &&
        "end" in obj &&
        typeof obj.start === "number" &&
        typeof obj.end === "number" &&
        isFinite(obj.start) &&
        isFinite(obj.end));
}
function isSeekableRangeBigEnough({ start, end }) {
    return end - start > FOUR_MINUTES;
}
function canPauseAndSeek(liveSupport, seekableRange) {
    return (liveSupport === LiveSupport.SEEKABLE && isSeekableRange(seekableRange) && isSeekableRangeBigEnough(seekableRange));
}
function autoResumeAtStartOfRange(currentTime, seekableRange, addEventCallback, removeEventCallback, checkNotPauseEvent, resume, timeShiftBufferDepthInSeconds) {
    const { start, end } = seekableRange;
    const duration = end - start;
    const windowLengthInSeconds = timeShiftBufferDepthInSeconds && duration < timeShiftBufferDepthInSeconds ? timeShiftBufferDepthInSeconds : duration;
    const resumeTimeOut = Math.max(0, windowLengthInSeconds - (end - currentTime) - AUTO_RESUME_WINDOW_START_CUSHION_SECONDS);
    DebugTool.dynamicMetric("auto-resume", resumeTimeOut);
    const autoResumeTimer = setTimeout(() => {
        removeEventCallback(undefined, detectIfUnpaused);
        resume();
    }, resumeTimeOut * 1000);
    addEventCallback(undefined, detectIfUnpaused);
    function detectIfUnpaused(event) {
        if (checkNotPauseEvent(event)) {
            removeEventCallback(undefined, detectIfUnpaused);
            clearTimeout(autoResumeTimer);
        }
    }
}

const TEN_SECONDS_IN_MILLISECONDS = 10000;
function isValidSeekableRange(obj) {
    return (obj != null &&
        typeof obj === "object" &&
        "start" in obj &&
        "end" in obj &&
        typeof obj.start === "number" &&
        typeof obj.end === "number" &&
        isFinite(obj.start) &&
        obj.end > obj.start);
}
function createTimeShiftDetector(onceDetected) {
    let currentIntervalId;
    let lastSeekableRangeStart;
    let isSliding = false;
    function observe(getSeekableRange) {
        if (currentIntervalId != null) {
            disconnect();
        }
        const initialRange = getSeekableRange();
        lastSeekableRangeStart = isValidSeekableRange(initialRange) ? initialRange.start : undefined;
        currentIntervalId = setInterval(() => {
            const currentRange = getSeekableRange();
            const currentSeekableRangeStart = isValidSeekableRange(currentRange) ? currentRange.start : undefined;
            if (typeof lastSeekableRangeStart === "number" &&
                typeof currentSeekableRangeStart === "number" &&
                currentSeekableRangeStart > lastSeekableRangeStart) {
                isSliding = true;
                onceDetected();
                disconnect();
            }
            lastSeekableRangeStart = currentSeekableRangeStart;
        }, TEN_SECONDS_IN_MILLISECONDS);
    }
    function disconnect() {
        clearInterval(currentIntervalId);
        currentIntervalId = undefined;
    }
    function isSeekableRangeSliding() {
        return isSliding;
    }
    return { disconnect, isSeekableRangeSliding, observe };
}

function SeekableLivePlayer(mediaPlayer) {
  const AUTO_RESUME_WINDOW_START_CUSHION_SECONDS = 8;

  const timeShiftDetector = createTimeShiftDetector(() => {
    if (getState() !== MediaPlayerBase.STATE.PAUSED) {
      return
    }

    startAutoResumeTimeout();
  });

  mediaPlayer.addEventCallback(null, (event) => {
    if (event.type === MediaPlayerBase.EVENT.METADATA) {
      // Avoid observing the seekable range before metadata loads
      timeShiftDetector.observe(getSeekableRange);
    }
  });

  function addEventCallback(thisArg, callback) {
    mediaPlayer.addEventCallback(thisArg, callback);
  }

  function removeEventCallback(callback) {
    mediaPlayer.removeEventCallback(callback);
  }

  function removeAllEventCallbacks() {
    mediaPlayer.removeAllEventCallbacks();
  }

  function resume() {
    mediaPlayer.resume();
  }

  function getState() {
    return mediaPlayer.getState()
  }

  function getSeekableRange() {
    return mediaPlayer.getSeekableRange()
  }

  function reset() {
    timeShiftDetector.disconnect();
    mediaPlayer.reset();
  }

  function stop() {
    timeShiftDetector.disconnect();
    mediaPlayer.stop();
  }

  function startAutoResumeTimeout() {
    autoResumeAtStartOfRange(
      mediaPlayer.getCurrentTime(),
      mediaPlayer.getSeekableRange(),
      addEventCallback,
      removeEventCallback,
      MediaPlayerBase.unpausedEventCheck,
      resume
    );
  }

  return {
    initialiseMedia: function initialiseMedia(mediaKind, sourceUrl, mimeType, sourceContainer, opts) {
      const mediaType =
        mediaKind === MediaPlayerBase.TYPE.AUDIO ? MediaPlayerBase.TYPE.LIVE_AUDIO : MediaPlayerBase.TYPE.LIVE_VIDEO;

      mediaPlayer.initialiseMedia(mediaType, sourceUrl, mimeType, sourceContainer, opts);
    },

    beginPlayback: function beginPlayback() {
      if (window.bigscreenPlayer?.overrides?.forceBeginPlaybackToEndOfWindow) {
        mediaPlayer.beginPlaybackFrom(Infinity);
      } else {
        mediaPlayer.beginPlayback();
      }
    },

    beginPlaybackFrom: function beginPlaybackFrom(presentationTimeInSeconds) {
      mediaPlayer.beginPlaybackFrom(presentationTimeInSeconds);
    },

    playFrom: function playFrom(presentationTimeInSeconds) {
      mediaPlayer.playFrom(presentationTimeInSeconds);
    },

    pause: function pause() {
      if (
        mediaPlayer.getCurrentTime() - mediaPlayer.getSeekableRange().start <=
        AUTO_RESUME_WINDOW_START_CUSHION_SECONDS
      ) {
        mediaPlayer.toPaused();
        mediaPlayer.toPlaying();

        return
      }

      mediaPlayer.pause();

      if (timeShiftDetector.isSeekableRangeSliding()) {
        startAutoResumeTimeout();
      }
    },

    resume,
    stop,
    reset,
    getState,
    getSource: () => mediaPlayer.getSource(),
    getCurrentTime: () => mediaPlayer.getCurrentTime(),
    getSeekableRange,
    getMimeType: () => mediaPlayer.getMimeType(),
    addEventCallback,
    removeEventCallback,
    removeAllEventCallbacks,
    getPlayerElement: () => mediaPlayer.getPlayerElement(),
    getLiveSupport: () => MediaPlayerBase.LIVE_SUPPORT.SEEKABLE,
  }
}

const getBasePlayer = () => {
  const configuredPlayer = window.bigscreenPlayer?.mediaPlayer;

  if (configuredPlayer === "cehtml") return Cehtml()
  if (configuredPlayer === "samsungmaple") return SamsungMaple()
  if (configuredPlayer === "samsungstreaming") return SamsungStreaming()
  if (configuredPlayer === "samsungstreaming2015") return SamsungStreaming2015()

  return Html5()
};

const getMediaPlayer = (mediaSources) => {
  const basePlayer = getBasePlayer();
  const liveSupport = window.bigscreenPlayer?.liveSupport;

  if (mediaSources.time().manifestType !== ManifestType.DYNAMIC) return basePlayer

  if (liveSupport === "none") return None()
  if (liveSupport === "restartable") return RestartableLivePlayer(basePlayer)
  if (liveSupport === "seekable") return SeekableLivePlayer(basePlayer)

  return PlayableLivePlayer(basePlayer)
};

const NativeStrategy = (mediaSources, _mediaKind, playbackElement, isUHD) =>
  LegacyPlayerAdapter(mediaSources, playbackElement, isUHD, getMediaPlayer(mediaSources));

NativeStrategy.getLiveSupport = () => window.bigscreenPlayer.liveSupport;

const AUDIO = "audio";
const VIDEO = "video";
const MediaKinds = { AUDIO, VIDEO };

function BasicStrategy(mediaSources, mediaKind, playbackElement) {
  const CLAMP_OFFSET_SECONDS = 1.1;
  const manifestType = mediaSources.time().manifestType;

  let eventCallbacks = [];
  let errorCallback;
  let timeUpdateCallback;

  let mediaElement;
  let metaDataLoaded;

  const timeShiftDetector = createTimeShiftDetector(() => {
    if (!isPaused()) {
      return
    }

    startAutoResumeTimeout();
  });

  function publishMediaState(mediaState) {
    for (let index = 0; index < eventCallbacks.length; index++) {
      eventCallbacks[index](mediaState);
    }
  }

  function publishTimeUpdate() {
    if (timeUpdateCallback) {
      timeUpdateCallback();
    }
  }

  function publishError(mediaError) {
    if (errorCallback) {
      errorCallback(mediaError);
    }
  }

  function load(_mimeType, startTime) {
    if (mediaElement == null) {
      setUpMediaElement(startTime);
      setUpMediaListeners();

      return
    }

    mediaElement.src = mediaSources.currentSource();
    setStartTime(startTime);
    mediaElement.load();
  }

  function setUpMediaElement(startTime) {
    mediaElement = mediaKind === MediaKinds.AUDIO ? document.createElement("audio") : document.createElement("video");

    mediaElement.style.position = "absolute";
    mediaElement.style.width = "100%";
    mediaElement.style.height = "100%";
    mediaElement.autoplay = true;
    mediaElement.preload = "auto";
    mediaElement.src = mediaSources.currentSource();

    playbackElement.insertBefore(mediaElement, playbackElement.firstChild);

    setStartTime(startTime);
    mediaElement.load();
  }

  function setUpMediaListeners() {
    mediaElement.addEventListener("timeupdate", onTimeUpdate);
    mediaElement.addEventListener("playing", onPlaying);
    mediaElement.addEventListener("pause", onPaused);
    mediaElement.addEventListener("waiting", onWaiting);
    mediaElement.addEventListener("seeking", onSeeking);
    mediaElement.addEventListener("seeked", onSeeked);
    mediaElement.addEventListener("ended", onEnded);
    mediaElement.addEventListener("error", onError);
    mediaElement.addEventListener("loadedmetadata", onLoadedMetadata);
  }

  function setStartTime(presentationTimeInSeconds) {
    if (presentationTimeInSeconds || presentationTimeInSeconds === 0) {
      // currentTime = 0 is interpreted as play from live point by many devices
      const startTimeInSeconds =
        manifestType === ManifestType.DYNAMIC && presentationTimeInSeconds === 0 ? 0.1 : presentationTimeInSeconds;

      mediaElement.currentTime = startTimeInSeconds;
    }
  }

  function onPlaying() {
    publishMediaState(MediaState.PLAYING);
  }

  function onPaused() {
    publishMediaState(MediaState.PAUSED);
  }

  function onSeeking() {
    publishMediaState(MediaState.WAITING);
  }

  function onWaiting() {
    publishMediaState(MediaState.WAITING);
  }

  function onSeeked() {
    if (isPaused()) {
      if (timeShiftDetector.isSeekableRangeSliding()) {
        startAutoResumeTimeout();
      }

      publishMediaState(MediaState.PAUSED);
    } else {
      publishMediaState(MediaState.PLAYING);
    }
  }

  function onEnded() {
    publishMediaState(MediaState.ENDED);
  }

  function onTimeUpdate() {
    DebugTool.updateElementTime(mediaElement.currentTime);

    publishTimeUpdate();
  }

  function onError(_event) {
    const mediaError = {
      code: (mediaElement && mediaElement.error && mediaElement.error.code) || 0,
      message: (mediaElement && mediaElement.error && mediaElement.error.message) || "unknown",
    };
    publishError(mediaError);
  }

  function onLoadedMetadata() {
    metaDataLoaded = true;

    if (manifestType === ManifestType.DYNAMIC) {
      timeShiftDetector.observe(getSeekableRange);
    }
  }

  function isPaused() {
    return mediaElement.paused
  }

  function getSeekableRange() {
    if (mediaElement && mediaElement.seekable && mediaElement.seekable.length > 0 && metaDataLoaded) {
      return {
        start: mediaElement.seekable.start(0),
        end: mediaElement.seekable.end(0),
      }
    }
    return {
      start: 0,
      end: 0,
    }
  }

  function getDuration() {
    if (mediaElement && metaDataLoaded) {
      return mediaElement.duration
    }

    return 0
  }

  function getCurrentTime() {
    return mediaElement ? mediaElement.currentTime : 0
  }

  function addEventCallback(thisArg, newCallback) {
    const eventCallback = (event) => newCallback.call(thisArg, event);
    eventCallbacks.push(eventCallback);
  }

  function removeEventCallback(callback) {
    const index = eventCallbacks.indexOf(callback);

    if (index !== -1) {
      eventCallbacks.splice(index, 1);
    }
  }

  function startAutoResumeTimeout() {
    autoResumeAtStartOfRange(
      getCurrentTime(),
      getSeekableRange(),
      addEventCallback,
      removeEventCallback,
      (event) => event !== MediaState.PAUSED,
      play
    );
  }

  function play() {
    handlePlayPromise(mediaElement.play());
  }

  function setCurrentTime(presentationTimeInSeconds) {
    // Without metadata we cannot clamp to seekableRange
    mediaElement.currentTime = metaDataLoaded
      ? getClampedTime(presentationTimeInSeconds, getSeekableRange())
      : presentationTimeInSeconds;
  }

  function setPlaybackRate(rate) {
    mediaElement.playbackRate = rate;
  }

  function getPlaybackRate() {
    return mediaElement.playbackRate
  }

  function getClampedTime(time, range) {
    return Math.min(Math.max(time, range.start), range.end - CLAMP_OFFSET_SECONDS)
  }

  function addErrorCallback(thisArg, newErrorCallback) {
    errorCallback = (event) => newErrorCallback.call(thisArg, event);
  }

  function addTimeUpdateCallback(thisArg, newTimeUpdateCallback) {
    timeUpdateCallback = () => newTimeUpdateCallback.call(thisArg);
  }

  function tearDown() {
    if (mediaElement) {
      mediaElement.removeEventListener("timeupdate", onTimeUpdate);
      mediaElement.removeEventListener("playing", onPlaying);
      mediaElement.removeEventListener("pause", onPaused);
      mediaElement.removeEventListener("waiting", onWaiting);
      mediaElement.removeEventListener("seeking", onSeeking);
      mediaElement.removeEventListener("seeked", onSeeked);
      mediaElement.removeEventListener("ended", onEnded);
      mediaElement.removeEventListener("error", onError);
      mediaElement.removeEventListener("loadedmetadata", onLoadedMetadata);
      mediaElement.removeAttribute("src");
      mediaElement.load();
      DOMHelpers.safeRemoveElement(mediaElement);
    }

    timeShiftDetector.disconnect();

    eventCallbacks = [];
    errorCallback = undefined;
    timeUpdateCallback = undefined;

    mediaElement = undefined;
    metaDataLoaded = undefined;
  }

  function reset() {}

  function isEnded() {
    return mediaElement.ended
  }

  function pause() {
    mediaElement.pause();

    if (timeShiftDetector.isSeekableRangeSliding()) {
      startAutoResumeTimeout();
    }
  }

  function getPlayerElement() {
    return mediaElement || undefined
  }

  return {
    transitions: {
      canBePaused: () => true,
      canBeginSeek: () => true,
    },
    addEventCallback,
    removeEventCallback,
    addErrorCallback,
    addTimeUpdateCallback,
    load,
    getSeekableRange,
    getCurrentTime,
    getDuration,
    tearDown,
    reset,
    isEnded,
    isPaused,
    pause,
    play,
    setCurrentTime,
    setPlaybackRate,
    getPlaybackRate,
    getPlayerElement,
  }
}

BasicStrategy.getLiveSupport = () => LiveSupport.SEEKABLE;

function StrategyPicker() {
  return new Promise((resolve, reject) => {
    if (window.bigscreenPlayer.playbackStrategy === PlaybackStrategy.MSE) {
      return import('./msestrategy-e1f8e32c.js')
        .then(({ default: MSEStrategy }) => resolve(MSEStrategy))
        .catch((reason) => {
          const error = new Error(isError(reason) ? reason.message : undefined);
          error.name = "StrategyDynamicLoadError";

          reject(error);
        })
    } else if (window.bigscreenPlayer.playbackStrategy === PlaybackStrategy.BASIC) {
      return resolve(BasicStrategy)
    }
    return resolve(NativeStrategy)
  })
}

function durationToSeconds(duration) {
    const matches = duration.match(/^PT(\d+(?:[,.]\d+)?H)?(\d+(?:[,.]\d+)?M)?(\d+(?:[,.]\d+)?S)?/) || [];
    const hours = parseFloat(matches[1] || "0") * 60 * 60;
    const mins = parseFloat(matches[2] || "0") * 60;
    const secs = parseFloat(matches[3] || "0");
    return hours + mins + secs || undefined;
}
function presentationTimeToAvailabilityTimeInMilliseconds(presentationTimeInSeconds, availabilityStartTimeInMilliseconds) {
    return presentationTimeInSeconds * 1000 + availabilityStartTimeInMilliseconds;
}
function availabilityTimeToPresentationTimeInSeconds(availabilityTimeInMilliseconds, availabilityStartTimeInMilliseconds) {
    return availabilityTimeInMilliseconds < availabilityStartTimeInMilliseconds
        ? 0
        : (availabilityTimeInMilliseconds - availabilityStartTimeInMilliseconds) / 1000;
}
function presentationTimeToMediaSampleTimeInSeconds(presentationTimeInSeconds, presentationTimeOffsetInMilliseconds) {
    return presentationTimeInSeconds + presentationTimeOffsetInMilliseconds / 1000;
}
function mediaSampleTimeToPresentationTimeInSeconds(mediaSampleTimeInSeconds, presentationTimeOffsetInMilliseconds) {
    const presentationTimeOffsetInSeconds = presentationTimeOffsetInMilliseconds / 1000;
    return mediaSampleTimeInSeconds < presentationTimeOffsetInSeconds
        ? 0
        : mediaSampleTimeInSeconds - presentationTimeOffsetInSeconds;
}

function PluginData(args) {
  this.status = args.status;
  this.stateType = args.stateType;
  this.isBufferingTimeoutError = args.isBufferingTimeoutError || false;
  this.isInitialPlay = args.isInitialPlay;
  this.cdn = args.cdn;
  this.newCdn = args.newCdn;
  this.timeStamp = new Date();
  this.code = args.code;
  this.message = args.message;
}

var PluginEnums = {
  STATUS: {
    STARTED: "started",
    DISMISSED: "dismissed",
    FATAL: "fatal",
    FAILOVER: "failover",
  },
  TYPE: {
    BUFFERING: "buffering",
    ERROR: "error",
  },
  ERROR_CODES: {
    MANIFEST_PARSE: 7,
    BUFFERING_TIMEOUT: 8,
    MANIFEST_LOAD: 9,
  },
  ERROR_MESSAGES: {
    BUFFERING_TIMEOUT: "bigscreen-player-buffering-timeout-error",
    MANIFEST: "bigscreen-player-manifest-error",
  },
};

var Utils = {
  clone: (args) => {
    const clone = {};
    for (const prop in args) {
      if (args.hasOwnProperty(prop)) {
        clone[prop] = args[prop];
      }
    }
    return clone
  },

  deepClone: function (objectToClone) {
    if (!objectToClone) {
      return objectToClone
    }

    let clone, propValue, propName;
    clone = Array.isArray(objectToClone) ? [] : {};
    for (propName in objectToClone) {
      propValue = objectToClone[propName];

      // check for date
      if (propValue && Object.prototype.toString.call(propValue) === "[object Date]") {
        clone[propName] = new Date(propValue);
        continue
      }

      clone[propName] = typeof propValue === "object" ? this.deepClone(propValue) : propValue;
    }
    return clone
  },

  cloneArray: function (arr) {
    const clone = [];

    for (let i = 0, n = arr.length; i < n; i++) {
      clone.push(this.clone(arr[i]));
    }

    return clone
  },

  merge: function () {
    const merged = {};

    for (let i = 0; i < arguments.length; i++) {
      const obj = arguments[i];
      for (const prop in obj) {
        if (obj.hasOwnProperty(prop)) {
          if (Object.prototype.toString.call(obj[prop]) === "[object Object]") {
            merged[prop] = this.merge(merged[prop], obj[prop]);
          } else {
            merged[prop] = obj[prop];
          }
        }
      }
    }

    return merged
  },

  arrayStartsWith: (array, partial) => {
    for (let i = 0; i < partial.length; i++) {
      if (array[i] !== partial[i]) {
        return false
      }
    }

    return true
  },

  find: (array, predicate) => {
    return array.reduce((acc, it, i) => {
      return acc !== false ? acc : predicate(it) && it
    }, false)
  },

  findIndex: (array, predicate) => {
    return array.reduce((acc, it, i) => {
      return acc !== false ? acc : predicate(it) && i
    }, false)
  },

  swap: (array, i, j) => {
    const arr = array.slice();
    const temp = arr[i];

    arr[i] = arr[j];
    arr[j] = temp;

    return arr
  },

  pluck: (array, property) => {
    const plucked = [];

    for (let i = 0; i < array.length; i++) {
      plucked.push(array[i][property]);
    }

    return plucked
  },

  flatten: (arr) => [].concat.apply([], arr),

  without: (arr, value) => {
    const newArray = [];

    for (let i = 0; i < arr.length; i++) {
      if (arr[i] !== value) {
        newArray.push(arr[i]);
      }
    }

    return newArray
  },

  contains: (arr, subset) => {
    return [].concat(subset).every((item) => {
      return [].concat(arr).indexOf(item) > -1
    })
  },

  pickRandomFromArray: (arr) => {
    return arr[Math.floor(Math.random() * arr.length)]
  },

  filter: (arr, predicate) => {
    const filteredArray = [];

    for (let i = 0; i < arr.length; i++) {
      if (predicate(arr[i])) {
        filteredArray.push(arr[i]);
      }
    }

    return filteredArray
  },

  noop: () => {},

  generateUUID: () => {
    let d = new Date().getTime();

    return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
      const r = (d + Math.random() * 16) % 16 | 0;
      d = Math.floor(d / 16);
      return (c === "x" ? r : (r & 0x3) | 0x8).toString(16)
    })
  },

  path: (object, keys) => {
    return (keys || []).reduce((accum, key) => {
      return (accum || {})[key]
    }, object || {})
  },
};

function DeferExceptions(callback) {
    try {
        callback();
    }
    catch (error) {
        setTimeout(() => {
            throw error;
        }, 0);
    }
}

function CallCallbacks(callbacks, data) {
    const originalCallbacks = Utils.deepClone(callbacks);
    for (let index = callbacks.length - 1; index >= 0; index--) {
        const originalLength = callbacks.length;
        DeferExceptions(() => callbacks[index](data));
        const newLength = callbacks.length;
        const callbackRemovedSelf = callbacks.indexOf(originalCallbacks[index]) === -1;
        if (originalLength !== newLength && !callbackRemovedSelf) {
            index = index - (originalLength - newLength);
        }
    }
}

let plugins = [];

function callOnAllPlugins(funcKey, evt) {
  const clonedEvent = Utils.deepClone(evt);
  const selectedPlugins = plugins
    .filter((plugin) => plugin[funcKey] && typeof plugin[funcKey] === "function")
    .map((plugin) => plugin[funcKey].bind(plugin));

  CallCallbacks(selectedPlugins, clonedEvent);
}

var Plugins = {
  registerPlugin: (plugin) => {
    plugins.push(plugin);
  },

  unregisterPlugin: (plugin) => {
    if (!plugin && plugins.length > 0) {
      plugins = [];
    } else {
      for (let pluginsIndex = plugins.length - 1; pluginsIndex >= 0; pluginsIndex--) {
        if (plugins[pluginsIndex] === plugin) {
          plugins.splice(pluginsIndex, 1);
        }
      }
    }
  },

  interface: {
    onError: (evt) => callOnAllPlugins("onError", evt),
    onFatalError: (evt) => callOnAllPlugins("onFatalError", evt),
    onErrorCleared: (evt) => callOnAllPlugins("onErrorCleared", evt),
    onErrorHandled: (evt) => callOnAllPlugins("onErrorHandled", evt),
    onBuffering: (evt) => callOnAllPlugins("onBuffering", evt),
    onBufferingCleared: (evt) => callOnAllPlugins("onBufferingCleared", evt),
    onScreenCapabilityDetermined: (tvInfo) => callOnAllPlugins("onScreenCapabilityDetermined", tvInfo),
    onPlayerInfoUpdated: (evt) => callOnAllPlugins("onPlayerInfoUpdated", evt),
    onManifestLoaded: (manifest) => callOnAllPlugins("onManifestLoaded", manifest),
    onManifestParseError: (evt) => callOnAllPlugins("onManifestParseError", evt),
    onQualityChangeRequested: (evt) => callOnAllPlugins("onQualityChangeRequested", evt),
    onQualityChangedRendered: (evt) => callOnAllPlugins("onQualityChangedRendered", evt),
    onSubtitlesLoadError: (evt) => callOnAllPlugins("onSubtitlesLoadError", evt),
    onSubtitlesTimeout: (evt) => callOnAllPlugins("onSubtitlesTimeout", evt),
    onSubtitlesXMLError: (evt) => callOnAllPlugins("onSubtitlesXMLError", evt),
    onSubtitlesTransformError: (evt) => callOnAllPlugins("onSubtitlesTransformError", evt),
    onSubtitlesRenderError: (evt) => callOnAllPlugins("onSubtitlesRenderError", evt),
    onSubtitlesDynamicLoadError: (evt) => callOnAllPlugins("onSubtitlesDynamicLoadError", evt),
    onFragmentContentLengthMismatch: (evt) => callOnAllPlugins("onFragmentContentLengthMismatch", evt),
    onQuotaExceeded: (evt) => callOnAllPlugins("onQuotaExceeded", evt),
    onPlaybackRateChanged: (evt) => callOnAllPlugins("onPlaybackRateChanged", evt),
    onPlaybackFrozen: (evt) => callOnAllPlugins("onPlaybackFrozen", evt),
  },
};

/**
 * @import { InitData } from './types.d.ts'
 * @import { MediaSources } from './mediasources'
 *
 * @typedef {{
 *   data: {
 *     currentTime: number,
 *     seekableRange: { start: number, end: number },
 *     state: MediaState,
 *     duration: number
 *   },
 *   timeUpdate: boolean,
 *   isBufferingTimeoutError: boolean
 * }} StateUpdateData
 */

/**
 *
 * @param {HTMLMediaElement} playbackElement HTML Media Element to use for Playback
 * @param {InitData} bigscreenPlayerData Player Initialisation Data
 * @param {ReturnType<MediaSources>} mediaSources Media Sources instance
 * @param {(data: StateUpdateData) => void} stateUpdateCallback Callback for State Changes
 * @param {(error: any) => void} errorCallback Callback for Errors
 * @param {(enable: boolean) => void} audioDescribedCallback Callback for AD Changes
 */
function PlayerComponent(
  playbackElement,
  bigscreenPlayerData,
  mediaSources,
  stateUpdateCallback,
  errorCallback,
  audioDescribedCallback
) {
  let setSubtitlesState;
  let _stateUpdateCallback = stateUpdateCallback;

  let mediaKind = bigscreenPlayerData.media.kind;
  let isInitialPlay = true;
  let errorTimeoutID = null;

  let playbackStrategy;
  let mediaMetaData;
  let fatalErrorTimeout;
  let fatalError;

  StrategyPicker()
    .then((strategy) => {
      playbackStrategy = strategy(
        mediaSources,
        mediaKind,
        playbackElement,
        bigscreenPlayerData.media.isUHD,
        bigscreenPlayerData.media.playerSettings,
        {
          enable: bigscreenPlayerData.enableAudioDescribed,
          callback: audioDescribedCallback,
        }
      );

      playbackStrategy.addEventCallback(this, eventCallback);
      playbackStrategy.addErrorCallback(this, onError);
      playbackStrategy.addTimeUpdateCallback(this, onTimeUpdate);

      bubbleErrorCleared();

      mediaMetaData = bigscreenPlayerData.media;

      loadMedia(bigscreenPlayerData.media.type, bigscreenPlayerData.initialPlaybackTime);

      if (setSubtitlesState) playbackStrategy.setSubtitles(setSubtitlesState);
    })
    .catch((error) => {
      errorCallback && errorCallback(error);
    });

  function play() {
    playbackStrategy?.play();
  }

  function isEnded() {
    return playbackStrategy?.isEnded()
  }

  function pause() {
    if (transitions().canBePaused()) {
      playbackStrategy?.pause();
    }
  }

  function isSubtitlesAvailable() {
    return playbackStrategy && playbackStrategy.isSubtitlesAvailable()
  }

  function setSubtitles(state) {
    if (playbackStrategy) {
      playbackStrategy.setSubtitles(state);
    } else {
      setSubtitlesState = state;
    }
  }

  function customiseSubtitles(styleOpts) {
    return playbackStrategy && playbackStrategy.customiseSubtitles(styleOpts)
  }

  function getDuration() {
    return playbackStrategy?.getDuration()
  }

  function getPlayerElement() {
    let element = null;
    if (playbackStrategy?.getPlayerElement) {
      element = playbackStrategy.getPlayerElement();
    }
    return element
  }

  function getCurrentTime() {
    return playbackStrategy?.getCurrentTime()
  }

  function getSeekableRange() {
    return playbackStrategy?.getSeekableRange()
  }

  function isAudioDescribedAvailable() {
    const sourceBasedAD = mediaSources.isAudioDescribedAvailable();
    const playbackStrategyProvidedAD = () => playbackStrategy && playbackStrategy.isAudioDescribedAvailable?.();

    return sourceBasedAD || playbackStrategyProvidedAD()
  }

  function isAudioDescribedEnabled() {
    const sourceBasedADAvailable = mediaSources.isAudioDescribedAvailable();
    const sourceBasedADEnabled = mediaSources.isAudioDescribedEnabled();
    const playbackStrategyProvidedAD = () => playbackStrategy && playbackStrategy.isAudioDescribedEnabled?.();

    return sourceBasedADAvailable ? sourceBasedADEnabled : playbackStrategyProvidedAD()
  }

  function sourceBasedAudioDescribedSwitch(enable) {
    const presentationTimeInSeconds = getCurrentTime();
    const availabilityTimeInMilliseconds = presentationTimeToAvailabilityTimeInMilliseconds(
      presentationTimeInSeconds,
      mediaSources.time().availabilityStartTimeInMilliseconds
    );

    return mediaSources
      .setAudioDescribed(enable)
      .then(() => {
        const presentationTimeInSeconds = availabilityTimeToPresentationTimeInSeconds(
          availabilityTimeInMilliseconds,
          mediaSources.time().availabilityStartTimeInMilliseconds
        );

        tearDownMediaElement();
        loadMedia(mediaMetaData.type, presentationTimeInSeconds);
      })
      .catch(() => {
        bubbleFatalError(true, {
          code: PluginEnums.ERROR_CODES.BUFFERING_TIMEOUT,
          message: PluginEnums.ERROR_MESSAGES.BUFFERING_TIMEOUT,
        });
      })
  }

  function setAudioDescribed(enable) {
    if (mediaSources.isAudioDescribedAvailable()) return sourceBasedAudioDescribedSwitch(enable)

    if (playbackStrategy) {
      if (!playbackStrategy.isAudioDescribedAvailable?.()) return Promise.resolve()

      enable ? playbackStrategy.setAudioDescribedOn?.() : playbackStrategy.setAudioDescribedOff?.();
    }

    return Promise.resolve()
  }

  function isPaused() {
    return playbackStrategy?.isPaused()
  }

  function setCurrentTime(presentationTimeInSeconds) {
    if (transitions().canBeginSeek()) {
      playbackStrategy?.setCurrentTime(presentationTimeInSeconds);
    }
  }

  function setPlaybackRate(rate) {
    playbackStrategy?.setPlaybackRate(rate);
  }

  function getPlaybackRate() {
    return playbackStrategy?.getPlaybackRate()
  }

  function transitions() {
    return playbackStrategy?.transitions
  }

  function tearDownMediaElement() {
    clearTimeouts();
    playbackStrategy?.reset();
  }

  function eventCallback(mediaState) {
    switch (mediaState) {
      case MediaState.PLAYING:
        onPlaying();
        break

      case MediaState.PAUSED:
        onPaused();
        break

      case MediaState.WAITING:
        onBuffering();
        break

      case MediaState.ENDED:
        onEnded();
        break
    }
  }

  function onPlaying() {
    clearTimeouts();
    publishMediaStateUpdate(MediaState.PLAYING, {});
    isInitialPlay = false;
  }

  function onPaused() {
    publishMediaStateUpdate(MediaState.PAUSED);
    clearTimeouts();
  }

  function onBuffering() {
    publishMediaStateUpdate(MediaState.WAITING);
    startBufferingErrorTimeout();
    bubbleErrorCleared();
    bubbleBufferingRaised();
  }

  function onEnded() {
    clearTimeouts();
    publishMediaStateUpdate(MediaState.ENDED);
  }

  function onTimeUpdate() {
    publishMediaStateUpdate(undefined, { timeUpdate: true });
  }

  function onError(mediaError) {
    bubbleBufferingCleared();
    raiseError(mediaError);
  }

  function startBufferingErrorTimeout() {
    const bufferingTimeout = isInitialPlay ? 30000 : 20000;
    clearBufferingErrorTimeout();
    errorTimeoutID = setTimeout(() => {
      bubbleBufferingCleared();
      attemptCdnFailover({
        code: PluginEnums.ERROR_CODES.BUFFERING_TIMEOUT,
        message: PluginEnums.ERROR_MESSAGES.BUFFERING_TIMEOUT,
      });
    }, bufferingTimeout);
  }

  function raiseError(mediaError) {
    clearBufferingErrorTimeout();
    publishMediaStateUpdate(MediaState.WAITING);
    bubbleErrorRaised(mediaError);
    startFatalErrorTimeout(mediaError);
  }

  function startFatalErrorTimeout(mediaError) {
    if (!fatalErrorTimeout && !fatalError) {
      fatalErrorTimeout = setTimeout(() => {
        fatalErrorTimeout = null;
        fatalError = true;
        attemptCdnFailover(mediaError);
      }, 5000);
    }
  }

  function attemptCdnFailover(mediaError) {
    const presentationTimeInSeconds = getCurrentTime();
    const availabilityTimeInMilliseconds = presentationTimeToAvailabilityTimeInMilliseconds(
      presentationTimeInSeconds,
      mediaSources.time().availabilityStartTimeInMilliseconds
    );
    const bufferingTimeoutError = mediaError.code === PluginEnums.ERROR_CODES.BUFFERING_TIMEOUT;

    const failoverParams = {
      isBufferingTimeoutError: bufferingTimeoutError,
      currentTime: presentationTimeInSeconds,
      duration: getDuration(),
      code: mediaError.code,
      message: mediaError.message,
    };

    mediaSources
      .failover(failoverParams)
      .then(() => {
        tearDownMediaElement();
        const presentationTimeInSeconds = availabilityTimeToPresentationTimeInSeconds(
          availabilityTimeInMilliseconds,
          mediaSources.time().availabilityStartTimeInMilliseconds
        );
        loadMedia(mediaMetaData.type, presentationTimeInSeconds);
      })
      .catch(() => {
        bubbleFatalError(bufferingTimeoutError, mediaError);
      });
  }

  function clearFatalErrorTimeout() {
    if (fatalErrorTimeout !== null) {
      clearTimeout(fatalErrorTimeout);
      fatalErrorTimeout = null;
    }
  }

  function clearBufferingErrorTimeout() {
    if (errorTimeoutID !== null) {
      clearTimeout(errorTimeoutID);
      errorTimeoutID = null;
    }
  }

  function clearTimeouts() {
    clearBufferingErrorTimeout();
    clearFatalErrorTimeout();
    fatalError = false;
    bubbleBufferingCleared();
    bubbleErrorCleared();
  }

  function bubbleErrorCleared() {
    const evt = new PluginData({ status: PluginEnums.STATUS.DISMISSED, stateType: PluginEnums.TYPE.ERROR });
    Plugins.interface.onErrorCleared(evt);
  }

  function bubbleErrorRaised(mediaError) {
    const evt = new PluginData({
      status: PluginEnums.STATUS.STARTED,
      stateType: PluginEnums.TYPE.ERROR,
      isBufferingTimeoutError: false,
      code: mediaError.code,
      message: mediaError.message,
    });
    Plugins.interface.onError(evt);
  }

  function bubbleBufferingRaised() {
    const evt = new PluginData({ status: PluginEnums.STATUS.STARTED, stateType: PluginEnums.TYPE.BUFFERING });
    Plugins.interface.onBuffering(evt);
  }

  function bubbleBufferingCleared() {
    const evt = new PluginData({
      status: PluginEnums.STATUS.DISMISSED,
      stateType: PluginEnums.TYPE.BUFFERING,
      isInitialPlay,
    });
    Plugins.interface.onBufferingCleared(evt);
  }

  function bubbleFatalError(bufferingTimeoutError, mediaError) {
    const evt = new PluginData({
      status: PluginEnums.STATUS.FATAL,
      stateType: PluginEnums.TYPE.ERROR,
      isBufferingTimeoutError: bufferingTimeoutError,
      code: mediaError.code,
      message: mediaError.message,
    });
    Plugins.interface.onFatalError(evt);
    publishMediaStateUpdate(MediaState.FATAL_ERROR, {
      isBufferingTimeoutError: bufferingTimeoutError,
      code: mediaError.code,
      message: mediaError.message,
    });
  }

  function publishMediaStateUpdate(state, opts) {
    const stateUpdateData = {
      data: {
        currentTime: getCurrentTime(),
        seekableRange: getSeekableRange(),
        state,
        duration: getDuration(),
      },
      timeUpdate: opts?.timeUpdate ?? false,
      isBufferingTimeoutError: opts?.isBufferingTimeoutError ?? false,
    };

    if (opts && opts.code > -1 && opts.message) {
      stateUpdateData.code = opts.code;
      stateUpdateData.message = opts.message;
    }

    // guard against attempting to call _stateUpdateCallback after a tearDown
    // can happen if tearing down whilst an async cdn failover is being attempted
    if (_stateUpdateCallback) {
      _stateUpdateCallback(stateUpdateData);
    }
  }

  function loadMedia(type, presentationTimeInSeconds) {
    playbackStrategy?.load(type, presentationTimeInSeconds);

    if (mediaSources.isAudioDescribedEnabled()) {
      audioDescribedCallback(true);
      DebugTool.info("Source changed. Audio Described on.");
    } else if (mediaSources.isAudioDescribedAvailable()) {
      audioDescribedCallback(false);
      DebugTool.info("Source changed. Audio Described off.");
    }
  }

  function tearDown() {
    tearDownMediaElement();
    playbackStrategy?.tearDown();
    playbackStrategy = null;
    isInitialPlay = true;
    errorTimeoutID = undefined;
    mediaKind = undefined;
    _stateUpdateCallback = undefined;
    mediaMetaData = undefined;
    fatalErrorTimeout = undefined;
    fatalError = undefined;
  }

  function setBitrateConstraint(mediaKind, minBitrateKbps, maxBitrateKbps) {
    playbackStrategy?.setBitrateConstraint(mediaKind, minBitrateKbps, maxBitrateKbps);
  }

  function getPlaybackBitrate(mediaKind) {
    return playbackStrategy?.getPlaybackBitrate(mediaKind)
  }

  return {
    play,
    pause,
    customiseSubtitles,
    transitions,
    isEnded,
    isSubtitlesAvailable,
    setPlaybackRate,
    getPlaybackRate,
    setCurrentTime,
    getCurrentTime,
    getDuration,
    getSeekableRange,
    getPlayerElement,
    isPaused,
    tearDown,
    isAudioDescribedAvailable,
    isAudioDescribedEnabled,
    setAudioDescribed,
    setSubtitles,
    setBitrateConstraint,
    getPlaybackBitrate,
  }
}

function getLiveSupport$1() {
  return window.bigscreenPlayer?.liveSupport || LiveSupport.SEEKABLE
}

PlayerComponent.getLiveSupport = getLiveSupport$1;

const PauseTriggers = {
    USER: 1,
    APP: 2,
    DEVICE: 3,
};

var Version = "10.10.2";

/* eslint-disable no-use-before-define */

let sourceList;
let source;
let cdn;

let timeUpdateCallbacks = [];
let subtitleCallbacks = [];
let stateChangeCallbacks = [];
let audioDescribedCallbacks = [];

let currentTime;
let isSeeking;
let seekableRange;
let duration;
let initialPlaybackTime = null;
let liveWindowStart = 0;
let pausedState = true;
let endedState;
let mediaKind;
let manifestType;
let subtitlesAvailable;
let subtitlesEnabled;
let subtitlesHidden;
let audioDescribedAvailable;
let audioDescribedEnabled;
let endOfStream;
let canSeekState;
let canPauseState;
let shallowClone;
const mockModes = {
  NONE: 0,
  PLAIN: 1,
  JASMINE: 2,
};
let mockStatus = { currentlyMocked: false, mode: mockModes.NONE };
let initialised;
let fatalErrorBufferingTimeout;

let autoProgress;
let autoProgressInterval;
let initialBuffering = false;

let manifestError;

let excludedFuncs = [
  "getDebugLogs",
  "mock",
  "mockJasmine",
  "unmock",
  "toggleDebug",
  "getLogLevels",
  "setLogLevel",
  "setPlaybackRate",
  "getPlaybackRate",
  "clearSubtitleExample",
  "areSubtitlesCustomisable",
  "convertAvailabilityTimeToPresentationTimeInSeconds",
  "convertMediaSampleTimeToPresentationTimeInSeconds",
  "convertPresentationTimeToAvailabilityTimeInMilliseconds",
  "convertPresentationTimeToMediaSampleTimeInSeconds",
];

let compatibilityFuncs = ["getWindowType", "convertVideoTimeSecondsToEpochMs", "convertEpochMsToVideoTimeSeconds"];

function startProgress(progressCause) {
  setTimeout(() => {
    if (!autoProgressInterval) {
      mockingHooks.changeState(MediaState.PLAYING, progressCause);

      autoProgressInterval = setInterval(() => {
        if (manifestType !== ManifestType.STATIC && seekableRange.start && seekableRange.end) {
          seekableRange.start += 0.5;
          seekableRange.end += 0.5;
        }

        mockingHooks.progressTime(currentTime + 0.5);

        if (currentTime >= duration) {
          clearInterval(autoProgressInterval);
          mockingHooks.changeState(MediaState.ENDED);
        }
      }, 500);
    }
  }, 100);
}

function stopProgress() {
  if (autoProgressInterval) {
    clearInterval(autoProgressInterval);
    autoProgressInterval = null;
  }
}

function mock(BigscreenPlayer, opts) {
  autoProgress = opts?.autoProgress;

  if (opts?.excludedFuncs) {
    excludedFuncs = [...excludedFuncs, ...opts.excludedFuncs];
  }

  if (opts?.compatibilityFuncs) {
    compatibilityFuncs = [...compatibilityFuncs, ...opts.compatibilityFuncs];
  }

  if (mockStatus.currentlyMocked) {
    throw new Error("mock() was called while BigscreenPlayer was already mocked")
  }
  shallowClone = Utils.clone(BigscreenPlayer);

  // Divert existing functions
  for (const func in BigscreenPlayer) {
    if (BigscreenPlayer[func] && mockFunctions[func]) {
      BigscreenPlayer[func] = mockFunctions[func];
    } else if (!Utils.contains(excludedFuncs, func)) {
      throw new Error(`${func} was not mocked or included in the exclusion list`)
    }
  }
  // Add extra functions
  for (const hook in mockingHooks) {
    BigscreenPlayer[hook] = mockingHooks[hook];
  }

  // Add compatibility functions
  for (let funcsSoFar = 0; funcsSoFar < compatibilityFuncs.length; funcsSoFar += 1) {
    const funcName = compatibilityFuncs[funcsSoFar];

    BigscreenPlayer[funcName] = mockFunctions[funcName];
  }

  mockStatus = { currentlyMocked: true, mode: mockModes.PLAIN };
}

function mockJasmine(BigscreenPlayer, opts) {
  autoProgress = opts && opts.autoProgress;

  if (opts && opts.excludedFuncs) {
    excludedFuncs = [...excludedFuncs, ...opts.excludedFuncs];
  }

  if (mockStatus.currentlyMocked) {
    throw new Error("mockJasmine() was called while BigscreenPlayer was already mocked")
  }

  for (const fn in BigscreenPlayer) {
    if (BigscreenPlayer[fn] && mockFunctions[fn]) {
      spyOn(BigscreenPlayer, fn).and.callFake(mockFunctions[fn]);
    } else if (!Utils.contains(excludedFuncs, fn)) {
      throw new Error(`${fn} was not mocked or included in the exclusion list`)
    }
  }

  for (const hook in mockingHooks) {
    BigscreenPlayer[hook] = mockingHooks[hook];
  }
  mockStatus = { currentlyMocked: true, mode: mockModes.JASMINE };
}

function unmock(BigscreenPlayer) {
  if (!mockStatus.currentlyMocked) {
    throw new Error("unmock() was called before BigscreenPlayer was mocked")
  }

  // Remove extra functions
  for (const hook in mockingHooks) {
    delete BigscreenPlayer[hook];
  }
  // Undo divert existing functions (plain mock only)
  if (mockStatus.mode === mockModes.PLAIN) {
    for (const func in shallowClone) {
      BigscreenPlayer[func] = shallowClone[func];
    }
  }

  timeUpdateCallbacks = [];
  stateChangeCallbacks = [];

  mockStatus = { currentlyMocked: false, mode: mockModes.NONE };
}

function callSubtitlesCallbacks(enabled) {
  CallCallbacks(subtitleCallbacks, { enabled });
}

const mockFunctions = {
  init(playbackElement, bigscreenPlayerData, callbacks) {
    initialPlaybackTime =
      typeof bigscreenPlayerData?.initialPlaybackTime === "number" ? bigscreenPlayerData.initialPlaybackTime : null;
    currentTime = initialPlaybackTime == null ? 0 : initialPlaybackTime;
    liveWindowStart = 0;
    pausedState = true;
    endedState = false;
    mediaKind = bigscreenPlayerData?.media?.kind || "video";
    subtitlesAvailable = true;
    subtitlesEnabled = bigscreenPlayerData?.enableSubtitles ?? false;
    audioDescribedAvailable = false;
    audioDescribedEnabled = false;
    canSeekState = true;
    canPauseState = true;
    sourceList = bigscreenPlayerData?.media?.urls;
    source = sourceList?.[0].url;
    cdn = sourceList?.[0].cdn;

    duration = manifestType === ManifestType.STATIC ? 4808 : Infinity;
    seekableRange = { start: 0, end: 4808 };

    if (manifestError) {
      if (callbacks && callbacks.onError) {
        callbacks.onError({ error: "manifest" });
      }
      return
    }

    mockingHooks.changeState(MediaState.WAITING);

    if (autoProgress && !initialBuffering) {
      startProgress();
    }

    initialised = true;

    if (subtitlesEnabled) {
      callSubtitlesCallbacks(true);
    }

    if (callbacks?.onSuccess) {
      callbacks.onSuccess();
    }
  },
  registerForTimeUpdates(callback) {
    timeUpdateCallbacks.push(callback);
    return callback
  },
  unregisterForTimeUpdates(callback) {
    timeUpdateCallbacks = timeUpdateCallbacks.filter((existingCallback) => callback !== existingCallback);
  },
  registerForSubtitleChanges(callback) {
    subtitleCallbacks.push(callback);
    return callback
  },
  unregisterForSubtitleChanges(callback) {
    subtitleCallbacks = subtitleCallbacks.filter((existingCallback) => callback !== existingCallback);
  },
  registerForAudioDescribedChanges(callback) {
    audioDescribedCallbacks.push(callback);
    return callback
  },
  unregisterForAudioDescribedChanges(callback) {
    audioDescribedCallbacks = audioDescribedCallbacks.filter((existingCallback) => callback !== existingCallback);
  },
  registerForStateChanges(callback) {
    stateChangeCallbacks.push(callback);
    return callback
  },
  unregisterForStateChanges(callback) {
    stateChangeCallbacks = stateChangeCallbacks.filter((existingCallback) => callback !== existingCallback);
  },
  setCurrentTime(time) {
    currentTime = time - liveWindowStart;
    isSeeking = true;

    if (autoProgress) {
      mockingHooks.changeState(MediaState.WAITING, "other");
      if (!pausedState) {
        startProgress();
      }
    } else {
      mockingHooks.progressTime(currentTime);
    }
  },
  getCurrentTime() {
    return currentTime + liveWindowStart
  },
  getInitialPlaybackTime() {
    return initialPlaybackTime
  },
  getMediaKind() {
    return mediaKind
  },
  getWindowType() {
    return manifestType === ManifestType.STATIC ? "staticWindow" : "slidingWindow"
  },
  getSeekableRange() {
    return seekableRange?.start && seekableRange?.end
      ? { start: seekableRange.start + liveWindowStart, end: seekableRange.end + liveWindowStart }
      : seekableRange
  },
  getDuration() {
    return duration
  },
  isPaused() {
    return pausedState
  },
  isEnded() {
    return endedState
  },
  play() {
    if (autoProgress) {
      startProgress("other");
    } else {
      mockingHooks.changeState(MediaState.PLAYING, "other");
    }
  },
  pause(opts) {
    mockingHooks.changeState(MediaState.PAUSED, "other", opts);
  },
  setSubtitlesEnabled(value) {
    subtitlesEnabled = value;
    callSubtitlesCallbacks(value);
  },
  isSubtitlesEnabled() {
    return subtitlesEnabled
  },
  isSubtitlesAvailable() {
    return subtitlesAvailable
  },
  customiseSubtitles() {},
  renderSubtitleExample() {},
  setAudioDescribed(value) {
    audioDescribedEnabled = value;
  },
  isAudioDescribedEnabled() {
    return audioDescribedEnabled
  },
  isAudioDescribedAvailable() {
    return audioDescribedAvailable
  },
  setTransportControlsPosition() {},
  canSeek() {
    return canSeekState
  },
  canPause() {
    return canPauseState
  },
  transitions() {
    return {
      canBePaused() {
        return true
      },
      canBeginSeek() {
        return true
      },
    }
  },
  isPlayingAtLiveEdge() {
    return false
  },
  resize() {
    subtitlesHidden = this.isSubtitlesEnabled();
    this.setSubtitlesEnabled(subtitlesHidden);
  },
  clearResize() {
    this.setSubtitlesEnabled(subtitlesHidden);
  },
  getPlayerElement() {},
  getFrameworkVersion() {
    return Version
  },
  tearDown() {
    manifestError = false;
    if (!initialised) {
      return
    }

    Plugins.interface.onBufferingCleared(
      new PluginData({
        status: PluginEnums.STATUS.DISMISSED,
        stateType: PluginEnums.TYPE.BUFFERING,
        isInitialPlay: initialBuffering,
      })
    );
    Plugins.interface.onErrorCleared(
      new PluginData({ status: PluginEnums.STATUS.DISMISSED, stateType: PluginEnums.TYPE.ERROR })
    );
    Plugins.unregisterPlugin();

    timeUpdateCallbacks = [];
    stateChangeCallbacks = [];

    if (autoProgress) {
      stopProgress();
    }

    initialised = false;
  },
  registerPlugin(plugin) {
    Plugins.registerPlugin(plugin);
  },
  unregisterPlugin(plugin) {
    Plugins.unregisterPlugin(plugin);
  },
  getPresentationTimeOffsetInMilliseconds() {
    return 0
  },
  getTimeShiftBufferDepthInMilliseconds() {
    return manifestType === ManifestType.STATIC ? 0 : 7200000
  },
  convertVideoTimeSecondsToEpochMs(seconds) {
    return seconds
  },
  convertEpochMsToVideoTimeSeconds(milliseconds) {
    return milliseconds
  },
};

const mockingHooks = {
  changeState(state, eventTrigger, opts) {
    const pauseTrigger = opts && opts.userPause === false ? PauseTriggers.APP : PauseTriggers.USER;

    pausedState = state === MediaState.PAUSED || state === MediaState.STOPPED || state === MediaState.ENDED;
    endedState = state === MediaState.ENDED;

    if (state === MediaState.WAITING) {
      fatalErrorBufferingTimeout = true;
      Plugins.interface.onBuffering(
        new PluginData({ status: PluginEnums.STATUS.STARTED, stateType: PluginEnums.TYPE.BUFFERING })
      );
    } else {
      Plugins.interface.onBufferingCleared(
        new PluginData({
          status: PluginEnums.STATUS.DISMISSED,
          stateType: PluginEnums.TYPE.BUFFERING,
          isInitialPlay: initialBuffering,
        })
      );
    }
    Plugins.interface.onErrorCleared(
      new PluginData({ status: PluginEnums.STATUS.DISMISSED, stateType: PluginEnums.TYPE.ERROR })
    );

    if (state === MediaState.FATAL_ERROR) {
      Plugins.interface.onFatalError(
        new PluginData({
          status: PluginEnums.STATUS.FATAL,
          stateType: PluginEnums.TYPE.ERROR,
          isBufferingTimeoutError: fatalErrorBufferingTimeout,
        })
      );
    }

    const stateObject = { state };
    if (state === MediaState.PAUSED) {
      stateObject.trigger = pauseTrigger;
      endOfStream = false;
    }
    if (state === MediaState.FATAL_ERROR) {
      stateObject.errorId = opts && opts.error;
      stateObject.isBufferingTimeoutError = opts && opts.isBufferingTimeoutError;
    }
    if (state === MediaState.WAITING) {
      stateObject.isSeeking = isSeeking;
      isSeeking = false;
    }
    stateObject.endOfStream = endOfStream;

    CallCallbacks(stateChangeCallbacks, stateObject);

    if (autoProgress) {
      if (state === MediaState.PLAYING) {
        startProgress();
      } else {
        stopProgress();
      }
    }
  },
  progressTime(time) {
    currentTime = time + liveWindowStart;
    CallCallbacks(timeUpdateCallbacks, {
      currentTime: time + liveWindowStart,
      endOfStream,
    });
  },
  setEndOfStream(isEndOfStream) {
    endOfStream = isEndOfStream;
  },
  setDuration(mediaDuration) {
    duration = mediaDuration;
  },
  setSeekableRange(newSeekableRange) {
    seekableRange = newSeekableRange;
  },
  setMediaKind(kind) {
    mediaKind = kind;
  },
  setWindowType(type) {
    manifestType = type === "staticWindow" ? "static" : "dynamic";
  },
  setCanSeek(value) {
    canSeekState = value;
  },
  setCanPause(value) {
    canPauseState = value;
  },
  setLiveWindowStart(value) {
    liveWindowStart = value;
  },
  setSubtitlesAvailable(value) {
    subtitlesAvailable = value;
  },
  getSource() {
    return source
  },
  triggerError() {
    fatalErrorBufferingTimeout = false;
    Plugins.interface.onError(
      new PluginData({
        status: PluginEnums.STATUS.STARTED,
        stateType: PluginEnums.TYPE.ERROR,
        isBufferingTimeoutError: false,
      })
    );
    this.changeState(MediaState.WAITING);
    stopProgress();
  },
  triggerManifestError() {
    manifestError = true;
  },
  triggerErrorHandled() {
    if (sourceList && sourceList.length > 1) {
      sourceList.shift();
      source = sourceList[0].url;
      cdn = sourceList[0].cdn;
    }
    Plugins.interface.onBufferingCleared(
      new PluginData({
        status: PluginEnums.STATUS.DISMISSED,
        stateType: PluginEnums.TYPE.BUFFERING,
        isInitialPlay: initialBuffering,
      })
    );
    Plugins.interface.onErrorCleared(
      new PluginData({ status: PluginEnums.STATUS.DISMISSED, stateType: PluginEnums.TYPE.ERROR })
    );
    Plugins.interface.onErrorHandled(
      new PluginData({
        status: PluginEnums.STATUS.FAILOVER,
        stateType: PluginEnums.TYPE.ERROR,
        isBufferingTimeoutError: fatalErrorBufferingTimeout,
        cdn,
      })
    );

    if (autoProgress) {
      stopProgress();
      startProgress();
    }
  },
  setInitialBuffering(value) {
    initialBuffering = value;
  },
};

var MockBigscreenPlayer = {
  mock,
  unmock,
  mockJasmine,
};

const DASH = "dash";
const HLS = "hls";
const PLAIN = "plain";
const TransferFormat = { DASH, HLS, PLAIN };

function getMPDType(mpd) {
    const type = mpd.getAttribute("type");
    if (type !== ManifestType.STATIC && type !== ManifestType.DYNAMIC) {
        throw new TypeError(`MPD type attribute must be 'static' or 'dynamic'. Got ${type}`);
    }
    return type;
}
function getMPDAvailabilityStartTimeInMilliseconds(mpd) {
    var _a;
    return Date.parse((_a = mpd.getAttribute("availabilityStartTime")) !== null && _a !== void 0 ? _a : "") || 0;
}
function getMPDTimeShiftBufferDepthInMilliseconds(mpd) {
    var _a;
    return (durationToSeconds((_a = mpd.getAttribute("timeShiftBufferDepth")) !== null && _a !== void 0 ? _a : "") || 0) * 1000;
}
function getMPDPresentationTimeOffsetInMilliseconds(mpd) {
    var _a, _b;
    // Can be either audio or video data. It doesn't matter as we use the factor of x/timescale. This is the same for both.
    const segmentTemplate = mpd.querySelector("SegmentTemplate");
    const presentationTimeOffsetInFrames = parseFloat((_a = segmentTemplate === null || segmentTemplate === void 0 ? void 0 : segmentTemplate.getAttribute("presentationTimeOffset")) !== null && _a !== void 0 ? _a : "");
    const timescale = parseFloat((_b = segmentTemplate === null || segmentTemplate === void 0 ? void 0 : segmentTemplate.getAttribute("timescale")) !== null && _b !== void 0 ? _b : "");
    return (presentationTimeOffsetInFrames / timescale) * 1000 || 0;
}
function parseMPD(manifestEl) {
    return new Promise((resolve, reject) => {
        const mpd = manifestEl.querySelector("MPD");
        if (mpd == null) {
            return reject(new TypeError("Could not find an 'MPD' tag in the document"));
        }
        const manifestType = getMPDType(mpd);
        const presentationTimeOffsetInMilliseconds = getMPDPresentationTimeOffsetInMilliseconds(mpd);
        const availabilityStartTimeInMilliseconds = getMPDAvailabilityStartTimeInMilliseconds(mpd);
        const timeShiftBufferDepthInMilliseconds = getMPDTimeShiftBufferDepthInMilliseconds(mpd);
        return resolve({
            manifestType,
            timeShiftBufferDepthInMilliseconds,
            availabilityStartTimeInMilliseconds,
            presentationTimeOffsetInMilliseconds,
        });
    }).catch((reason) => {
        const errorWithCode = (isError(reason) ? reason : new Error("manifest-dash-parse-error"));
        errorWithCode.code = PluginEnums.ERROR_CODES.MANIFEST_PARSE;
        throw errorWithCode;
    });
}
function parseM3U8(manifest) {
    return new Promise((resolve) => {
        const programDateTimeInMilliseconds = getM3U8ProgramDateTimeInMilliseconds(manifest);
        const durationInMilliseconds = getM3U8WindowSizeInMilliseconds(manifest);
        if (programDateTimeInMilliseconds == null ||
            durationInMilliseconds == null ||
            (programDateTimeInMilliseconds === 0 && durationInMilliseconds === 0)) {
            throw new Error("manifest-hls-attributes-parse-error");
        }
        const manifestType = hasM3U8EndList(manifest) ? ManifestType.STATIC : ManifestType.DYNAMIC;
        return resolve({
            manifestType,
            timeShiftBufferDepthInMilliseconds: 0,
            availabilityStartTimeInMilliseconds: manifestType === ManifestType.STATIC ? 0 : programDateTimeInMilliseconds,
            presentationTimeOffsetInMilliseconds: programDateTimeInMilliseconds,
        });
    }).catch((reason) => {
        const errorWithCode = (isError(reason) ? reason : new Error("manifest-hls-parse-error"));
        errorWithCode.code = PluginEnums.ERROR_CODES.MANIFEST_PARSE;
        throw errorWithCode;
    });
}
function getM3U8ProgramDateTimeInMilliseconds(data) {
    const match = /^#EXT-X-PROGRAM-DATE-TIME:(.*)$/m.exec(data);
    if (match == null) {
        return 0;
    }
    const parsedDate = Date.parse(match[1]);
    return isNaN(parsedDate) ? null : parsedDate;
}
function getM3U8WindowSizeInMilliseconds(data) {
    const regex = /#EXTINF:(\d+(?:\.\d+)?)/g;
    let matches = regex.exec(data);
    let result = 0;
    while (matches) {
        result += +matches[1];
        matches = regex.exec(data);
    }
    return Math.floor(result * 1000);
}
function hasM3U8EndList(data) {
    const match = /^#EXT-X-ENDLIST$/m.exec(data);
    return match != null;
}
function parse({ body, type }) {
    return Promise.resolve()
        .then(() => {
        switch (type) {
            case TransferFormat.DASH:
                return parseMPD(body);
            case TransferFormat.HLS:
                return parseM3U8(body);
        }
    })
        .catch((error) => {
        DebugTool.error(error);
        Plugins.interface.onManifestParseError({ code: error.code, message: error.message });
        return {
            manifestType: ManifestType.STATIC,
            timeShiftBufferDepthInMilliseconds: 0,
            availabilityStartTimeInMilliseconds: 0,
            presentationTimeOffsetInMilliseconds: 0,
        };
    });
}
var ManifestParser = {
    parse,
};

function LoadUrl(url, opts) {
    const xhr = new XMLHttpRequest();
    if (opts.timeout) {
        xhr.timeout = opts.timeout;
    }
    if (opts.onTimeout) {
        xhr.ontimeout = opts.onTimeout;
    }
    xhr.addEventListener("readystatechange", function listener() {
        if (xhr.readyState === 4) {
            xhr.removeEventListener("readystatechange", listener);
            if (xhr.status >= 200 && xhr.status < 300) {
                if (opts.onLoad) {
                    opts.onLoad(xhr.responseXML, xhr.responseText, xhr.status);
                }
            }
            else {
                if (opts.onError) {
                    opts.onError({ errorType: "NON_200_ERROR", statusCode: xhr.status });
                }
            }
        }
    });
    try {
        xhr.open(opts.method || "GET", url, true);
        if (opts.headers) {
            for (const header in opts.headers) {
                if (Object.prototype.hasOwnProperty.call(opts.headers, header)) {
                    xhr.setRequestHeader(header, opts.headers[header]);
                }
            }
        }
        xhr.send(opts.data || null);
    }
    catch (error) {
        if (opts.onError) {
            opts.onError({ errorType: isError(error) ? error.name : "unknown", statusCode: xhr.status });
        }
    }
}

function parseXmlString(text) {
    const parser = new DOMParser();
    const document = parser.parseFromString(text, "application/xml");
    // DOMParser lists the XML errors in the document
    if (document.querySelector("parsererror") != null) {
        throw new TypeError(`Failed to parse input string to XML`);
    }
    return document;
}
function retrieveDashManifest(url) {
    return new Promise((resolveLoad, rejectLoad) => LoadUrl(url, {
        method: "GET",
        headers: {},
        timeout: 10000,
        // Try to parse ourselves if the XHR parser failed due to f.ex. content-type
        onLoad: (responseXML, responseText) => resolveLoad(responseXML || parseXmlString(responseText)),
        onError: () => rejectLoad(new Error("Network error: Unable to retrieve DASH manifest")),
    }))
        .then((xml) => {
        if (xml == null) {
            throw new TypeError("Unable to retrieve DASH XML response");
        }
        return ManifestParser.parse({ body: xml, type: TransferFormat.DASH });
    })
        .then((time) => ({ time, transferFormat: TransferFormat.DASH }))
        .catch((error) => {
        if (isError(error) && error.message.indexOf("DASH") !== -1) {
            throw error;
        }
        throw new Error("Unable to retrieve DASH XML response");
    });
}
function retrieveHLSManifest(url) {
    return new Promise((resolveLoad, rejectLoad) => LoadUrl(url, {
        method: "GET",
        headers: {},
        timeout: 10000,
        onLoad: (_, responseText) => resolveLoad(responseText),
        onError: () => rejectLoad(new Error("Network error: Unable to retrieve HLS master playlist")),
    })).then((text) => {
        if (!text || typeof text !== "string") {
            throw new TypeError("Unable to retrieve HLS master playlist");
        }
        let streamUrl = getStreamUrl(text);
        if (!streamUrl || typeof streamUrl !== "string") {
            throw new TypeError("Unable to retrieve playlist url from HLS master playlist");
        }
        if (streamUrl.indexOf("http") !== 0) {
            const parts = url.split("/");
            parts.pop();
            parts.push(streamUrl);
            streamUrl = parts.join("/");
        }
        return retrieveHLSLivePlaylist(streamUrl);
    });
}
function retrieveHLSLivePlaylist(url) {
    return new Promise((resolveLoad, rejectLoad) => LoadUrl(url, {
        method: "GET",
        headers: {},
        timeout: 10000,
        onLoad: (_, responseText) => resolveLoad(responseText),
        onError: () => rejectLoad(new Error("Network error: Unable to retrieve HLS live playlist")),
    }))
        .then((text) => {
        if (!text || typeof text !== "string") {
            throw new TypeError("Unable to retrieve HLS live playlist");
        }
        return ManifestParser.parse({ body: text, type: TransferFormat.HLS });
    })
        .then((time) => ({ time, transferFormat: TransferFormat.HLS }));
}
function getStreamUrl(data) {
    const match = /#EXT-X-STREAM-INF:.*[\n\r]+(.*)[\n\r]?/.exec(data);
    return match ? match[1] : null;
}
var ManifestLoader = {
    load: (mediaUrl) => {
        if (/\.mpd(\?.*)?$/.test(mediaUrl)) {
            return retrieveDashManifest(mediaUrl);
        }
        if (/\.m3u8(\?.*)?$/.test(mediaUrl)) {
            return retrieveHLSManifest(mediaUrl);
        }
        if (/\.mp4(\?.*)?$/.test(mediaUrl)) {
            return Promise.resolve({
                time: {
                    manifestType: ManifestType.STATIC,
                    presentationTimeOffsetInMilliseconds: 0,
                    timeShiftBufferDepthInMilliseconds: 0,
                    availabilityStartTimeInMilliseconds: 0,
                },
                transferFormat: TransferFormat.PLAIN,
            });
        }
        return Promise.reject(new Error("Invalid media url"));
    },
};

var SourceType;
(function (SourceType) {
    SourceType[SourceType["MEDIA"] = 0] = "MEDIA";
    SourceType[SourceType["AUDIO_DESCRIBED"] = 1] = "AUDIO_DESCRIBED";
})(SourceType || (SourceType = {}));
function MediaSources() {
    let failoverSort = null;
    const sources = {
        [SourceType.MEDIA]: [],
        [SourceType.AUDIO_DESCRIBED]: [],
    };
    let subtitlesSources = [];
    let current = SourceType.MEDIA;
    let protectionData = null;
    let failoverResetTokens = [];
    let time = null;
    let transferFormat = null;
    // Can be overridden with media.subtitlesRequestTimeout
    let subtitlesRequestTimeout = 5000;
    let failoverResetTimeMs = 120000;
    function init(media, setAudioDescribedOn) {
        return new Promise((resolve, reject) => {
            var _a, _b, _c, _d, _e, _f;
            if (!((_a = media.urls) === null || _a === void 0 ? void 0 : _a.length)) {
                return reject(new Error("Media Sources urls are undefined"));
            }
            if (media.subtitlesRequestTimeout) {
                subtitlesRequestTimeout = media.subtitlesRequestTimeout;
            }
            if ((_b = media.playerSettings) === null || _b === void 0 ? void 0 : _b.failoverResetTime) {
                failoverResetTimeMs = media.playerSettings.failoverResetTime;
            }
            if ((_c = media.playerSettings) === null || _c === void 0 ? void 0 : _c.failoverSort) {
                failoverSort = media.playerSettings.failoverSort;
            }
            sources[SourceType.MEDIA] = Utils.cloneArray((_d = media.urls) !== null && _d !== void 0 ? _d : []);
            sources[SourceType.AUDIO_DESCRIBED] = Utils.cloneArray((_e = media.audioDescribed) !== null && _e !== void 0 ? _e : []);
            protectionData = media.protectionData || null;
            subtitlesSources = Utils.cloneArray((_f = media.captions) !== null && _f !== void 0 ? _f : []);
            if (setAudioDescribedOn && isAudioDescribedAvailable())
                current = SourceType.AUDIO_DESCRIBED;
            updateDebugOutput();
            return resolve(loadManifest());
        });
    }
    function failover(failoverParams) {
        return new Promise((resolve, reject) => {
            if (!isFailoverInfoValid(failoverParams)) {
                return reject(new TypeError("Invalid failover params"));
            }
            if ((time === null || time === void 0 ? void 0 : time.manifestType) === ManifestType.STATIC &&
                isAboutToEnd(failoverParams.currentTime, failoverParams.duration)) {
                return reject(new Error("Current time too close to end"));
            }
            if (!hasSourcesToFailoverTo()) {
                return reject(new Error("Exhaused all sources"));
            }
            if (isFirstManifest(failoverParams.serviceLocation)) {
                return resolve();
            }
            emitCdnFailover(failoverParams);
            updateCdns(failoverParams.serviceLocation);
            updateDebugOutput();
            return needToGetManifest() ? resolve(loadManifest()) : resolve();
        });
    }
    function failoverSubtitles(_a = {}) {
        var { statusCode } = _a, rest = __rest(_a, ["statusCode"]);
        return new Promise((resolve, reject) => {
            if (subtitlesSources.length <= 1) {
                Plugins.interface.onSubtitlesLoadError(Object.assign({ status: statusCode, severity: PluginEnums.STATUS.FATAL, cdn: getCurrentSubtitlesCdn(), subtitlesSources: subtitlesSources.length }, rest));
                return reject(new Error("Exhaused all subtitle sources"));
            }
            Plugins.interface.onSubtitlesLoadError(Object.assign({ status: statusCode, severity: PluginEnums.STATUS.FAILOVER, cdn: getCurrentSubtitlesCdn(), subtitlesSources: subtitlesSources.length }, rest));
            subtitlesSources.shift();
            updateDebugOutput();
            return resolve();
        });
    }
    function isAboutToEnd(currentTime, duration) {
        return typeof currentTime === "number" && typeof duration === "number" && duration > 0 && currentTime > duration - 5;
    }
    function stripQueryParamsAndHash(url) {
        return url.replace(/[#?].*/, "");
    }
    // we don't want to failover on the first playback
    // the serviceLocation is set to our first cdn url
    // see manifest modifier - generateBaseUrls
    function isFirstManifest(serviceLocation) {
        return typeof serviceLocation === "string" && doHostsMatch(serviceLocation, getCurrentUrl());
    }
    function doHostsMatch(firstUrl, secondUrl) {
        // Matches anything between *:// and / or the end of the line
        const hostRegex = /\w+?:\/\/(.*?)(?:\/|$)/;
        const serviceLocNoQueryHash = stripQueryParamsAndHash(firstUrl);
        const currUrlNoQueryHash = stripQueryParamsAndHash(secondUrl);
        const serviceLocationHost = hostRegex.exec(serviceLocNoQueryHash);
        const currentUrlHost = hostRegex.exec(currUrlNoQueryHash);
        return serviceLocationHost && currentUrlHost
            ? serviceLocationHost[1] === currentUrlHost[1]
            : serviceLocNoQueryHash === currUrlNoQueryHash;
    }
    function isFailoverInfoValid(failoverParams) {
        const infoValid = typeof failoverParams === "object" && typeof failoverParams.isBufferingTimeoutError === "boolean";
        if (!infoValid) {
            DebugTool.error("failoverInfo is not valid");
        }
        return infoValid;
    }
    function failoverResetTime() {
        return failoverResetTimeMs;
    }
    function needToGetManifest() {
        const hasManifestBeenLoaded = transferFormat != null;
        return (!hasManifestBeenLoaded || (transferFormat === TransferFormat.HLS && (time === null || time === void 0 ? void 0 : time.manifestType) === ManifestType.DYNAMIC));
    }
    function refresh() {
        return new Promise((resolve) => resolve(loadManifest()));
    }
    function isAudioDescribedAvailable() {
        return sources[SourceType.AUDIO_DESCRIBED].length > 0;
    }
    function isAudioDescribedEnabled() {
        return current === SourceType.AUDIO_DESCRIBED;
    }
    function setAudioDescribed(enable) {
        current = enable ? SourceType.AUDIO_DESCRIBED : SourceType.MEDIA;
        updateDebugOutput();
        return new Promise((resolve) => resolve(needToGetManifest() ? refresh() : undefined));
    }
    function loadManifest() {
        return ManifestLoader.load(getCurrentUrl())
            .then(({ time: newTime, transferFormat: newTransferFormat }) => {
            time = newTime;
            transferFormat = newTransferFormat;
            DebugTool.sourceLoaded(Object.assign(Object.assign({}, time), { transferFormat }));
        })
            .catch((reason) => {
            DebugTool.error(`Failed to load manifest: ${isError(reason) ? reason.message : "cause n/a"}`);
            return failover({
                isBufferingTimeoutError: false,
                code: PluginEnums.ERROR_CODES.MANIFEST_LOAD,
                message: PluginEnums.ERROR_MESSAGES.MANIFEST,
            });
        })
            .catch((reason) => {
            const error = new Error(isError(reason) ? reason.message : undefined);
            error.name = "ManifestLoadError";
            throw error;
        });
    }
    function getCurrentProtectionData() {
        return protectionData;
    }
    function getCurrentUrl() {
        return sources[current].length > 0 ? sources[current][0].url.toString() : "";
    }
    function getCurrentSubtitlesUrl() {
        return subtitlesSources.length > 0 ? subtitlesSources[0].url.toString() : "";
    }
    function getCurrentSubtitlesSegmentLength() {
        return subtitlesSources.length > 0 ? subtitlesSources[0].segmentLength : undefined;
    }
    function getSubtitlesRequestTimeout() {
        return subtitlesRequestTimeout;
    }
    function getCurrentSubtitlesCdn() {
        return subtitlesSources.length > 0 ? subtitlesSources[0].cdn : undefined;
    }
    function availableUrls() {
        return sources[current].map((mediaSource) => mediaSource.url);
    }
    function generateTime() {
        return time;
    }
    function getCurrentTransferFormat() {
        return transferFormat;
    }
    function updateFailedOverSources(mediaSource, currentSources) {
        if (failoverSort) {
            sources[currentSources] = failoverSort(sources[currentSources]);
        }
        const failoverResetToken = setTimeout(() => {
            if (mediaSource == null || sources[currentSources].length === 0)
                return;
            DebugTool.info(`${mediaSource.cdn} has been added back in to available CDNs`);
            sources[currentSources].push(mediaSource);
            updateDebugOutput();
        }, failoverResetTimeMs);
        failoverResetTokens.push(failoverResetToken);
    }
    function updateCdns(serviceLocation) {
        const currentSources = current;
        const source = sources[currentSources].shift();
        if (source == null)
            return;
        updateFailedOverSources(source, currentSources);
        if (serviceLocation == null)
            return;
        moveMediaSourceToFront(serviceLocation);
    }
    function moveMediaSourceToFront(serviceLocation) {
        const currentSources = current;
        let serviceLocationIdx = sources[currentSources]
            .map((mediaSource) => stripQueryParamsAndHash(mediaSource.url))
            .indexOf(stripQueryParamsAndHash(serviceLocation));
        if (serviceLocationIdx < 0)
            serviceLocationIdx = 0;
        sources[currentSources].unshift(sources[currentSources].splice(serviceLocationIdx, 1)[0]);
    }
    function hasSourcesToFailoverTo() {
        return sources[current].length > 1;
    }
    function emitCdnFailover(failoverInfo) {
        var _a, _b;
        const currentSources = current;
        const evt = new PluginData({
            status: PluginEnums.STATUS.FAILOVER,
            stateType: PluginEnums.TYPE.ERROR,
            isBufferingTimeoutError: failoverInfo.isBufferingTimeoutError,
            cdn: (_a = sources[currentSources][0]) === null || _a === void 0 ? void 0 : _a.cdn,
            newCdn: (_b = sources[currentSources][1]) === null || _b === void 0 ? void 0 : _b.cdn,
            code: failoverInfo.code,
            message: failoverInfo.message,
        });
        Plugins.interface.onErrorHandled(evt);
    }
    function availableCdns() {
        return sources[current].map((mediaSource) => mediaSource.cdn);
    }
    function availableSubtitlesCdns() {
        return subtitlesSources.map((subtitleSource) => subtitleSource.cdn);
    }
    function updateDebugOutput() {
        DebugTool.dynamicMetric("cdns-available", availableCdns());
        DebugTool.dynamicMetric("current-url", stripQueryParamsAndHash(getCurrentUrl()));
        DebugTool.dynamicMetric("subtitle-cdns-available", availableSubtitlesCdns());
        DebugTool.dynamicMetric("subtitle-current-url", stripQueryParamsAndHash(getCurrentSubtitlesUrl()));
    }
    function tearDown() {
        failoverResetTokens.forEach((token) => clearTimeout(token));
        time = null;
        transferFormat = null;
        sources[SourceType.MEDIA] = [];
        sources[SourceType.AUDIO_DESCRIBED] = [];
        failoverResetTokens = [];
        subtitlesSources = [];
    }
    return {
        init,
        failover,
        failoverSubtitles,
        refresh,
        isAudioDescribedAvailable,
        isAudioDescribedEnabled,
        setAudioDescribed,
        currentSource: getCurrentUrl,
        currentSubtitlesSource: getCurrentSubtitlesUrl,
        currentSubtitlesSegmentLength: getCurrentSubtitlesSegmentLength,
        currentSubtitlesCdn: getCurrentSubtitlesCdn,
        currentProtectionData: getCurrentProtectionData,
        subtitlesRequestTimeout: getSubtitlesRequestTimeout,
        availableSources: availableUrls,
        failoverResetTime,
        time: generateTime,
        transferFormat: getCurrentTransferFormat,
        tearDown,
    };
}

function Resizer() {
    let resized;
    function resize(element, top, left, width, height, zIndex) {
        element.style.top = `${top}px`;
        element.style.left = `${left}px`;
        element.style.width = `${width}px`;
        element.style.height = `${height}px`;
        element.style.zIndex = `${zIndex}`;
        element.style.position = "absolute";
        resized = true;
    }
    function clear(element) {
        element.style.top = "";
        element.style.left = "";
        element.style.width = "";
        element.style.height = "";
        element.style.zIndex = "";
        element.style.position = "";
        resized = false;
    }
    function isResized() {
        return resized || false;
    }
    return {
        resize,
        clear,
        isResized,
    };
}

function ReadyHelper(initialPlaybackTime, manifestType, liveSupport, callback) {
    let ready = false;
    const callbackWhenReady = ({ data, timeUpdate }) => {
        if (ready)
            return;
        if (!data) {
            ready = false;
        }
        else if (timeUpdate) {
            ready = isValidTime(data);
        }
        else {
            ready = isValidState(data) && isValidTime(data);
        }
        if (ready && callback) {
            callback();
        }
    };
    function isValidState({ state }) {
        return state ? state !== MediaState.FATAL_ERROR : false;
    }
    function isValidTime({ currentTime, seekableRange }) {
        if (manifestType === ManifestType.STATIC)
            return validateStaticTime(currentTime);
        return validateLiveTime(currentTime, seekableRange);
    }
    function validateStaticTime(currentTime) {
        if (currentTime !== undefined) {
            return initialPlaybackTime ? currentTime > 0 : currentTime >= 0;
        }
        return false;
    }
    function validateLiveTime(currentTime, seekableRange) {
        if (liveSupport === LiveSupport.PLAYABLE || liveSupport === LiveSupport.RESTARTABLE) {
            return currentTime ? currentTime >= 0 : false;
        }
        return isValidSeekableRange(seekableRange);
    }
    function isValidSeekableRange(seekableRange) {
        return seekableRange ? !(seekableRange.start === 0 && seekableRange.end === 0) : false;
    }
    return {
        callbackWhenReady,
    };
}

const SEGMENT_TEMPLATE_MATCHER = /\$[A-Za-z]+\$/g;
function findSegmentTemplate(url) {
    const matches = url.match(SEGMENT_TEMPLATE_MATCHER);
    if (matches == null) {
        return null;
    }
    return matches[matches.length - 1];
}

function Subtitles(mediaPlayer, autoStart, playbackElement, defaultStyleOpts, mediaSources, callback) {
  const useLegacySubs = window.bigscreenPlayer?.overrides?.legacySubtitles ?? false;
  const embeddedSubs = window.bigscreenPlayer?.overrides?.embeddedSubtitles ?? false;

  const isSeekableLiveSupport =
    window.bigscreenPlayer.liveSupport == null || window.bigscreenPlayer.liveSupport === "seekable";

  let subtitlesEnabled = autoStart;
  let subtitlesContainer;

  if (available()) {
    if (useLegacySubs) {
      import('./legacysubtitles-1b558d73.js')
        .then(({ default: LegacySubtitles }) => {
          subtitlesContainer = LegacySubtitles(mediaPlayer, autoStart, playbackElement, mediaSources, defaultStyleOpts);
          callback(subtitlesEnabled);
        })
        .catch(() => {
          Plugins.interface.onSubtitlesDynamicLoadError();
        });
    } else if (embeddedSubs) {
      import('./embeddedsubtitles-72e6399b.js')
        .then(({ default: EmbeddedSubtitles }) => {
          subtitlesContainer = EmbeddedSubtitles(
            mediaPlayer,
            autoStart,
            playbackElement,
            mediaSources,
            defaultStyleOpts
          );
          callback(subtitlesEnabled);
        })
        .catch(() => {
          Plugins.interface.onSubtitlesDynamicLoadError();
        });
    } else {
      import('./imscsubtitles-0782d86a.js')
        .then(({ default: IMSCSubtitles }) => {
          subtitlesContainer = IMSCSubtitles(mediaPlayer, autoStart, playbackElement, mediaSources, defaultStyleOpts);
          callback(subtitlesEnabled);
        })
        .catch(() => {
          Plugins.interface.onSubtitlesDynamicLoadError();
        });
    }
  } else {
    /* This is needed to deal with a race condition wherein the Subtitles Callback runs before the Subtitles object
     * has finished construction. This is leveraging a feature of the Javascript Event Loop, specifically how it interacts
     * with Promises, called Microtasks.
     *
     * For more information, please see:
     * https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide
     */
    Promise.resolve().then(() => {
      callback(subtitlesEnabled);
    });
  }

  function enable() {
    subtitlesEnabled = true;
  }

  function disable() {
    subtitlesEnabled = false;
  }

  function show() {
    if (available() && enabled()) {
      subtitlesContainer?.start();
    }
  }

  function hide() {
    if (available()) {
      subtitlesContainer?.stop();
    }
  }

  function enabled() {
    return subtitlesEnabled
  }

  function available() {
    if (embeddedSubs) {
      return mediaPlayer && mediaPlayer.isSubtitlesAvailable()
    }

    const url = mediaSources.currentSubtitlesSource();

    if (!(typeof url === "string" && url !== "")) {
      return false
    }

    const isWhole = findSegmentTemplate(url) == null;

    return isWhole || (!useLegacySubs && isSeekableLiveSupport)
  }

  function setPosition(position) {
    subtitlesContainer?.updatePosition(position);
  }

  function customise(styleOpts) {
    subtitlesContainer?.customise(styleOpts, subtitlesEnabled);
  }

  function renderExample(exampleXmlString, styleOpts, safePosition) {
    subtitlesContainer?.renderExample(exampleXmlString, styleOpts, safePosition);
  }

  function clearExample() {
    subtitlesContainer?.clearExample();
  }

  function tearDown() {
    subtitlesContainer?.tearDown();
  }

  return {
    enable,
    disable,
    show,
    hide,
    enabled,
    available,
    setPosition,
    customise,
    renderExample,
    clearExample,
    tearDown,
  }
}

const Timeline = {
    AVAILABILITY_TIME: "availabilityTime",
    MEDIA_SAMPLE_TIME: "mediaSampleTime",
    PRESENTATION_TIME: "presentationTime",
};

/**
 * @module bigscreenplayer/bigscreenplayer
 */

/**
 * @import {
 *   InitCallbacks, InitData, SubtitlesCustomisationOptions,PlaybackTime
 * } from './types.ts'
 */

function BigscreenPlayer() {
  const _callbacks = {
    stateChange: [],
    timeUpdate: [],
    subtitle: [],
    audioDescribed: [],
    playerReady: undefined,
    playerError: undefined,
  };

  let mediaKind;
  let initialPlaybackTime;
  let playerComponent;
  let resizer;
  let pauseTrigger;
  let isSeeking = false;
  let endOfStream;
  let mediaSources;
  let playbackElement;
  let readyHelper;
  let subtitles;

  const END_OF_STREAM_TOLERANCE = 10;

  function mediaStateUpdateCallback(evt) {
    if (evt.timeUpdate) {
      CallCallbacks(_callbacks.timeUpdate, {
        currentTime: evt.data.currentTime,
        endOfStream,
      });
    } else {
      let stateObject = { state: evt.data.state };

      if (evt.data.state === MediaState.PAUSED) {
        endOfStream = false;
        stateObject.trigger = pauseTrigger || PauseTriggers.DEVICE;
        pauseTrigger = undefined;
      }

      if (evt.data.state === MediaState.FATAL_ERROR) {
        stateObject = {
          state: MediaState.FATAL_ERROR,
          isBufferingTimeoutError: evt.isBufferingTimeoutError,
          code: evt.code,
          message: evt.message,
        };
      }

      if (evt.data.state === MediaState.WAITING) {
        stateObject.isSeeking = isSeeking;
        isSeeking = false;
      }

      stateObject.endOfStream = endOfStream;
      DebugTool.statechange(evt.data.state);

      CallCallbacks(_callbacks.stateChange, stateObject);
    }

    if (
      evt.data.seekableRange &&
      typeof evt.data.seekableRange.start === "number" &&
      typeof evt.data.seekableRange.end === "number"
    ) {
      DebugTool.staticMetric("seekable-range", [evt.data.seekableRange.start, evt.data.seekableRange.end]);
    }

    if (evt.data.duration) {
      DebugTool.staticMetric("duration", evt.data.duration);
    }

    if (playerComponent && readyHelper) {
      readyHelper.callbackWhenReady(evt);
    }
  }

  function bigscreenPlayerDataLoaded({ media, enableSubtitles, enableAudioDescribed }) {
    const initialPresentationTime =
      initialPlaybackTime == null ? undefined : convertPlaybackTimeToPresentationTimeInSeconds(initialPlaybackTime);

    endOfStream =
      mediaSources.time().manifestType === ManifestType.DYNAMIC &&
      !initialPresentationTime &&
      initialPresentationTime !== 0;

    playerComponent = PlayerComponent(
      playbackElement,
      { media, enableAudioDescribed, initialPlaybackTime: initialPresentationTime },
      mediaSources,
      mediaStateUpdateCallback,
      _callbacks.playerError,
      callAudioDescribedCallbacks
    );

    readyHelper = ReadyHelper(
      initialPresentationTime,
      mediaSources.time().manifestType,
      PlayerComponent.getLiveSupport(),
      () => {
        _callbacks.playerReady && _callbacks.playerReady();
        subtitles = Subtitles(
          playerComponent,
          enableSubtitles,
          playbackElement,
          media.subtitleCustomisation,
          mediaSources,
          callSubtitlesCallbacks
        );
      }
    );
  }

  /**
   * @typedef {Object} PlaybackTimeInit
   * @property {number} seconds
   * @property {Timeline} [timeline]
   */

  /**
   * Normalise time input to the 'PlaybackTime' model, so the unit and timeline is explicit.
   * @param {number | PlaybackTimeInit} init
   * @returns {PlaybackTime}
   */
  function createPlaybackTime(init) {
    if (typeof init === "number") {
      return { seconds: init, timeline: Timeline.PRESENTATION_TIME }
    }

    if (init == null || typeof init !== "object" || typeof init.seconds !== "number") {
      throw new TypeError("A numerical playback time must be provided")
    }

    return { seconds: init.seconds, timeline: init.timeline ?? Timeline.PRESENTATION_TIME }
  }

  function convertPlaybackTimeToPresentationTimeInSeconds(playbackTime) {
    const { seconds, timeline } = playbackTime;

    switch (timeline) {
      case Timeline.PRESENTATION_TIME:
        return seconds
      case Timeline.MEDIA_SAMPLE_TIME:
        return convertMediaSampleTimeToPresentationTimeInSeconds(seconds)
      case Timeline.AVAILABILITY_TIME:
        return convertAvailabilityTimeToPresentationTimeInSeconds(seconds * 1000)
      default:
        return seconds
    }
  }

  function convertPresentationTimeToMediaSampleTimeInSeconds(presentationTimeInSeconds) {
    return mediaSources?.time() == null
      ? null
      : presentationTimeToMediaSampleTimeInSeconds(
          presentationTimeInSeconds,
          mediaSources.time().presentationTimeOffsetInMilliseconds
        )
  }

  function convertMediaSampleTimeToPresentationTimeInSeconds(mediaSampleTimeInSeconds) {
    return mediaSources?.time() == null
      ? null
      : mediaSampleTimeToPresentationTimeInSeconds(
          mediaSampleTimeInSeconds,
          mediaSources.time().presentationTimeOffsetInMilliseconds
        )
  }

  function convertPresentationTimeToAvailabilityTimeInMilliseconds(presentationTimeInSeconds) {
    return mediaSources?.time() == null || mediaSources?.time().manifestType === ManifestType.STATIC
      ? null
      : presentationTimeToAvailabilityTimeInMilliseconds(
          presentationTimeInSeconds,
          mediaSources.time().availabilityStartTimeInMilliseconds
        )
  }

  function convertAvailabilityTimeToPresentationTimeInSeconds(availabilityTimeInMilliseconds) {
    return mediaSources?.time() == null || mediaSources?.time().manifestType === ManifestType.STATIC
      ? null
      : availabilityTimeToPresentationTimeInSeconds(
          availabilityTimeInMilliseconds,
          mediaSources.time().availabilityStartTimeInMilliseconds
        )
  }

  function getInitialPlaybackTime() {
    return initialPlaybackTime
  }

  function toggleDebug() {
    if (playerComponent) {
      DebugTool.toggleVisibility();
    }
  }

  function callSubtitlesCallbacks(enabled) {
    CallCallbacks(_callbacks.subtitle, { enabled });
  }

  function setSubtitlesEnabled(enabled) {
    enabled ? subtitles.enable() : subtitles.disable();
    callSubtitlesCallbacks(enabled);

    if (!resizer.isResized()) {
      enabled ? subtitles.show() : subtitles.hide();
    }
  }

  function isSubtitlesEnabled() {
    return subtitles ? subtitles.enabled() : false
  }

  function isSubtitlesAvailable() {
    return subtitles ? subtitles.available() : false
  }

  function getTimeShiftBufferDepthInMilliseconds() {
    return mediaSources.time()?.timeShiftBufferDepthInMilliseconds ?? null
  }

  function getPresentationTimeOffsetInMilliseconds() {
    return mediaSources.time()?.presentationTimeOffsetInMilliseconds ?? null
  }

  function callAudioDescribedCallbacks(enabled) {
    CallCallbacks(_callbacks.audioDescribed, { enabled });
  }

  return /** @alias module:bigscreenplayer/bigscreenplayer */ {
    /**
     * Call first to initialise bigscreen player for playback.
     * @function
     * @name init
     * @param {HTMLDivElement} playbackElement - The Div element where content elements should be rendered
     * @param {InitData} bigscreenPlayerData
     * @param {InitCallbacks} callbacks
     */
    init: (newPlaybackElement, bigscreenPlayerData, callbacks = {}) => {
      playbackElement = newPlaybackElement;
      DebugTool.init();
      DebugTool.setRootElement(playbackElement);
      resizer = Resizer();

      mediaKind = bigscreenPlayerData.media.kind;

      if (bigscreenPlayerData.initialPlaybackTime || bigscreenPlayerData.initialPlaybackTime === 0) {
        initialPlaybackTime = createPlaybackTime(bigscreenPlayerData.initialPlaybackTime);
      }

      DebugTool.staticMetric("version", Version);

      if (initialPlaybackTime) {
        const { seconds, timeline } = initialPlaybackTime;
        DebugTool.staticMetric("initial-playback-time", [seconds, timeline]);
      }

      if (typeof window.bigscreenPlayer?.playbackStrategy === "string") {
        DebugTool.staticMetric("strategy", window.bigscreenPlayer && window.bigscreenPlayer.playbackStrategy);
      }

      _callbacks.playerReady = callbacks.onSuccess;
      _callbacks.playerError = callbacks.onError;

      mediaSources = MediaSources();

      mediaSources
        .init(bigscreenPlayerData.media, bigscreenPlayerData.enableAudioDescribed)
        .then(() => bigscreenPlayerDataLoaded(bigscreenPlayerData))
        .catch((reason) => {
          if (typeof callbacks?.onError === "function") {
            callbacks.onError(reason);
          }
        });
    },

    /**
     * Should be called at the end of all playback sessions. Resets state and clears any UI.
     * @function
     * @name tearDown
     */
    tearDown() {
      if (subtitles) {
        subtitles.tearDown();
        subtitles = undefined;
      }

      if (playerComponent) {
        playerComponent.tearDown();
        playerComponent = undefined;
      }

      if (mediaSources) {
        mediaSources.tearDown();
        mediaSources = undefined;
      }

      _callbacks.stateChange = [];
      _callbacks.timeUpdate = [];
      _callbacks.subtitle = [];
      _callbacks.audioDescribed = [];
      endOfStream = undefined;
      mediaKind = undefined;
      pauseTrigger = undefined;
      resizer = undefined;
      this.unregisterPlugin();
      DebugTool.tearDown();
    },

    /**
     * Pass a function to call whenever the player transitions state.
     * @see {@link module:models/mediastate}
     * @function
     * @param {Function} callback
     */
    registerForStateChanges: (callback) => {
      _callbacks.stateChange.push(callback);
      return callback
    },

    /**
     * Unregisters a previously registered callback.
     * @function
     * @param {Function} callback
     */
    unregisterForStateChanges: (callback) => {
      const indexOf = _callbacks.stateChange.indexOf(callback);
      if (indexOf !== -1) {
        _callbacks.stateChange.splice(indexOf, 1);
      }
    },

    /**
     * Pass a function to call whenever the player issues a time update.
     * @function
     * @param {Function} callback
     */
    registerForTimeUpdates: (callback) => {
      _callbacks.timeUpdate.push(callback);
      return callback
    },

    /**
     * Unregisters a previously registered callback.
     * @function
     * @param {Function} callback
     */
    unregisterForTimeUpdates: (callback) => {
      const indexOf = _callbacks.timeUpdate.indexOf(callback);
      if (indexOf !== -1) {
        _callbacks.timeUpdate.splice(indexOf, 1);
      }
    },

    /**
     * Pass a function to be called whenever subtitles are enabled or disabled.
     * @function
     * @param {Function} callback
     */
    registerForSubtitleChanges: (callback) => {
      _callbacks.subtitle.push(callback);
      return callback
    },

    /**
     * Unregisters a previously registered callback for changes to subtitles.
     * @function
     * @param {Function} callback
     */
    unregisterForSubtitleChanges: (callback) => {
      const indexOf = _callbacks.subtitle.indexOf(callback);
      if (indexOf !== -1) {
        _callbacks.subtitle.splice(indexOf, 1);
      }
    },

    /**
     * Pass a function to be called whenever Audio Described is enabled or disabled.
     * @function
     * @param {Function} callback
     */
    registerForAudioDescribedChanges: (callback) => {
      _callbacks.audioDescribed.push(callback);
      return callback
    },

    /**
     * Unregisters a previously registered callback for changes to Audio Described.
     * @function
     * @param {Function} callback
     */
    unregisterForAudioDescribedChanges: (callback) => {
      const indexOf = _callbacks.audioDescribed.indexOf(callback);
      if (indexOf !== -1) {
        _callbacks.audioDescribed.splice(indexOf, 1);
      }
    },

    /**
     * Sets the current time of the media asset.
     * @function
     * @param {number} seconds
     * @param {Timeline} timeline
     */
    setCurrentTime(seconds, timeline) {
      const playbackTime = createPlaybackTime({ seconds, timeline });

      DebugTool.apicall("setCurrentTime", [playbackTime.seconds.toFixed(3), playbackTime.timeline]);

      if (playerComponent) {
        // this flag must be set before calling into playerComponent.setCurrentTime - as this synchronously fires a WAITING event (when native strategy).
        isSeeking = true;

        const presentationTimeInSeconds = convertPlaybackTimeToPresentationTimeInSeconds(playbackTime);

        playerComponent.setCurrentTime(presentationTimeInSeconds);

        endOfStream =
          mediaSources.time().manifestType === ManifestType.DYNAMIC &&
          Math.abs(this.getSeekableRange().end - presentationTimeInSeconds) < END_OF_STREAM_TOLERANCE;
      }
    },

    /**
     * Set the media element playback rate
     *
     * @function
     * @param {Number} rate
     */
    setPlaybackRate: (rate) => {
      if (playerComponent) {
        playerComponent.setPlaybackRate(rate);
      }
    },

    /**
     * Get the current playback rate
     * @function
     * @returns {Number} the current media playback rate
     */
    getPlaybackRate: () => playerComponent && playerComponent.getPlaybackRate(),

    /**
     * Set constrained bitrate given a min/max range and mediakind.
     */
    setBitrateConstraint: (mediaKind, minBitrateKbps, maxBitrateKbps) => {
      if (playerComponent) {
        playerComponent.setBitrateConstraint(mediaKind, minBitrateKbps, maxBitrateKbps);
      }
    },

    /**
     * Returns current playback bitrate for media kind.
     */
    getPlaybackBitrate: (mediaKind) => {
      if (playerComponent) {
        return playerComponent.getPlaybackBitrate(mediaKind)
      }
    },

    /**
     * Returns the media asset's current time in seconds.
     * @function
     * @returns {Number}
     */
    getCurrentTime: () => (playerComponent && playerComponent.getCurrentTime()) || 0,

    /**
     * Returns the current media kind.
     * 'audio' or 'video'
     * @function
     */
    getMediaKind: () => mediaKind,

    /**
     * Returns an object including the current start and end times.
     * @function
     * @returns {Object | null} {start: Number, end: Number}
     */
    getSeekableRange: () => playerComponent?.getSeekableRange() ?? null,

    /**
     * @function
     * @returns {boolean} Returns true if media is initialised and playing a live stream within a tolerance of the end of the seekable range (10 seconds).
     */
    isPlayingAtLiveEdge() {
      return (
        !!playerComponent &&
        mediaSources.time().manifestType === ManifestType.DYNAMIC &&
        Math.abs(this.getSeekableRange().end - this.getCurrentTime()) < END_OF_STREAM_TOLERANCE
      )
    },

    /**
     * @function
     * @returns the duration of the media asset.
     */
    getDuration: () => playerComponent && playerComponent.getDuration(),

    /**
     * @function
     * @returns if the player is paused.
     */
    isPaused: () => (playerComponent ? playerComponent.isPaused() : true),

    /**
     * @function
     * @returns if the media asset has ended.
     */
    isEnded: () => (playerComponent ? playerComponent.isEnded() : false),

    /**
     * Play the media assest from the current point in time.
     * @function
     */
    play: () => {
      DebugTool.apicall("play");

      playerComponent.play();
    },
    /**
     * Pause the media asset.
     * @function
     * @param {*} opts
     * @param {boolean} opts.userPause
     */
    pause: (opts) => {
      DebugTool.apicall("pause");

      pauseTrigger = opts?.userPause || opts?.userPause == null ? PauseTriggers.USER : PauseTriggers.APP;

      playerComponent.pause();
    },

    /**
     * Resize the video container div in the most compatible way
     *
     * @function
     * @param {Number} top - px
     * @param {Number} left -  px
     * @param {Number} width -  px
     * @param {Number} height -  px
     * @param {Number} zIndex
     */
    resize: (top, left, width, height, zIndex) => {
      subtitles.hide();
      resizer.resize(playbackElement, top, left, width, height, zIndex);
    },

    /**
     * Clear any resize properties added with `resize`
     * @function
     */
    clearResize: () => {
      if (subtitles.enabled()) {
        subtitles.show();
      } else {
        subtitles.hide();
      }
      resizer.clear(playbackElement);
    },

    /**
     * Set whether or not subtitles should be enabled.
     * @function
     * @param {boolean} value
     */
    setSubtitlesEnabled,

    /**
     * @function
     * @return if subtitles are currently enabled.
     */
    isSubtitlesEnabled,

    /**
     * @function
     * @return Returns whether or not subtitles are currently enabled.
     */
    isSubtitlesAvailable,

    /**
     * Returns if a device supports the customisation of subtitles
     *
     * @returns boolean
     */
    areSubtitlesCustomisable: () =>
      !(window.bigscreenPlayer && window.bigscreenPlayer.overrides && window.bigscreenPlayer.overrides.legacySubtitles),

    /**
     * Customise the rendered subitles style
     *
     * @param {SubtitlesCustomisationOptions} styleOpts
     */
    customiseSubtitles: (styleOpts) => {
      if (subtitles) {
        subtitles.customise(styleOpts);
      }
    },

    /**
     * Render an example subtitles string with a given style and location
     *
     * @param {string} xmlString - EBU-TT-D compliant XML String
     * @param {SubtitlesCustomisationOptions} styleOpts
     * @param {DOMRect} safePosition
     */
    renderSubtitleExample: (xmlString, styleOpts, safePosition) => {
      if (subtitles) {
        subtitles.renderExample(xmlString, styleOpts, safePosition);
      }
    },

    /**
     * Clear the example subtitle string
     */
    clearSubtitleExample: () => {
      if (subtitles) {
        subtitles.clearExample();
      }
    },

    /**
     * @function
     * @returns {boolean} true if an Audio Described track is available
     */
    isAudioDescribedAvailable: () => playerComponent && playerComponent.isAudioDescribedAvailable(),

    /**
     * @function
     * @returns {boolean} true if the Audio Described track is currently being used
     */
    isAudioDescribedEnabled: () => playerComponent && playerComponent.isAudioDescribedEnabled(),

    /**
     * @function
     * @returns {Promise<void>}
     */
    setAudioDescribed: (enabled) => playerComponent.setAudioDescribed(enabled),

    /**
     *
     * An enum may be used to set the on-screen position of any transport controls
     * (work in progress to remove this - UI concern).
     * @function
     * @param {*} position
     */
    setTransportControlsPosition: (position) => {
      if (subtitles) {
        subtitles.setPosition(position);
      }
    },

    /**
     * @function
     * @return Returns whether the current media asset is seekable.
     */
    canSeek() {
      return (
        mediaSources.time().manifestType === ManifestType.STATIC ||
        canPauseAndSeek(getLiveSupport(), this.getSeekableRange())
      )
    },

    /**
     * @function
     * @return Returns whether the current media asset is pausable.
     */
    canPause() {
      return (
        mediaSources.time().manifestType === ManifestType.STATIC ||
        canPauseAndSeek(getLiveSupport(), this.getSeekableRange())
      )
    },

    /**
     * Return a mock for in place testing.
     * @function
     * @param {*} opts
     */
    mock(opts) {
      MockBigscreenPlayer.mock(this, opts);
    },

    /**
     * Unmock the player.
     * @function
     */
    unmock() {
      MockBigscreenPlayer.unmock(this);
    },

    /**
     * Return a mock for unit tests.
     * @function
     * @param {*} opts
     */
    mockJasmine(opts) {
      MockBigscreenPlayer.mockJasmine(this, opts);
    },

    /**
     * Register a plugin for extended events.
     * @function
     * @param {*} plugin
     */
    registerPlugin: (plugin) => Plugins.registerPlugin(plugin),

    /**
     * Unregister a previously registered plugin.
     * @function
     * @param {*} plugin
     */
    unregisterPlugin: (plugin) => Plugins.unregisterPlugin(plugin),

    /**
     * Returns an object with a number of functions related to the ability to transition state
     * given the current state and the playback strategy in use.
     * @function
     */
    transitions: () => (playerComponent ? playerComponent.transitions() : {}),

    /**
     * @function
     * @return The media element currently being used.
     */
    getPlayerElement: () => playerComponent && playerComponent.getPlayerElement(),

    /**
     * @function
     * @return The runtime version of the library.
     */
    getFrameworkVersion: () => Version,

    /**
     * Toggle the visibility of the debug tool overlay.
     * @function
     */
    toggleDebug,

    /**
     * @function
     * @return {Object} - Key value pairs of available log levels
     */
    getLogLevels: () => DebugTool.logLevels,

    /**
     * @function
     * @param logLevel -  log level to display @see getLogLevels
     */
    setLogLevel: (level) => DebugTool.setLogLevel(level),
    getDebugLogs: () => DebugTool.getDebugLogs(),
    convertPresentationTimeToMediaSampleTimeInSeconds,
    convertMediaSampleTimeToPresentationTimeInSeconds,
    convertPresentationTimeToAvailabilityTimeInMilliseconds,
    convertAvailabilityTimeToPresentationTimeInSeconds,
    getInitialPlaybackTime,
    getTimeShiftBufferDepthInMilliseconds,
    getPresentationTimeOffsetInMilliseconds,
  }
}

function getLiveSupport() {
  return PlayerComponent.getLiveSupport()
}

BigscreenPlayer.getLiveSupport = getLiveSupport;

BigscreenPlayer.version = Version;

/**
 * Provides an enumeration of on-screen transport control positions, which can be combined as flags.
 */
const TransportControlPosition = {
    /** No transport controls are visible. */
    NONE: 0,
    /** The basic transport controls are visible. */
    CONTROLS_ONLY: 1,
    /** The transport controls are visible with an expanded info area. */
    CONTROLS_WITH_INFO: 2,
    /** The left-hand onwards navigation carousel is visible. */
    LEFT_CAROUSEL: 4,
    /** The bottom-right onwards navigation carousel is visible. */
    BOTTOM_CAROUSEL: 8,
    /** The whole screen is obscured by a navigation menu. */
    FULLSCREEN: 16,
};

/**
 * Enums for WindowTypes
 * @readonly
 * @enum {string}
 */
const WindowTypes = {
    /** Media with a duration */
    STATIC: "staticWindow",
    /** Media with a start time but without a duration until an indeterminate time in the future */
    GROWING: "growingWindow",
    /** Media with a rewind window that progresses through a media timeline */
    SLIDING: "slidingWindow",
};

export { BigscreenPlayer as B, DOMHelpers as D, EntryCategory as E, LoadUrl as L, MediaKinds as M, Plugins as P, TransportControlPosition as T, Utils as U, WindowTypes as W, DebugTool as a, LiveSupport as b, ManifestType as c, MediaState as d, autoResumeAtStartOfRange as e, findSegmentTemplate as f, MockBigscreenPlayer as g, PauseTriggers as h, PlaybackStrategy as i, TransferFormat as j, Timeline as k, isMessage as l, isMetric as m, isTrace as n };