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