UNPKG

@liveryvideo/player

Version:

Livery video player for use in web browsers.

1,570 lines (1,568 loc) 1.05 MB
import { css as oi, LitElement as ai, html as dt, nothing as Mc } from "lit"; import { property as pe, query as zt, customElement as Qr, state as st, eventOptions as Uc } from "lit/decorators.js"; import { classMap as Ke } from "lit/directives/class-map.js"; import { ifDefined as cs } from "lit/directives/if-defined.js"; import { repeat as Oc } from "lit/directives/repeat.js"; import "reflect-metadata"; import ja from "lodash-es/debounce.js"; import "sprintf-js"; import Qo from "lodash-es/throttle.js"; const yr = class yr extends Event { constructor(e, t) { super(yr.type, t), this.config = e; } }; yr.type = "livery-config-change"; let Es = yr; const br = class br extends Event { constructor(e, t) { super(br.type, t), this.display = e; } }; br.type = "livery-display-change"; let Cs = br; const vr = class vr extends Event { constructor(e, t) { super(vr.type, t), this.error = e; } }; vr.type = "livery-error"; let Qi = vr; const wr = class wr extends Event { constructor(e, t) { super(wr.type, t), this.features = e; } }; wr.type = "livery-features-change"; let Ts = wr; const Sr = class Sr extends Event { constructor(e, t) { super(Sr.type, t), this.fullscreen = e; } }; Sr.type = "livery-fullscreen-change"; let As = Sr; const Er = class Er extends Event { constructor(e, t) { super(Er.type, t), this.mode = e; } }; Er.type = "livery-mode-change"; let xs = Er; const Cr = class Cr extends Event { constructor(e, t) { super(Cr.type, t), this.phase = e; } }; Cr.type = "livery-phase-change"; let Is = Cr; const Tr = class Tr extends Event { constructor(e, t) { super(Tr.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); } }; Tr.type = "livery-playback-change"; let Rs = Tr; const Ar = class Ar extends Event { constructor(e, t) { super(Ar.type, t), this.qualities = e; } }; Ar.type = "livery-qualities-change"; let Ns = Ar; const xr = class xr extends Event { constructor(e, t) { super(xr.type, t), this.quality = e; } }; xr.type = "livery-quality-change"; let _s = xr; const Ir = class Ir extends Event { constructor(e) { super(Ir.type, e); } }; Ir.type = "livery-recovered"; let ks = Ir; const Rr = class Rr extends Event { constructor(e, t, i) { super(Rr.type, i), this.volume = e, this.muted = t; } }; Rr.type = "livery-volume-change"; let qn = Rr; function $n(s) { const e = typeof s == "number" ? `Timed out after ${s}ms` : s; return new DOMException(e, "TimeoutError"); } let Ps = !0; try { const s = new EventTarget(), e = new AbortController(); e.abort(), s.addEventListener( "test", () => { Ps = !1; }, { signal: e.signal } ), s.dispatchEvent(new Event("test")); } catch { Ps = !1; } let za = !1; const us = new Error("test"); try { AbortSignal.abort(us).throwIfAborted(); } catch (s) { za = s === us && us.message === "test"; } class Re 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(Re.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 = Re.createAbortError()) { if (AbortSignal.abort) return AbortSignal.abort(e); const t = new Re(); 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 (!Re.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 ? Re.createAbortError() : Re.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 Re(); 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 = Re.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((r) => [ new Promise((n, o) => { t = e(n, o, r), t && (i = this.onAbort(t)); }) ]).finally(() => { i == null || i(); }); } /** * Returns a new Abortable that's a child of this one. * * I.e: that can be aborted separately, but will also be aborted when this parent is. */ child() { return new Re(this.signal); } /** * 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 * * @deprecated Instead use `target.addEventListener` with `signal` option; that is sufficiently supported now * @example * // Add a window click listener until abortable is aborted * abortable.eventListener(window, 'click', listener); */ eventListener(e, t, i, r = {}) { if (this.aborted) return () => { }; const n = { ...r, signal: this.signal }; e.addEventListener( t, i, n ); const o = () => e.removeEventListener( t, i, n ); if (Ps) 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 r = window.setInterval(e, t, ...i), n = () => window.clearInterval(r), o = this.onAbort(n); return () => { n(), 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 Re(this.signal), i = e(t.signal), r = this.promise; return Promise.race([r, ...i]).finally(() => { t.abort(Re.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 AbortSignal.throwIfAborted} or a shim when that's not supported * (e.g: Safari v15.3 and older) or not properly supported (e.g: Chrome). * * @throws Abort reason when this is aborted * * @example * ```typescript * // Before and after unabortable method calls * abortable.throwIfAborted(); * await doAsyncUnAbortableThing1(); * abortable.throwIfAborted(); * ``` */ throwIfAborted() { if (za) { 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 r = window.setTimeout(() => { o(), e(...i); }, t), n = () => window.clearTimeout(r), o = this.onAbort(n); return () => { n(), o(); }; } } const Ds = 60, Ls = 60 * Ds, hi = 24 * Ls, qc = 7 * hi, $c = 365 * hi, Bc = ["", "k", "M", "G", "T", "P", "E", "Z", "Y"], Fc = ["", "m", "μ", "n", "p", "f", "a", "z", "y"]; function Yt(s) { return Yr(s, "b/s"); } function Pn(s) { const e = s / 1e3; return e >= $c ? Jt(e / hi, "y") : e >= qc ? Jt(e / hi, "w") : e >= hi ? Jt(e / hi, "d") : e >= Ls ? Jt(e / Ls, "h") : e >= Ds ? Jt(e / Ds, "m") : Yr(e, "s"); } function Yr(s, e = "") { if (Number.isNaN(s)) return `NaN${e}`; const t = s < 0, i = t ? "-" : ""; if (!Number.isFinite(s)) return `${i}∞${e}`; const r = t ? -s : s, n = r === 0 ? 0 : Math.log10(r), o = Math.floor(n), c = Math.floor(o / 3), a = 10 ** (c * 3), d = r / a, u = Jt(d), l = c < 0 ? Fc[-c] : Bc[c]; return `${i}${u}${l != null ? l : "⁉️"}${e}`; } function mt(s, e = [], t = !1) { if (e.includes(s)) return "[circular reference]"; if (Array.isArray(s)) { const i = e.slice(0); return i.push(s), `[${s.map((n) => mt(n, i, !0)).join(", ")}]`; } if (s instanceof Error) return `${s.name}: ${s.message}`; if (typeof s == "object" && s !== null) { const i = e.concat(s); return `{ ${Object.entries(s).map(([n, o]) => `${n}: ${mt(o, i, !0)}`).join(", ")} }`; } return typeof s == "function" || typeof s == "symbol" ? `[${typeof s}]` : typeof s == "string" ? t ? `'${s}'` : s : typeof s == "number" ? Yr(s) : String(s); } function Vc(s, e = 1) { const t = e === 1 ? s : s / e; return Jt(100 * t, "%"); } function Jt(s, e) { const t = s < 10 ? s.toFixed(2) : s < 1e3 ? s.toPrecision(3) : Math.round(s), i = Number(t); return e === void 0 ? String(i) : `${i}${e}`; } function jc(s = /* @__PURE__ */ new Date(), e = 3) { const t = s instanceof Date ? s : new Date(s), i = t.toTimeString().substring(0, 8), r = (t.getMilliseconds() / 1e3).toFixed(e).substring(1); return `${i}${r}`; } function Oe(s) { return s instanceof Error ? s : new Error(String(s)); } const Ms = { 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 } }, zc = Object.keys(Ms), it = class it { /** * Logger constructor. * * @param name - Name of Logger, used as prefix */ constructor(e) { const t = it.nameCountMap.get(e) || 0; this.name = e, this.nr = t + 1, it.nameCountMap.set(e, this.nr); } /** * Add a LogListener function to invoke alongside the default console logging. */ static addLogListener(e) { if (it.logListeners.has(e)) throw new Error("listener has already been added"); return it.logListeners.add(e), () => { it.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: r, timestamp: n } = e, o = r === 1 ? "" : `#${r}`; return `${n} ${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 r = i.split(` `); return r[0] === e.toString() && r.shift(), e.message === "" && r.shift(), r[r.length - 1] === "" && r.pop(), r.length > t && (r.length = t), r.map((n) => n.trim()).join(` `); } /** * Returns a log string for a LogListener call using {@link createPrefix} and {@link fmtObject}. * 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 = [it.createPrefix(e)], { details: i, error: r, message: n } = e; if (n && t.push(n), i && t.push(mt(i)), r) { const o = it.createStack(r, 3).split(` `).map((c) => ` ${c}`).join(` `); t.push(`${r.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 zc.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 r = new Error(t); throw this.log("ERROR", { message: t, details: i, error: r }), r; } } /** * 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("Stack trace")) { 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(Oe(e)); } /** * Returns a function that, when called, converts the argument to an Error and logs that as a warning. */ handleWarn() { return (e) => this.warn(Oe(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: r }) { if (e === "QUIET") throw new Error("Can not log at level QUIET"); const { options: n } = it, o = Ms[e], c = n.level ? o.weight >= Ms[n.level].weight : !1, a = { details: t, emoji: o.emoji, enabled: c, error: i, level: e, message: r, name: this.name, nr: this.nr, timestamp: jc() }; for (const d of it.logListeners) d(a); if (c && n.console) { const d = [ it.createPrefix(a), r, 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("Stack trace")) { if (e instanceof Error) { this.log("WARN", { error: e }); return; } this.log("WARN", { message: e, details: t, error: i }); } }; it.options = { /** * If true then log to console. */ console: !0, /** * Global log level. */ level: "INFO" }, it.logListeners = /* @__PURE__ */ new Set(), it.nameCountMap = /* @__PURE__ */ new Map(); let Ee = it; var Hc = Object.defineProperty, Gc = Object.getOwnPropertyDescriptor, Xe = (s, e, t, i) => { for (var r = i > 1 ? void 0 : i ? Gc(e, t) : e, n = s.length - 1, o; n >= 0; n--) (o = s[n]) && (r = (i ? o(e, t, r) : o(r)) || r); return i && r && Hc(e, t, r), r; }; let He = class extends ai { constructor() { super(...arguments), this.abrBandwidthColor = null, this.abrBufferColor = null, 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 Ee("livery-buffer-graph"); } get eventInit() { return { bubbles: this.bubbles, composed: this.bubbles }; } /** @internal */ connectedCallback() { super.connectedCallback(), this.onStateChange(); } /** @internal */ disconnectedCallback() { super.disconnectedCallback(), this.onStateChange(); } /** @internal */ render() { return dt`<div id="container"></div>`; } /** @internal */ updated() { this.onStateChange(!0); } addRow() { const { chart: s, marks: e, rows: t } = this.active; t.push(this.createRow()); const i = this.maxRows * this.updateInterval, r = Date.now(); let n = t.findIndex(({ date: c }) => r - c < i); n > 2 && t.splice(0, n - 2), n = e.findIndex(({ date: c }) => r - c < i), n >= 0 && e.splice(0, n); const o = (c, a) => t.map((d) => ({ name: String(d.date), value: [d.date, a ? a(d[c]) : d[c]] })); s.setOption( { series: [ { data: o("audioBuffer") }, { data: o("videoBuffer"), markPoint: { data: e.map(({ date: c, quality: a, targetLatency: d, up: u }) => ({ coord: [c, d], symbolRotate: u ? 0 : 180, value: a })), symbol: "triangle", symbolSize: 18 } }, { data: o("abrBuffer") }, { data: o("latency") }, // Cap values to not drop below 0 to deal with value resets on engine reload/pause { data: o("decodedFrames", (c) => Math.max(0, c)) }, { data: o("droppedFrames", (c) => Math.max(0, c)) }, { data: o("abrBandwidth") } ], xAxis: { min: r - i } }, !1 ); } createChart() { var i, r, n, o, c, a, d, u, l, h, p, f, v, b, w, E; const s = this.container, e = window.echarts.init(s), t = new ResizeObserver(() => e.resize()); return t.observe(s), 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: (r = this.textColor) != null ? r : "#eee" }, width: "75%" }, series: [ { itemStyle: { color: (o = (n = this.audioColor) != null ? n : 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: (u = (d = this.abrBufferColor) != null ? d : this.bufferColor) != null ? u : "#bbddff" }, name: "ABR B̅uf", symbol: "none", type: "line" }, { itemStyle: { color: (l = this.latencyColor) != null ? l : "#ffa500" }, name: "Latency", symbol: "none", type: "line" }, { itemStyle: { color: (h = this.decodedColor) != null ? h : "#f6c2f3" }, lineStyle: { opacity: 0.5 }, name: "Decoded", showSymbol: !1, symbol: "triangle", type: "line", yAxisIndex: 1 }, { itemStyle: { color: (p = this.droppedColor) != null ? p : "#fc1c1c" }, lineStyle: { opacity: 0.5 }, name: "Dropped", showSymbol: !1, symbol: "triangle", type: "line", yAxisIndex: 1 }, { itemStyle: { color: (f = this.abrBandwidthColor) != null ? f : "#ffcccc" }, name: "ABR B̅w", showSymbol: !1, symbol: "square", type: "line", yAxisIndex: 2 } ], xAxis: { axisLabel: { rotate: 30 }, axisLine: { lineStyle: { color: (v = this.textColor) != null ? v : "#eee" } }, type: "time" }, yAxis: [ { axisLine: { lineStyle: { color: (b = this.textColor) != null ? b : "#eee" } }, max: ({ max: S }) => Math.ceil((S + 0.01) * 10) / 10, min: ({ min: S }) => Math.trunc((S - 0.01) * 10) / 10, minInterval: 0.1, name: "● Seconds", triggerEvent: !0, type: "value" }, { axisLine: { lineStyle: { color: (w = this.textColor) != null ? w : "#eee" } }, name: "▲ FPS", splitLine: { show: !1 }, triggerEvent: !0, type: "value" }, { axisLine: { lineStyle: { color: (E = this.textColor) != null ? E : "#eee" }, onZero: !1 }, name: "■ MBPS", offset: 50, position: "right", splitLine: { show: !1 }, triggerEvent: !0, type: "value" } ] }), e.on( "click", { componentType: "yAxis", targetType: "axisName" }, (S) => { if (S.name === "● Seconds") for (const x of ["Audio", "Video", "ABR B̅uf", "Latency"]) e.dispatchAction({ name: x, type: "legendToggleSelect" }); if (S.name === "▲ FPS") for (const x of ["Decoded", "Dropped"]) e.dispatchAction({ name: x, type: "legendToggleSelect" }); S.name === "■ MBPS" && e.dispatchAction({ name: "ABR B̅w", type: "legendToggleSelect" }); } ), { chart: e, resizeObserver: t }; } createRow() { const s = this.active, { lastDecoded: e, lastDropped: t, lastTime: i, player: { engine: r } } = s, n = Date.now(); if (!r) return { abrBandwidth: Number.NaN, abrBuffer: Number.NaN, audioBuffer: Number.NaN, date: n, decodedFrames: Number.NaN, droppedFrames: Number.NaN, latency: Number.NaN, videoBuffer: Number.NaN }; const { abrBandwidth: o, abrBuffer: c, audioBuffer: a, buffer: d, decodedFrames: u, droppedFrames: l, latency: h, playing: p, videoBuffer: f } = r, v = (n - i) / 1e3; return s.lastTime = n, s.lastDecoded = u, s.lastDropped = l, { abrBandwidth: o / 1e6, abrBuffer: c, audioBuffer: Number.isNaN(a) ? d : a, date: n, decodedFrames: (u - e) / v || Number.NaN, droppedFrames: (l - t) / v || Number.NaN, latency: p ? h : Number.NaN, videoBuffer: Number.isNaN(f) ? d : f }; } handleEngine(s) { 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, !s) { this.addRow(); return; } s.onProperty("paused", (i) => { window.clearInterval(e.intervalId), e.intervalId = Number.NaN, this.addRow(), !i && (e.intervalId = window.setInterval( () => this.addRow(), this.updateInterval )); }); let t = -1; s.onProperty("activeQuality", (i) => { this.active && i >= 0 && this.active.marks.push({ date: Date.now(), quality: i, targetLatency: s.targetLatency, up: i > t }), t = i; }); } loadScript() { if (!!document.getElementById(He.scriptId)) return; this.log.debug("loadScript"); const e = document.createElement("script"); e.id = He.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.6.0/dist/echarts.min.js", document.head.appendChild(e); } onError(s) { const e = Oe(s); this.log.error(e), this.dispatchEvent(new Qi(e, this.eventInit)); } onStateChange(s = !1) { if (!window.echarts) { this.loadScript(); return; } this.isConnected ? (s && this.stop(), this.start()) : this.stop(); } start() { var r; if (this.active) { this.log.debug("start(): already active"); return; } const s = (r = this.player) != null ? r : document.querySelector("livery-player"); if (!s) { this.log.debug("start(): wait for player reference to be set"); return; } this.log.debug("start"); const e = new Re(), { chart: t, resizeObserver: i } = this.createChart(); this.active = { abortable: e, chart: t, intervalId: Number.NaN, lastDecoded: Number.NaN, lastDropped: Number.NaN, lastTime: Number.NaN, marks: [], player: s, resizeObserver: i, rows: [] }, this.handleEngine(s.engine), e.eventListener( s, "livery-engine-change", ({ engine: n }) => this.handleEngine(n) ); } stop() { if (!this.active) return; this.log.debug("stop"); const { abortable: s, chart: e, intervalId: t, resizeObserver: i } = this.active; s.abort(), window.clearInterval(t), e.dispose(), i.disconnect(), this.active = void 0; } }; He.styles = oi` :host { display: block; } :host([hidden]) { display: none; } #container { height: 300px; } `; He.scriptId = "livery-buffer-graph-loader"; Xe([ pe({ reflect: !0, type: String }) ], He.prototype, "abrBandwidthColor", 2); Xe([ pe({ reflect: !0, type: String }) ], He.prototype, "abrBufferColor", 2); Xe([ pe({ reflect: !0, type: String }) ], He.prototype, "audioColor", 2); Xe([ pe({ reflect: !0, type: String }) ], He.prototype, "backgroundColor", 2); Xe([ pe({ reflect: !0, type: Boolean }) ], He.prototype, "bubbles", 2); Xe([ pe({ reflect: !0, type: String }) ], He.prototype, "bufferColor", 2); Xe([ pe({ reflect: !0, type: String }) ], He.prototype, "decodedColor", 2); Xe([ pe({ reflect: !0, type: String }) ], He.prototype, "droppedColor", 2); Xe([ pe({ reflect: !0, type: String }) ], He.prototype, "latencyColor", 2); Xe([ pe({ reflect: !0, type: Number }) ], He.prototype, "maxRows", 2); Xe([ pe({ type: Object }) ], He.prototype, "player", 2); Xe([ pe({ reflect: !0, type: String }) ], He.prototype, "textColor", 2); Xe([ pe({ reflect: !0, type: Number }) ], He.prototype, "updateInterval", 2); Xe([ pe({ reflect: !0, type: String }) ], He.prototype, "videoColor", 2); Xe([ zt("#container") ], He.prototype, "container", 2); He = Xe([ Qr("livery-buffer-graph") ], He); var Wc = "1.0.37", Jr = "", Yo = "?", Us = "function", Os = "undefined", To = "object", Xr = "string", Ha = "major", z = "model", Q = "name", F = "type", H = "vendor", ee = "version", ft = "architecture", Ui = "console", Te = "mobile", Ae = "tablet", ct = "smarttv", li = "wearable", qs = "embedded", $s = 500, Sn = "Amazon", _i = "Apple", Jo = "ASUS", Xo = "BlackBerry", Zt = "Browser", En = "Chrome", Kc = "Edge", Cn = "Firefox", Tn = "Google", ea = "Huawei", ls = "LG", ds = "Microsoft", ta = "Motorola", An = "Opera", xn = "Samsung", ia = "Sharp", In = "Sony", hs = "Xiaomi", ps = "Zebra", na = "Facebook", Ga = "Chromium OS", Wa = "Mac OS", Zc = function(s, e) { var t = {}; for (var i in s) e[i] && e[i].length % 2 === 0 ? t[i] = e[i].concat(s[i]) : t[i] = s[i]; return t; }, es = function(s) { for (var e = {}, t = 0; t < s.length; t++) e[s[t].toUpperCase()] = s[t]; return e; }, ra = function(s, e) { return typeof s === Xr ? Fi(e).indexOf(Fi(s)) !== -1 : !1; }, Fi = function(s) { return s.toLowerCase(); }, Qc = function(s) { return typeof s === Xr ? s.replace(/[^\d\.]/g, Jr).split(".")[0] : void 0; }, Bs = function(s, e) { if (typeof s === Xr) return s = s.replace(/^\s\s*/, Jr), typeof e === Os ? s : s.substring(0, $s); }, ki = function(s, e) { for (var t = 0, i, r, n, o, c, a; t < e.length && !c; ) { var d = e[t], u = e[t + 1]; for (i = r = 0; i < d.length && !c && d[i]; ) if (c = d[i++].exec(s), c) for (n = 0; n < u.length; n++) a = c[++r], o = u[n], typeof o === To && o.length > 0 ? o.length === 2 ? typeof o[1] == Us ? this[o[0]] = o[1].call(this, a) : this[o[0]] = o[1] : o.length === 3 ? typeof o[1] === Us && !(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; } }, fs = function(s, e) { for (var t in e) if (typeof e[t] === To && e[t].length > 0) { for (var i = 0; i < e[t].length; i++) if (ra(e[t][i], s)) return t === Yo ? void 0 : t; } else if (ra(e[t], s)) return t === Yo ? void 0 : t; return s; }, Yc = { "1.0": "/8", "1.2": "/1", "1.3": "/3", "2.0": "/412", "2.0.2": "/416", "2.0.3": "/417", "2.0.4": "/419", "?": "/" }, sa = { 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" }, oa = { 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, An + " Mini"]], [ /\bopr\/([\w\.]+)/i // Opera Webkit ], [ee, [Q, An]], [ // 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" + Zt]], [ /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 " + Zt]], [ /(avast|avg)\/([\w\.]+)/i // Avast/AVG Secure Browser ], [[Q, /(.+)/, "$1 Secure " + Zt], ee], [ /\bfocus\/([\w\.]+)/i // Firefox Focus ], [ee, [Q, Cn + " Focus"]], [ /\bopt\/([\w\.]+)/i // Opera Touch ], [ee, [Q, An + " 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, An + " Coast"]], [ /miuibrowser\/([\w\.]+)/i // MIUI Browser ], [ee, [Q, "MIUI " + Zt]], [ /fxios\/([-\w\.]+)/i // Firefox for iOS ], [ee, [Q, Cn]], [ /\bqihu|(qi?ho?o?|360)browser/i // 360 ], [[Q, "360 " + Zt]], [/(oculus|sailfish|huawei|vivo)browser\/([\w\.]+)/i], [[Q, /(.+)/, "$1 " + Zt], ee], [ // Oculus/Sailfish/HuaweiBrowser/VivoBrowser /samsungbrowser\/([\w\.]+)/i // Samsung Internet ], [ee, [Q, xn + " 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, na], 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, En + " Headless"]], [ / wv\).+(chrome)\/([\w\.]+)/i // Chrome WebView ], [[Q, En + " WebView"], ee], [ /droid.+ version\/([\w\.]+)\b.+(?:mobile safari|safari)/i // Android Browser ], [ee, [Q, "Android " + Zt]], [ /(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, fs, Yc]], [/(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, Cn + " 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