UNPKG

@liveryvideo/player

Version:

Livery video player for use in web browsers.

1,610 lines (1,608 loc) 1.04 MB
import { css as si, LitElement as ri, html as dt, nothing as vc } from "lit"; import { property as fe, query as zt, customElement as js, state as st } from "lit/decorators.js"; import { classMap as Ke } from "lit/directives/class-map.js"; import { ifDefined as er } from "lit/directives/if-defined.js"; import { repeat as wc } from "lit/directives/repeat.js"; import "reflect-metadata"; import Ia from "lodash-es/debounce.js"; import "sprintf-js"; import Uo from "lodash-es/throttle.js"; const ls = class ls extends Event { constructor(e, t) { super(ls.type, t), this.config = e; } }; ls.type = "livery-config-change"; let pr = ls; const ds = class ds extends Event { constructor(e, t) { super(ds.type, t), this.display = e; } }; ds.type = "livery-display-change"; let fr = ds; const hs = class hs extends Event { constructor(e, t) { super(hs.type, t), this.error = e; } }; hs.type = "livery-error"; let Hi = hs; const ps = class ps extends Event { constructor(e, t) { super(ps.type, t), this.features = e; } }; ps.type = "livery-features-change"; let mr = ps; const fs = class fs extends Event { constructor(e, t) { super(fs.type, t), this.fullscreen = e; } }; fs.type = "livery-fullscreen-change"; let gr = fs; const ms = class ms extends Event { constructor(e, t) { super(ms.type, t), this.mode = e; } }; ms.type = "livery-mode-change"; let yr = ms; const gs = class gs extends Event { constructor(e, t) { super(gs.type, t), this.phase = e; } }; gs.type = "livery-phase-change"; let br = gs; const ys = class ys extends Event { constructor(e, t) { super(ys.type, t), this.playbackState = e, this.paused = ["ENDED", "PAUSED"].includes(e), this.playing = ["FAST_FORWARD", "PLAYING", "REWIND", "SLOW_MO"].includes( e ), this.stalled = ["BUFFERING", "SEEKING"].includes(e); } }; ys.type = "livery-playback-change"; let vr = ys; const bs = class bs extends Event { constructor(e, t) { super(bs.type, t), this.qualities = e; } }; bs.type = "livery-qualities-change"; let wr = bs; const vs = class vs extends Event { constructor(e, t) { super(vs.type, t), this.quality = e; } }; vs.type = "livery-quality-change"; let Sr = vs; const ws = class ws extends Event { constructor(e) { super(ws.type, e); } }; ws.type = "livery-recovered"; let Er = ws; const Ss = class Ss extends Event { constructor(e, t, i) { super(Ss.type, i), this.volume = e, this.muted = t; } }; Ss.type = "livery-volume-change"; let Pn = Ss; function _n(r) { const e = typeof r == "number" ? `Timed out after ${r}ms` : r; return new DOMException(e, "TimeoutError"); } let Cr = !0; try { const r = new EventTarget(), e = new AbortController(); e.abort(), r.addEventListener( "test", () => { Cr = !1; }, { signal: e.signal } ), r.dispatchEvent(new Event("test")); } catch { Cr = !1; } class ke extends AbortController { /** * Returns an Abortable which will be aborted with the related reason when one of the specified parent abort signals * (if any) is aborted or when this is aborted by calling the `abort(reason)` method. * * Note: This supports `AbortSignal | undefined` arguments to facilitate use with an optional signal argument. * * For details see class documentation: {@link Abortable} * * @example * ```ts * class Example { * abortable: Abortable; * * constructor(signal?: AbortSignal) { * this.abortable = new Abortable(signal); * // The instance abortable can be used to control the life cycle of the instance * // e.g: Keep doing some work in the background until it is aborted * // directly (`this.abortable.abort()`) or through the signal specified to this constructor * } * * doSomethingAsync(signal?: AbortSignal) { * const abortable = new Abortable(this.abortable.signal, signal); * // The method abortable will be aborted with the corresponding reason as soon as * // either the instance or the method is aborted * } * } * ``` */ constructor(...e) { super(), this.promise = new Promise((t, i) => { this.onAbort(i); }), this.promise.catch(() => { }); for (const t of e) { if (!t) continue; const i = () => this.abort(ke.getAbortError(t)); if (t.aborted) { i(); return; } this.eventListener(t, "abort", i); } } /** * Returns true if this Abortable has signaled to abort, and false otherwise. * * @example * // This is just a shortcut * abortable.aborted === abortable.signal.aborted; */ get aborted() { return this.signal.aborted; } /** * Returns abort reason when aborted, and undefined otherwise. * * @example * ```typescript * // Equivalent to abortable.throwIfAborted() * if (abortable.reason) { // i.e: aborted * throw abortable.reason; * } * ``` */ get reason() { return this.signal.reason; } /** * Returns an `AbortSignal` that is already set as aborted with `reason` (and which does not trigger an abort event). * * This is an alias to {@link AbortSignal.abort} or a shim when that's not supported (e.g: Safari v14 and older). * * @param reason - The reason why the operation was aborted, defaults to an `'AbortError'` `DOMException` * * @example * // For testing or if a function call can't be prevented: * doSomething(Abortable.abort()); */ static abort(e = ke.createAbortError()) { if (AbortSignal.abort) return AbortSignal.abort(e); const t = new ke(); return t.abort(e), t.signal; } /** * Silently ignore specified `error` if it is an AbortError, otherwise rethrow it. * * Useful to swallow expected AbortErrors, e.g: after a user aborts an action. * * @param error - Error * @throws `error` when it is not an AbortError * * @example * // If this signal is aborted that's fine and we don't need to do anything * doSomething(signal).catch(Abortable.catchAbortError); */ static catchAbortError(e) { if (!ke.isAbortError(e)) throw e; } /** * Returns a DOMException with `code: 20`, `name: 'AbortError'` and default `message: 'Aborted'`, * as generally used when an abortable function (e.g: `fetch()`) is aborted. * * @param message - Error message * * @example * // Create AbortError at this stack location and with custom message * abortable.abort(Abortable.createAbortError('Stopped')); */ static createAbortError(e = "Aborted") { return new DOMException(e, "AbortError"); } /** * Returns an `Error` based on specified `signal`'s abort `reason`. * * If the `reason` is an `Error` that is returned. * If it is `undefined` an `'AbortError'` with default message `'Aborted'` is created and returned. * Otherwise an `AbortError` with the `reason` converted to string as message is created and returned. * * @param signal - `AbortSignal` to derive `Error` from * @throws Error when `signal` is not aborted * * @example * ```typescript * // Throw abort reason Error; even if reason might not be an Error * if (signal.aborted) { * throw Abortable.getAbortError(signal); * } * ``` */ static getAbortError(e) { if (!e.aborted) throw new Error("Signal is not aborted"); return e.reason instanceof Error ? e.reason : e.reason === void 0 ? ke.createAbortError() : ke.createAbortError(String(e.reason)); } /** * Returns true if `error` is an `Error` with `name: 'AbortError'`, false otherwise. * * Note: This does not require `error` to be a DOMException with `code: 20` as that is commonly, * but not necessarily always, the case. * * @param error - Error object * * @example * ```typescript * doSomething(signal).catch((reason) => { * if (Abortable.isAbortError(reason)) { * // .. * } * }); * ``` */ static isAbortError(e) { return e instanceof Error && e.name === "AbortError"; } /** * Returns an `AbortSignal` that will automatically abort after the specified `time`. * * The signal aborts with a TimeoutError DOMException on timeout, * or with AbortError DOMException due to pressing a browser stop button (or some other inbuilt "stop" operation). * This allow UIs to differentiate timeout errors, which typically require user notification, * from user-triggered aborts that do not. * * The timeout is based on active rather than elapsed time, and will effectively be paused if the code is running * in a suspended worker, or while the document is in a back-forward cache ("bfcache"). * * This is an alias to {@link AbortSignal.timeout} or a shim when that's not supported (e.g: Safari v15 and older). * * @param time - The 'active' time in milliseconds before the returned AbortSignal will abort * * @example * // Abort with TimeoutError after 3 seconds * doSomething(Abortable.timeout(3000)); */ static timeout(e) { if (AbortSignal.timeout) return AbortSignal.timeout(e); const t = new ke(); return window.setTimeout(() => { t.abort(_n(e)); }, e), t.signal; } /** * Invoking this method will change this `signal.aborted` to `true` and `signal.reason` to specified value * and it will synchronously call all `'abort'` listeners, in the order that they were added, * to inform that the associated activity is to be aborted. * * Note that if a listener throws that will result in a global uncaught error without disrupting remaining listeners. * * This is an alias to {@link AbortController.abort}, * but this defaults to an `'AbortError'` `DOMException` as defined by the standard * and unlike the standard this `reason` argument has to be an `Error` to facilitate debugging. * * This shims `reason` support when that's not supported (e.g: Safari v15.3 and older) * * @param reason - The reason why the operation was aborted, defaults to an `'AbortError'` `DOMException` * * @example * ```typescript * doSomething(abortable.signal).catch(() => console.log('rejected')); * abortable.abort(); // => logs: "rejected" * ``` */ abort(e = ke.createAbortError()) { this.aborted || (super.abort(e), this.reason !== e && (this.signal.reason = e)); } /** * Returns a Promise that is resolved with an array of results when all of the promises returned * by specified `promisesFactory` resolve. * When any factory returned Promise is rejected the returned Promise is rejected with that reason. * Then the `settledSignal` provided to the factory is aborted with an `AbortError` with message * `'Settled'` to cancel any remaining work in the other promises. * If this abortable is aborted before that time the returned Promise rejects with the abort reason instead * and the `settledSignal` will be aborted with that reason as well. * * @param promisesFactory - Function returning the Iterable of promises to fulfill * * @example * ```typescript * // To Promise.all() async methods immediately rejecting when aborted and aborting other promises when one rejects: * await abortable.all((settledSignal) => [ * doAsyncAbortableThing1(settledSignal), * doAsyncUnAbortableThing1(), * ]); * ``` */ all(e) { return this.race((t) => [ Promise.all(e(t)) ]); } /** * Returns a Promise that resolves with the value of the first Promise to resolve from the promises * returned by specified `promisesFactory`. * Then the `settledSignal` provided to the factory is aborted with an `AbortError` with message * `'Settled'` to cancel any remaining work in the other promises. * It rejects when all of the promises reject, with an AggregateError containing an array of rejection reasons. * If this abortable is aborted before that time the returned Promise rejects with the abort reason instead * and the `settledSignal` will be aborted with that reason as well. * * @param promisesFactory - Function returning the Iterable of promises to race * * @example * ```typescript * // To Promise.any() async methods immediately rejecting when aborted and aborting other promises when one resolves: * await abortable.any((settledSignal) => [ * doAsyncAbortableThing1(settledSignal), * doAsyncUnAbortableThing1(), * ]); * ``` */ any(e) { return this.race((t) => [ Promise.any(e(t)) ]); } /** * Returns a Promise that settles like `new Promise(executor)` unless this Abortable is aborted before that time, * in which case it rejects with the abort reason instead. * * If `signal` is aborted already while this is called then `executor` will not be called. * * Besides the usual `resolve` and `reject` arguments the `executor` is passed the argument: `settledSignal` * which facilitates aborting any remaining work within the `executor` function after that time. * The `settledSignal` is aborted with message `'Settled'` when the returned Promise settles (resolves or rejects). * If this abortable is aborted before that time it will be aborted with that reason instead. * * If `executor` returns an abort listener function then that will be called with the abort reason when aborted * before otherwise settling as described above. * * @param executor - Promise like executor that can return an abort listener function * * @example * ```typescript * // This facilitates wrapping non-abortable and abortable code with minimal boiler plate: * const response = await abortable.call<Response>((resolve, reject, signal) => { * fetch(url, { signal }).then(resolve, reject); * const timeoutId = window.setTimeout(() => resolve(cachedResponse), timeoutMs); * return () => window.clearTimeout(timeoutId); * }); * ``` */ call(e) { let t, i; return this.race((n) => [ new Promise((s, o) => { t = e(s, o, n), t && (i = this.onAbort(t)); }) ]).finally(() => { i == null || i(); }); } /** * Returns a Promise that resolves after specified `delay`, unless this Abortable is aborted before that time, * in which case it rejects with the abort reason. * * @param delay - Time in milliseconds that the Promise should wait to be resolved * * @example * // Wait for half a second unless aborted * await abortable.delay(500); */ delay(e) { return this.call((t) => { const i = window.setTimeout(t, e); return () => window.clearTimeout(i); }); } /** * Adds an event listener to an event target that is removed when this Abortable is aborted. * * This uses `target.addEventListener` with the `signal` option or shims it when that's not supported * (e.g: Safari v14 and older). * * This method is strict about what EventType strings to accept based specified EventMap * which defaults to the GlobalEventHandlersEventMap. * Unfortunately it seems like this can't be inferred from the target argument, so you'll have * to manually specify the EventMap corresponding to your EventTarget class where necessary. * * @param target - Target to add event listener to * @param type - Event type to listen for * @param listener - Event listener function * @param options - Event listener options * @returns Function that removes event listener and stops listening to 'abort' if shimming * * @example * // Add a window click listener until abortable is aborted * abortable.eventListener(window, 'click', listener); */ eventListener(e, t, i, n = {}) { if (this.aborted) return () => { }; const s = { ...n, signal: this.signal }; e.addEventListener( t, i, s ); const o = () => e.removeEventListener( t, i, s ); if (Cr) return o; const c = this.onAbort(o); return () => { o(), c(); }; } /** * Sets a `callback` to be called repeatedly after specified `delay` with specified `args` * until this Abortable is aborted. * * @param callback - Function to be called * @param delay - Time in milliseconds to delay each callback * @param args - Arguments to pass to callback * @returns Function that clears interval and stops listening to 'abort' * * @example * ```typescript * // To wait until something checks out, unless aborted before that time: * const removeInterval = abortable.interval(() => { * if (checkSomething()) { * removeInterval(); * doSomethingElse() * } * }, 100); * ``` */ interval(e, t, ...i) { const n = window.setInterval(e, t, ...i), s = () => window.clearInterval(n), o = this.onAbort(s); return () => { s(), o(); }; } /** * Add a listener to be called once, either immediately when this Abortable is already aborted or * from this Abortable signal's 'abort' event listener when it becomes aborted later. * * This `Abortable`'s abort `reason: Error` is passed as an argument to the listener. * * @param listener - Function to call when this Abortable is or becomes aborted * @returns Function that removes listener * * @example * ```typescript * abortable.onAbort((reason) => console.log('abort', reason)); * abortable.abort(); // => logs: 'abort' AbortError: Aborted * ``` */ onAbort(e) { const t = () => e(this.reason); return this.aborted ? (t(), () => { }) : (this.signal.addEventListener("abort", t, { once: !0 }), () => { this.signal.removeEventListener("abort", t); }); } /** * Returns a Promise that settles with the eventual state of the first promise that settles from the promises, * returned by specified `promisesFactory`. * When the returned Promise is settled the `settledSignal` provided to the factory is aborted with an `AbortError` * with message `'Settled'` to facilitate aborting any remaining work in the other promises. * If this abortable is aborted before that time the returned Promise rejects with the abort reason instead * and the `settledSignal` will be aborted with that reason as well. * * @param promisesFactory - Function returning the Iterable of promises to race * * @example * ```typescript * // To Promise.race() async methods immediately rejecting when aborted and aborting other promises when one settles: * await abortable.race((settledSignal) => [ * doAsyncAbortableThing1(settledSignal), * doAsyncUnAbortableThing1(), * ]); * ``` */ race(e) { if (this.reason) return Promise.reject(this.reason); const t = new ke(this.signal), i = e(t.signal), n = this.promise; return Promise.race([n, ...i]).finally(() => { t.abort(ke.createAbortError("Settled")); }); } /** * Throws the signal's abort reason if the signal has been aborted; otherwise it does nothing. * * Useful to test at the beginning or in between sections of an async function so as to abort at that point * when the abort event is signalled. * * This is an alias to {@link AbortController.throwIfAborted} or a shim when that's not supported * (e.g: Safari v15.3 and older). * * @throws Abort reason when this is aborted * * @example * ```typescript * // Before and after unabortable method calls * abortable.throwIfAborted(); * await doAsyncUnAbortableThing1(); * abortable.throwIfAborted(); * ``` */ throwIfAborted() { if (this.signal.throwIfAborted) { this.signal.throwIfAborted(); return; } if (this.reason) throw this.reason; } /** * Sets a `callback` to be called after specified `delay` with specified `args` * unless this Abortable is aborted before that time. * * @param callback - Function to be called * @param delay - Time in milliseconds to delay the callback * @param args - Arguments to pass to callback * @returns Function that clears timeout and stops listening to 'abort' * * @example * ```typescript * // To do something after a timeout, unless aborted before that time: * const clearTimeout = abortable.timeout(() => doSomething(), 500); * // And to stop that without aborting this abortable: * clearTimeout(); * ``` */ timeout(e, t, ...i) { const n = window.setTimeout(() => { o(), e(...i); }, t), s = () => window.clearTimeout(n), o = this.onAbort(s); return () => { s(), o(); }; } } function mt(r, e = [], t = !1) { if (e.includes(r)) return "[circular reference]"; if (Array.isArray(r)) { const i = e.slice(0); return i.push(r), `[${r.map((s) => mt(s, i, !0)).join(", ")}]`; } if (r instanceof Error) return `${r.name}: ${r.message}`; if (typeof r == "object" && r !== null) { const i = e.concat(r); return `{ ${Object.entries(r).map( ([s, o]) => `${s}: ${mt(o, i, !0)}` ).join(", ")} }`; } return typeof r == "function" || typeof r == "symbol" ? `[${typeof r}]` : typeof r == "string" ? t ? `'${r}'` : r : String(r); } function Sc(r = /* @__PURE__ */ new Date(), e = 3) { const t = r instanceof Date ? r : new Date(r), i = t.toTimeString().substring(0, 8), n = (t.getMilliseconds() / 1e3).toFixed(e).substring(1); return `${i}${n}`; } function Ne(r) { return r instanceof Error ? r : new Error(String(r)); } const Tr = { DEBUG: { consoleMethod: "debug", emoji: "🔸", weight: 2 }, ERROR: { consoleMethod: "error", emoji: "❌", weight: 5 }, INFO: { consoleMethod: "info", emoji: "🔹", weight: 3 }, QUIET: { weight: 6 }, SPAM: { consoleMethod: "debug", emoji: "⬩", weight: 1 }, WARN: { consoleMethod: "warn", emoji: "⚠️", weight: 4 } }, Ec = Object.keys(Tr), tt = class tt { /** * Logger constructor. * * @param name - Name of Logger, used as prefix */ constructor(e) { const t = tt.nameCountMap.get(e) || 0; this.name = e, this.nr = t + 1, tt.nameCountMap.set(e, this.nr); } /** * Add a LogListener function to invoke alongside the default console logging. */ static addLogListener(e) { if (tt.logListeners.has(e)) throw new Error("listener has already been added"); return tt.logListeners.add(e), () => { tt.logListeners.delete(e); }; } /** * Returns a log prefix string for a LogListener call with log timestamp, level emoji, logger name and nr. * P.e: "15:09:45.227 🔸 [DashMseEngine#2]" * * @param params - Log parameters */ static createPrefix(e) { const { emoji: t, name: i, nr: n, timestamp: s } = e, o = n === 1 ? "" : `#${n}`; return `${s} ${t} [${i}${o}]`; } /** * Returns a stack trace that can be used to suffix a LogListener call. * This will either use the first Error found amonst the log arguments, or create an Error itself. * * @param args - Log arguments */ static createStack(e, t) { const { stack: i } = e; if (!i) return "Error stack unsupported"; const n = i.split(` `); return n[0] === e.toString() && n.shift(), e.message === "" && n.shift(), n[n.length - 1] === "" && n.pop(), n.length > t && (n.length = t), n.map((s) => s.trim()).join(` `); } /** * Returns a log string for a LogListener call using {@link createPrefix} and {@link humanStringify}. * If the log level is ERROR or WARN this also includes the top 3 lines of {@link createStack}. * * @param params - Log parameters * @param args - Log arguments */ static createString(e) { const t = [tt.createPrefix(e)], { details: i, error: n, message: s } = e; if (s && t.push(s), i && t.push(mt(i)), n) { const o = tt.createStack(n, 3).split(` `).map((c) => ` ${c}`).join(` `); t.push(`${n.toString()} ${o}`); } return t.join(" "); } /** * Type guard function that returns true if the specified value is a LogLevelName. * * @param name - Name to check */ static isLevelName(e) { return Ec.includes(e); } /** * Log and throw an error with specified message and details if assertion is false. * Asserts assertion for TypeScript otherwise. * * Note: To use this you will have to explicitly type the instance you are using, * e.g: `log: Logger = new Logger()`. * * @param assertion - Do nothing if true, log and throw error otherwise * @param message - Assertion error message * @param details - Assertion error details */ assert(e, t, i) { if (!e) { const n = new Error(t); throw this.log("ERROR", { message: t, details: i, error: n }), n; } } /** * Log debug information. * * @param message - Log message * @param details - Optional details */ debug(e, t) { this.log("DEBUG", { message: e, details: t }); } /** * Log an error. * * If the first argument is an `Error` then that is used in place of the `error` argument. * * @param messageOrError - Error message or `Error` instance * @param details - Optional details * @param error - Optional `Error` instance for which to show name, message and stack trace */ error(e, t, i = new Error("Undefined error")) { if (e instanceof Error) { this.log("ERROR", { details: t, error: e }); return; } this.log("ERROR", { message: e, details: t, error: i }); } /** * Returns a function that, when called, converts the argument to an Error and logs that as an error. */ handleError() { return (e) => this.error(Ne(e)); } /** * Returns a function that, when called, converts the argument to an Error and logs that as a warning. */ handleWarn() { return (e) => this.warn(Ne(e)); } /** * Log information. * * @param message - Log message * @param details - Optional details */ info(e, t) { this.log("INFO", { message: e, details: t }); } /** * Log arguments at specified level. * * Note: It is generally recommended to use one of the higher level methods instead, * e.g: `error()`, `warn()`, `info()`, `debug()` or `spam()`. * * Calls each LogListener function (if any). * * Logs to console if that is enabled and the call level has a weight equal or higher than the Logger level. * * @param levelName - Name of LogLevel * @param args - Arguments to log */ log(e, { details: t, error: i, message: n }) { if (e === "QUIET") throw new Error("Can not log at level QUIET"); const { options: s } = tt, o = Tr[e], c = s.level ? o.weight >= Tr[s.level].weight : !1, a = { details: t, emoji: o.emoji, enabled: c, error: i, level: e, message: n, name: this.name, nr: this.nr, timestamp: Sc() }; for (const d of tt.logListeners) d(a); if (c && s.console) { const d = [ tt.createPrefix(a), n, t, i ].filter((u) => u !== void 0); console[o.consoleMethod](...d); } } /** * Log spam information. * * @param message - Log message * @param details - Optional details */ spam(e, t) { this.log("SPAM", { message: e, details: t }); } /** * Log a warning. * * If the first argument is an `Error` then that is used in place of the `error` argument. * * @param messageOrError - Error message or `Error` instance * @param details - Optional details * @param error - Optional `Error` instance for which to show name, message and stack trace */ warn(e, t, i = new Error("Undefined error")) { if (e instanceof Error) { this.log("WARN", { error: e }); return; } this.log("WARN", { message: e, details: t, error: i }); } }; tt.options = { /** * If true then log to console. */ console: !0, /** * Global log level. */ level: "INFO" }, tt.logListeners = /* @__PURE__ */ new Set(), tt.nameCountMap = /* @__PURE__ */ new Map(); let we = tt; var Cc = Object.defineProperty, Tc = Object.getOwnPropertyDescriptor, at = (r, e, t, i) => { for (var n = i > 1 ? void 0 : i ? Tc(e, t) : e, s = r.length - 1, o; s >= 0; s--) (o = r[s]) && (n = (i ? o(e, t, n) : o(n)) || n); return i && n && Cc(e, t, n), n; }; let We = class extends ri { constructor() { super(...arguments), this.audioColor = null, this.backgroundColor = null, this.bubbles = !1, this.bufferColor = null, this.decodedColor = null, this.droppedColor = null, this.latencyColor = null, this.maxRows = 60, this.player = null, this.textColor = null, this.updateInterval = 500, this.videoColor = null, this.log = new we("livery-buffer-graph"); } get eventInit() { return { bubbles: this.bubbles, composed: this.bubbles }; } connectedCallback() { super.connectedCallback(), this.onStateChange(); } disconnectedCallback() { super.disconnectedCallback(), this.onStateChange(); } render() { return dt`<div id="container"></div>`; } updated() { this.onStateChange(!0); } addRow() { const { chart: r, rows: e } = this.active; e.push(this.createRow()); const t = this.maxRows * this.updateInterval, i = Date.now(), n = e.findIndex(({ date: o }) => i - o < t); n > 2 && e.splice(0, n - 2); const s = (o, c) => e.map((a) => ({ name: String(a.date), value: [a.date, c ? c(a[o]) : a[o]] })); r.setOption( { series: [ { data: s("audioBuffer") }, { data: s("videoBuffer") }, { data: s("latency") }, // Cap values to not drop below 0 to deal with value resets on engine reload/pause { data: s("decodedFrames", (o) => Math.max(0, o)) }, { data: s("droppedFrames", (o) => Math.max(0, o)) } ], xAxis: { min: i - t } }, !1 ); } createChart() { var i, n, s, o, c, a, d, u, l, h, p, f; const r = this.container, e = window.echarts.init(r), t = new ResizeObserver(() => e.resize()); return t.observe(r), e.setOption({ // Animation looks nice, but negatively affects performance, so let's disable it for now // animationEasingUpdate: 'linear', animation: !1, grid: { backgroundColor: (i = this.backgroundColor) != null ? i : "transparent" }, legend: { show: !0, textStyle: { align: "", color: (n = this.textColor) != null ? n : "#eee" }, width: "75%" }, series: [ { itemStyle: { color: (o = (s = this.audioColor) != null ? s : this.bufferColor) != null ? o : "#0b8" }, name: "Audio", symbol: "none", type: "line" }, { itemStyle: { color: (a = (c = this.videoColor) != null ? c : this.bufferColor) != null ? a : "#00bfff" }, name: "Video", symbol: "none", type: "line" }, { itemStyle: { color: (d = this.latencyColor) != null ? d : "#ffa500" }, name: "Latency", symbol: "none", type: "line" }, { itemStyle: { color: (u = this.decodedColor) != null ? u : "#f6c2f3" }, lineStyle: { opacity: 0.5 }, name: "Decoded", showSymbol: !1, symbol: "triangle", type: "line", yAxisIndex: 1 }, { itemStyle: { color: (l = this.droppedColor) != null ? l : "#fc1c1c" }, lineStyle: { opacity: 0.5 }, name: "Dropped", showSymbol: !1, symbol: "triangle", type: "line", yAxisIndex: 1 } ], xAxis: { axisLabel: { rotate: 30 }, axisLine: { lineStyle: { color: (h = this.textColor) != null ? h : "#eee" } }, type: "time" }, yAxis: [ { axisLine: { lineStyle: { color: (p = this.textColor) != null ? p : "#eee" } }, max: ({ max: v }) => Math.ceil((v + 0.01) * 10) / 10, minInterval: 0.1, min: ({ min: v }) => Math.trunc((v - 0.01) * 10) / 10, name: "● Seconds", type: "value" }, { axisLine: { lineStyle: { color: (f = this.textColor) != null ? f : "#eee" } }, name: "▲ FPS", splitLine: { lineStyle: { opacity: 0.5 } }, type: "value" } ] }), { chart: e, resizeObserver: t }; } createRow() { const r = this.active, { lastDecoded: e, lastDropped: t, lastTime: i, player: { engine: n } } = r, s = Date.now(); if (!n) return { audioBuffer: Number.NaN, date: s, decodedFrames: Number.NaN, droppedFrames: Number.NaN, latency: Number.NaN, videoBuffer: Number.NaN }; const { audioBuffer: o, buffer: c, decodedFrames: a, droppedFrames: d, latency: u, playing: l, videoBuffer: h } = n, p = (s - i) / 1e3; return r.lastTime = s, r.lastDecoded = a, r.lastDropped = d, { audioBuffer: Number.isNaN(o) ? c : o, date: s, decodedFrames: (a - e) / p || Number.NaN, droppedFrames: (d - t) / p || Number.NaN, latency: l ? u : Number.NaN, videoBuffer: Number.isNaN(h) ? c : h }; } handleEngine(r) { const e = this.active; if (window.clearInterval(e.intervalId), e.intervalId = Number.NaN, e.lastDecoded = Number.NaN, e.lastDropped = Number.NaN, e.lastTime = Number.NaN, !r) { this.addRow(); return; } r.onProperty("paused", (t) => { window.clearInterval(e.intervalId), e.intervalId = Number.NaN, this.addRow(), !t && (e.intervalId = window.setInterval( () => this.addRow(), this.updateInterval )); }); } loadScript() { if (!!document.getElementById(We.scriptId)) return; this.log.debug("loadScript"); const e = document.createElement("script"); e.id = We.scriptId, e.onload = () => this.onStateChange(), e.onerror = () => this.onError(new Error(`Error loading script: ${e.src}`)), e.src = "https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js", document.head.appendChild(e); } onError(r) { const e = Ne(r); this.log.error(e), this.dispatchEvent(new Hi(e, this.eventInit)); } onStateChange(r = !1) { if (!window.echarts) { this.loadScript(); return; } this.isConnected ? (r && this.stop(), this.start()) : this.stop(); } start() { var n; if (this.active) { this.log.debug("start(): already active"); return; } const r = (n = this.player) != null ? n : document.querySelector("livery-player"); if (!r) { this.log.debug("start(): wait for player reference to be set"); return; } this.log.debug("start"); const e = new ke(), { chart: t, resizeObserver: i } = this.createChart(); this.active = { abortable: e, chart: t, intervalId: Number.NaN, lastDecoded: Number.NaN, lastDropped: Number.NaN, lastTime: Number.NaN, player: r, resizeObserver: i, rows: [] }, this.handleEngine(r.engine), e.eventListener( r, "livery-engine-change", ({ engine: s }) => this.handleEngine(s) ); } stop() { if (!this.active) return; this.log.debug("stop"); const { abortable: r, chart: e, intervalId: t, resizeObserver: i } = this.active; r.abort(), window.clearInterval(t), e.dispose(), i.disconnect(), this.active = void 0; } }; We.styles = si` :host { display: block; } :host([hidden]) { display: none; } #container { height: 300px; } `; We.scriptId = "livery-buffer-graph-loader"; at([ fe({ reflect: !0, type: String }) ], We.prototype, "audioColor", 2); at([ fe({ reflect: !0, type: String }) ], We.prototype, "backgroundColor", 2); at([ fe({ reflect: !0, type: Boolean }) ], We.prototype, "bubbles", 2); at([ fe({ reflect: !0, type: String }) ], We.prototype, "bufferColor", 2); at([ fe({ reflect: !0, type: String }) ], We.prototype, "decodedColor", 2); at([ fe({ reflect: !0, type: String }) ], We.prototype, "droppedColor", 2); at([ fe({ reflect: !0, type: String }) ], We.prototype, "latencyColor", 2); at([ fe({ reflect: !0, type: Number }) ], We.prototype, "maxRows", 2); at([ fe({ type: Object }) ], We.prototype, "player", 2); at([ fe({ reflect: !0, type: String }) ], We.prototype, "textColor", 2); at([ fe({ reflect: !0, type: Number }) ], We.prototype, "updateInterval", 2); at([ fe({ reflect: !0, type: String }) ], We.prototype, "videoColor", 2); at([ zt("#container") ], We.prototype, "container", 2); We = at([ js("livery-buffer-graph") ], We); var xc = "1.0.37", zs = "", Oo = "?", xr = "function", Ar = "undefined", uo = "object", Gs = "string", Na = "major", z = "model", Q = "name", $ = "type", G = "vendor", ee = "version", ft = "architecture", _i = "console", Ce = "mobile", Te = "tablet", ct = "smarttv", ci = "wearable", Rr = "embedded", Ir = 500, yn = "Amazon", Ri = "Apple", Bo = "ASUS", qo = "BlackBerry", Kt = "Browser", bn = "Chrome", Ac = "Edge", vn = "Firefox", wn = "Google", Vo = "Huawei", tr = "LG", ir = "Microsoft", $o = "Motorola", Sn = "Opera", En = "Samsung", Fo = "Sharp", Cn = "Sony", nr = "Xiaomi", sr = "Zebra", jo = "Facebook", ka = "Chromium OS", Pa = "Mac OS", Rc = function(r, e) { var t = {}; for (var i in r) e[i] && e[i].length % 2 === 0 ? t[i] = e[i].concat(r[i]) : t[i] = r[i]; return t; }, Ws = function(r) { for (var e = {}, t = 0; t < r.length; t++) e[r[t].toUpperCase()] = r[t]; return e; }, zo = function(r, e) { return typeof r === Gs ? Vi(e).indexOf(Vi(r)) !== -1 : !1; }, Vi = function(r) { return r.toLowerCase(); }, Ic = function(r) { return typeof r === Gs ? r.replace(/[^\d\.]/g, zs).split(".")[0] : void 0; }, Nr = function(r, e) { if (typeof r === Gs) return r = r.replace(/^\s\s*/, zs), typeof e === Ar ? r : r.substring(0, Ir); }, Ii = function(r, e) { for (var t = 0, i, n, s, o, c, a; t < e.length && !c; ) { var d = e[t], u = e[t + 1]; for (i = n = 0; i < d.length && !c && d[i]; ) if (c = d[i++].exec(r), c) for (s = 0; s < u.length; s++) a = c[++n], o = u[s], typeof o === uo && o.length > 0 ? o.length === 2 ? typeof o[1] == xr ? this[o[0]] = o[1].call(this, a) : this[o[0]] = o[1] : o.length === 3 ? typeof o[1] === xr && !(o[1].exec && o[1].test) ? this[o[0]] = a ? o[1].call(this, a, o[2]) : void 0 : this[o[0]] = a ? a.replace(o[1], o[2]) : void 0 : o.length === 4 && (this[o[0]] = a ? o[3].call(this, a.replace(o[1], o[2])) : void 0) : this[o] = a || void 0; t += 2; } }, rr = function(r, e) { for (var t in e) if (typeof e[t] === uo && e[t].length > 0) { for (var i = 0; i < e[t].length; i++) if (zo(e[t][i], r)) return t === Oo ? void 0 : t; } else if (zo(e[t], r)) return t === Oo ? void 0 : t; return r; }, Nc = { "1.0": "/8", "1.2": "/1", "1.3": "/3", "2.0": "/412", "2.0.2": "/416", "2.0.3": "/417", "2.0.4": "/419", "?": "/" }, Go = { ME: "4.90", "NT 3.11": "NT3.51", "NT 4.0": "NT4.0", 2e3: "NT 5.0", XP: ["NT 5.1", "NT 5.2"], Vista: "NT 6.0", 7: "NT 6.1", 8: "NT 6.2", "8.1": "NT 6.3", 10: ["NT 6.4", "NT 10.0"], RT: "ARM" }, Wo = { browser: [ [ /\b(?:crmo|crios)\/([\w\.]+)/i // Chrome for Android/iOS ], [ee, [Q, "Chrome"]], [ /edg(?:e|ios|a)?\/([\w\.]+)/i // Microsoft Edge ], [ee, [Q, "Edge"]], [ // Presto based /(opera mini)\/([-\w\.]+)/i, // Opera Mini /(opera [mobiletab]{3,6})\b.+version\/([-\w\.]+)/i, // Opera Mobi/Tablet /(opera)(?:.+version\/|[\/ ]+)([\w\.]+)/i // Opera ], [Q, ee], [ /opios[\/ ]+([\w\.]+)/i // Opera mini on iphone >= 8.0 ], [ee, [Q, Sn + " Mini"]], [ /\bopr\/([\w\.]+)/i // Opera Webkit ], [ee, [Q, Sn]], [ // Mixed /\bb[ai]*d(?:uhd|[ub]*[aekoprswx]{5,6})[\/ ]?([\w\.]+)/i // Baidu ], [ee, [Q, "Baidu"]], [ /(kindle)\/([\w\.]+)/i, // Kindle /(lunascape|maxthon|netfront|jasmine|blazer)[\/ ]?([\w\.]*)/i, // Lunascape/Maxthon/Netfront/Jasmine/Blazer // Trident based /(avant|iemobile|slim)\s?(?:browser)?[\/ ]?([\w\.]*)/i, // Avant/IEMobile/SlimBrowser /(?:ms|\()(ie) ([\w\.]+)/i, // Internet Explorer // Webkit/KHTML based // Flock/RockMelt/Midori/Epiphany/Silk/Skyfire/Bolt/Iron/Iridium/PhantomJS/Bowser/QupZilla/Falkon /(flock|rockmelt|midori|epiphany|silk|skyfire|bolt|iron|vivaldi|iridium|phantomjs|bowser|quark|qupzilla|falkon|rekonq|puffin|brave|whale(?!.+naver)|qqbrowserlite|qq|duckduckgo)\/([-\w\.]+)/i, // Rekonq/Puffin/Brave/Whale/QQBrowserLite/QQ, aka ShouQ /(heytap|ovi)browser\/([\d\.]+)/i, // Heytap/Ovi /(weibo)__([\d\.]+)/i // Weibo ], [Q, ee], [ /(?:\buc? ?browser|(?:juc.+)ucweb)[\/ ]?([\w\.]+)/i // UCBrowser ], [ee, [Q, "UC" + Kt]], [ /microm.+\bqbcore\/([\w\.]+)/i, // WeChat Desktop for Windows Built-in Browser /\bqbcore\/([\w\.]+).+microm/i, /micromessenger\/([\w\.]+)/i // WeChat ], [ee, [Q, "WeChat"]], [ /konqueror\/([\w\.]+)/i // Konqueror ], [ee, [Q, "Konqueror"]], [ /trident.+rv[: ]([\w\.]{1,9})\b.+like gecko/i // IE11 ], [ee, [Q, "IE"]], [ /ya(?:search)?browser\/([\w\.]+)/i // Yandex ], [ee, [Q, "Yandex"]], [ /slbrowser\/([\w\.]+)/i // Smart Lenovo Browser ], [ee, [Q, "Smart Lenovo " + Kt]], [ /(avast|avg)\/([\w\.]+)/i // Avast/AVG Secure Browser ], [[Q, /(.+)/, "$1 Secure " + Kt], ee], [ /\bfocus\/([\w\.]+)/i // Firefox Focus ], [ee, [Q, vn + " Focus"]], [ /\bopt\/([\w\.]+)/i // Opera Touch ], [ee, [Q, Sn + " Touch"]], [ /coc_coc\w+\/([\w\.]+)/i // Coc Coc Browser ], [ee, [Q, "Coc Coc"]], [ /dolfin\/([\w\.]+)/i // Dolphin ], [ee, [Q, "Dolphin"]], [ /coast\/([\w\.]+)/i // Opera Coast ], [ee, [Q, Sn + " Coast"]], [ /miuibrowser\/([\w\.]+)/i // MIUI Browser ], [ee, [Q, "MIUI " + Kt]], [ /fxios\/([-\w\.]+)/i // Firefox for iOS ], [ee, [Q, vn]], [ /\bqihu|(qi?ho?o?|360)browser/i // 360 ], [[Q, "360 " + Kt]], [/(oculus|sailfish|huawei|vivo)browser\/([\w\.]+)/i], [[Q, /(.+)/, "$1 " + Kt], ee], [ // Oculus/Sailfish/HuaweiBrowser/VivoBrowser /samsungbrowser\/([\w\.]+)/i // Samsung Internet ], [ee, [Q, En + " Internet"]], [ /(comodo_dragon)\/([\w\.]+)/i // Comodo Dragon ], [[Q, /_/g, " "], ee], [ /metasr[\/ ]?([\d\.]+)/i // Sogou Explorer ], [ee, [Q, "Sogou Explorer"]], [ /(sogou)mo\w+\/([\d\.]+)/i // Sogou Mobile ], [[Q, "Sogou Mobile"], ee], [ /(electron)\/([\w\.]+) safari/i, // Electron-based App /(tesla)(?: qtcarbrowser|\/(20\d\d\.[-\w\.]+))/i, // Tesla /m?(qqbrowser|2345Explorer)[\/ ]?([\w\.]+)/i // QQBrowser/2345 Browser ], [Q, ee], [ /(lbbrowser)/i, // LieBao Browser /\[(linkedin)app\]/i // LinkedIn App for iOS & Android ], [Q], [ // WebView /((?:fban\/fbios|fb_iab\/fb4a)(?!.+fbav)|;fbav\/([\w\.]+);)/i // Facebook App for iOS & Android ], [[Q, jo], ee], [ /(Klarna)\/([\w\.]+)/i, // Klarna Shopping Browser for iOS & Android /(kakao(?:talk|story))[\/ ]([\w\.]+)/i, // Kakao App /(naver)\(.*?(\d+\.[\w\.]+).*\)/i, // Naver InApp /safari (line)\/([\w\.]+)/i, // Line App for iOS /\b(line)\/([\w\.]+)\/iab/i, // Line App for Android /(alipay)client\/([\w\.]+)/i, // Alipay /(chromium|instagram|snapchat)[\/ ]([-\w\.]+)/i // Chromium/Instagram/Snapchat ], [Q, ee], [ /\bgsa\/([\w\.]+) .*safari\//i // Google Search Appliance on iOS ], [ee, [Q, "GSA"]], [ /musical_ly(?:.+app_?version\/|_)([\w\.]+)/i // TikTok ], [ee, [Q, "TikTok"]], [ /headlesschrome(?:\/([\w\.]+)| )/i // Chrome Headless ], [ee, [Q, bn + " Headless"]], [ / wv\).+(chrome)\/([\w\.]+)/i // Chrome WebView ], [[Q, bn + " WebView"], ee], [ /droid.+ version\/([\w\.]+)\b.+(?:mobile safari|safari)/i // Android Browser ], [ee, [Q, "Android " + Kt]], [ /(chrome|omniweb|arora|[tizenoka]{5} ?browser)\/v?([\w\.]+)/i // Chrome/OmniWeb/Arora/Tizen/Nokia ], [Q, ee], [ /version\/([\w\.\,]+) .*mobile\/\w+ (safari)/i // Mobile Safari ], [ee, [Q, "Mobile Safari"]], [ /version\/([\w(\.|\,)]+) .*(mobile ?safari|safari)/i // Safari & Safari Mobile ], [ee, Q], [ /webkit.+?(mobile ?safari|safari)(\/[\w\.]+)/i // Safari < 3.0 ], [Q, [ee, rr, Nc]], [/(webkit|khtml)\/([\w\.]+)/i], [Q, ee], [ // Gecko based /(navigator|netscape\d?)\/([-\w\.]+)/i // Netscape ], [[Q, "Netscape"], ee], [ /mobile vr; rv:([\w\.]+)\).+firefox/i // Firefox Reality ], [ee, [Q, vn + " Reality"]], [ /ekiohf.+(flow)\/([\w\.]+)/i, // Flow /(swiftfox)/i, // Swiftfox /(icedragon|iceweasel|camino|chimera|fennec|maemo browser|minimo|conkeror|klar)[\/ ]?([\w\.\+]+)/i, // IceDragon/Iceweasel/Camino/Chimera/Fennec/Maemo/Minimo/Conkeror/Klar /(seamonkey|k-meleon|icecat|iceape|firebird|phoenix|palemoon|basilisk|waterfox)\/([-\w\.]+)$/i, // Firefox/SeaMonkey/K-Meleon/IceCat/IceApe/Firebird/Phoenix /(firefox)\/([\w\.]+)/i, // Other Firefox-based /(mozilla)\/([\w\.]+) .+rv\:.+gecko\/\d+/i, // Mozilla // Other /(polaris|lynx|dillo|icab|doris|amaya|w3m|netsurf|sleipnir|obigo|mosaic|(?:go|ice|up)[\. ]?browser)[-\/ ]?v?([\w\.]+)/i, // Polaris/Lynx/Dillo/iCab/Doris/Amaya/w3m/NetSurf/Sleipnir/Obigo/Mosaic/Go/ICE/UP.Browser /(links) \(([\w\.]+)/i, // Links /panasonic;(viera)/i // Panasonic Viera ], [Q, ee], [ /(cobalt)\/([\w\.]+)/i // Cobalt ], [Q, [ee, /master.|lts./, ""]] ], cpu: [ [ /(?:(amd|x(?:(?:86|64)[-_])?|wow|win)64)[;\)]/i // AMD64 (x64) ], [[ft, "amd64"]], [ /(ia32(?=;))/i // IA32 (quicktime) ], [[ft, Vi]], [ /((?:i[346]|x)86)[;\)]/i // IA32 (x86) ], [[ft, "ia32"]], [ /\b(aarch64|arm(v?8e?l?|_?64))\b/i // ARM64 ], [[ft, "arm64"]], [ /\b(arm(?:v[67])?ht?n?[fl]p?)\b/i // ARMHF ], [[ft, "armhf"]], [ // PocketPC mistakenly identified as PowerPC /windows (ce|mobile); ppc;/i ], [[ft, "arm"]], [ /((?:ppc|powerpc)(?:64)?)(?: mac|;|\))/i // PowerPC ], [[ft, /ower/, zs, Vi]], [ /(sun4\w)[;\)]/i // SPARC ], [[ft, "sparc"]], [ /((?:avr32|ia64(?=;))|68k(?=\))|\barm(?=v(?:[1-7]|[5-7]1)l?|;|eabi)|(?=atmel )avr|(?:irix|mips|sparc)(?:64)?\b|pa-risc)/i // IA64, 68K, ARM/64, AVR/32, IRIX/64, MIPS/64, SPARC/64, PA-RISC ], [[ft, Vi]] ], device: [ [ ////////////////////////// // MOBILES & TABLETS ///////////////////////// // Samsung /\b(sch-i[89]0\d|shw-m380s|sm-[ptx]\w{2,4}|gt-[pn]\d{2,4}|sgh-t8[56]9|nexus 10)/i ], [z, [G, En], [$, Te]], [ /\b((?:s[cgp]h|gt|sm)-\w+|sc[g-]?[\d]+a?|galaxy nexus)/i, /samsung[- ]([-\w]+)/i, /sec-(sgh\w+)/i ], [z, [G, En], [$, Ce]], [ // Apple /(?:\/|\()(ip(?:hone|od)[\w, ]*)(?:\/|;)/i // iPod/iPhone ], [z, [G, Ri], [$, Ce]], [ /\((ipad);[-\w\),; ]+apple/i, // iPad /applecoremedia\/[\w\.]+ \((ipad)/i, /\b(ipad)\d\d?,\d\d?[;\]].+ios/i ], [z, [G, Ri], [$, Te]], [/(macintosh);/i], [z, [G, Ri]], [ // Sharp /\b(sh-?[altvz]?\d\d[a-ekm]?)/i ], [z, [G, Fo], [$, Ce]], [ // Huawei /\b((?:ag[rs][23]?|bah2?|sht?|btv)-a?[lw]\d{2})\b(?!.+d\/s)/i ], [z, [G, Vo], [$, Te]], [ /(?:huawei|honor)([-\w ]+)[;\)]/i, /\b(nexus 6p|\w{2,4}e?-[atu]?[ln][\dx][012359c][adn]?)\b(?!.+d\/s)/i ], [z, [G, Vo], [$, Ce]], [ // Xiaomi /\b(poco[\w ]+|m2\d{3}j\d\d[a-z]{2})(?: bui|\))/i, // Xiaomi POCO /\b; (\w+) build\/hm\1/i, // Xiaomi Hongmi 'numeric' models /\b(hm[-_ ]?note?[_ ]?(?:\d\w)?) bui/i, // Xiaomi Hongmi /\b(redmi[\-_ ]?(?:note|k)?[\w_ ]+)(?: bui|\))/i, // Xiaomi Redmi /oid[^\)]+; (m?[12][0-389][01]\w{3,6}[c-y])( bui|; wv|\))/i, // Xiaomi Redmi 'numeric' models /\b(mi[-_ ]?(?:a\d|one|one[_ ]plus|note lte|max|cc)?[_ ]?(?:\d?\w?)[_ ]?(?:plus|se|lite)?)(?: bui|\))/i // Xiaomi Mi ], [ [z, /_/g, " "], [G, nr], [$, Ce] ], [ /oid[^\)]+; (2\d{4}(283|rpbf)[cgl])( bui|\))/i, // Redmi Pad /\b(mi[-_ ]?(?:pad)(?:[\w_ ]+))(?: bui|\))/i // Mi Pad tablets ], [ [z, /_/g, " "], [G, nr], [$, Te] ], [ // OPPO /; (\w+) bui.+ oppo/i, /\b(cph[12]\d{3}|p(?:af|c[al]|d\w|e[ar])[mt]\d0|x9007|a101op)\b/i ], [z, [G, "OPPO"], [$, Ce]], [ // Vivo /vivo (\w+)(?: bui|\))/i, /\b(v[12]\d{3}\w?[at])(?: bui|;)/i ], [z, [G, "Vivo"], [$, Ce]], [ // Realme /\b(rmx[1-3]\d{3})(?: bui|;|\))/i ], [z, [G, "Realme"], [$, Ce]], [ // Motorola /\b(milestone|droid(?:[2-4x]| (?:bionic