@liveryvideo/player
Version:
Livery video player for use in web browsers.
1,610 lines (1,608 loc) • 1.04 MB
JavaScript
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