nixamp 0.7.18 → 0.7.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server.d.ts +2 -0
- package/dist/server.js +50 -4
- package/dist/share.d.ts +13 -2
- package/dist/share.js +14 -3
- package/package.json +1 -1
- package/src/server.ts +53 -4
- package/src/share.ts +15 -4
- package/web/dist/assets/{hls-3VKVEQE3-IlqvYfvF.js → hls-3VKVEQE3-DadaxhMr.js} +1 -1
- package/web/dist/assets/index-DH3yIa7s.js +1 -0
- package/web/dist/assets/{mpegts-BvAPLY0u.js → mpegts-D0wKrt1r.js} +1 -1
- package/web/dist/assets/{mpegts-LO6RVLD6-CiS20_Z-.js → mpegts-LO6RVLD6-Cgabq96z.js} +1 -1
- package/web/dist/index.html +10 -1
- package/web/dist/sw.js +5 -5
- package/web/dist/assets/index-BIeMVcOg.js +0 -1
package/dist/server.d.ts
CHANGED
|
@@ -359,6 +359,8 @@ export interface HandlerOptions {
|
|
|
359
359
|
destinations: Destination[];
|
|
360
360
|
settings: EncoderSettings;
|
|
361
361
|
};
|
|
362
|
+
/** What this server calls itself, for the list of what is live on it. */
|
|
363
|
+
serverName?: string;
|
|
362
364
|
/**
|
|
363
365
|
* Going live: whether this server is listed, and how to change that.
|
|
364
366
|
*
|
package/dist/server.js
CHANGED
|
@@ -775,10 +775,17 @@ export function createHandler(engine, options) {
|
|
|
775
775
|
// cookie, so every later fetch, EventSource and <audio src> carries it
|
|
776
776
|
// without the page knowing anything about keys. Either key works here, and
|
|
777
777
|
// which one was used decides what the browser can then do.
|
|
778
|
-
const
|
|
779
|
-
if (
|
|
780
|
-
const offered =
|
|
781
|
-
|
|
778
|
+
const inPath = key !== null ? keyInPath(path) : null;
|
|
779
|
+
if (inPath !== null) {
|
|
780
|
+
const offered = inPath.key;
|
|
781
|
+
// The path has to be telling the truth about the key it carries. A `/v/`
|
|
782
|
+
// link holding the control key would read as view-only to whoever you
|
|
783
|
+
// sent it to and hand them the controls, which is the whole reason for
|
|
784
|
+
// naming the two shapes in the first place.
|
|
785
|
+
//
|
|
786
|
+
// A mismatch answers exactly as a wrong key does, so nothing is learned
|
|
787
|
+
// from the difference between the two.
|
|
788
|
+
if (scopeOf(offered, key, listenKey) !== inPath.wants) {
|
|
782
789
|
json(response, 404, { error: "not found" });
|
|
783
790
|
return;
|
|
784
791
|
}
|
|
@@ -1618,6 +1625,44 @@ export function createHandler(engine, options) {
|
|
|
1618
1625
|
// is a worse answer than a 401 to someone who has neither.
|
|
1619
1626
|
if (options.paywall && (await options.paywall(request, response, path)))
|
|
1620
1627
|
return;
|
|
1628
|
+
/**
|
|
1629
|
+
* What is live on this server, for somebody deciding what to watch.
|
|
1630
|
+
*
|
|
1631
|
+
* Two different things are, and they were only ever visible in two
|
|
1632
|
+
* different places: the server's own stream -- its playlist, which is what
|
|
1633
|
+
* it is listed in the directory as -- and any channels being published
|
|
1634
|
+
* into it from OBS or a phone. A person connected to a server had no way
|
|
1635
|
+
* to see either as a list, so "what is on here?" had no answer.
|
|
1636
|
+
*
|
|
1637
|
+
* Readable by anyone holding either link, because this is the viewing
|
|
1638
|
+
* side: it names streams and how to reach them, and says nothing about
|
|
1639
|
+
* how to change them.
|
|
1640
|
+
*/
|
|
1641
|
+
if (path === "/api/streams") {
|
|
1642
|
+
const now = engine.snapshot(false);
|
|
1643
|
+
const state = options.live?.status();
|
|
1644
|
+
json(response, 200, {
|
|
1645
|
+
server: {
|
|
1646
|
+
name: options.serverName ?? "this server",
|
|
1647
|
+
nowPlaying: engine.snapshot().tracks?.[now.index]?.title ?? "",
|
|
1648
|
+
tracks: now.trackCount,
|
|
1649
|
+
playing: now.playing,
|
|
1650
|
+
// Only when it has been published: a code is a thing you dial, and
|
|
1651
|
+
// one nobody can dial is not worth showing.
|
|
1652
|
+
live: state?.live === true,
|
|
1653
|
+
code: state?.code ?? "",
|
|
1654
|
+
url: state?.url ?? "",
|
|
1655
|
+
},
|
|
1656
|
+
channels: (options.channels?.list() ?? []).map((one) => ({
|
|
1657
|
+
id: one.id,
|
|
1658
|
+
name: one.name,
|
|
1659
|
+
via: one.via,
|
|
1660
|
+
listeners: one.listeners,
|
|
1661
|
+
startedAt: one.startedAt,
|
|
1662
|
+
})),
|
|
1663
|
+
});
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1621
1666
|
// --- several streams at once ------------------------------------------
|
|
1622
1667
|
//
|
|
1623
1668
|
// A channel is one publisher and everybody listening to them. Two or three
|
|
@@ -2640,6 +2685,7 @@ export async function serve(argv, version = "0.1.0") {
|
|
|
2640
2685
|
owner,
|
|
2641
2686
|
channels,
|
|
2642
2687
|
publishUrls: () => publishUrls,
|
|
2688
|
+
serverName: options.name || hostname(),
|
|
2643
2689
|
live: {
|
|
2644
2690
|
status: () => ({
|
|
2645
2691
|
live: publisher !== null,
|
package/dist/share.d.ts
CHANGED
|
@@ -94,8 +94,19 @@ export declare function reachableAddresses(host: string, port: number, publicUrl
|
|
|
94
94
|
* A link should say what it is before anybody clicks it.
|
|
95
95
|
*/
|
|
96
96
|
export declare const KEY_PATHS: readonly ["/a/", "/v/"];
|
|
97
|
-
/**
|
|
98
|
-
|
|
97
|
+
/**
|
|
98
|
+
* The key in a share link, and what the link claimed to be.
|
|
99
|
+
*
|
|
100
|
+
* Both halves, because a label nobody checks is a label that can lie. `/a/`
|
|
101
|
+
* means this administers and `/v/` means this only views; handed the other
|
|
102
|
+
* key, a path would otherwise have said one thing and done the other -- and
|
|
103
|
+
* the dangerous direction is real: a `/v/` link built around the control key
|
|
104
|
+
* reads as view-only to the person you send it to and hands them the controls.
|
|
105
|
+
*/
|
|
106
|
+
export declare function keyInPath(path: string): {
|
|
107
|
+
key: string;
|
|
108
|
+
wants: Scope;
|
|
109
|
+
} | null;
|
|
99
110
|
/** The full link, key and all. `admin` picks the shape that says which it is. */
|
|
100
111
|
export declare function shareLink(base: string, key: string | null, admin?: boolean): string;
|
|
101
112
|
/**
|
package/dist/share.js
CHANGED
|
@@ -251,7 +251,15 @@ export function reachableAddresses(host, port, publicUrl = "", scheme = "http")
|
|
|
251
251
|
* A link should say what it is before anybody clicks it.
|
|
252
252
|
*/
|
|
253
253
|
export const KEY_PATHS = ["/a/", "/v/"];
|
|
254
|
-
/**
|
|
254
|
+
/**
|
|
255
|
+
* The key in a share link, and what the link claimed to be.
|
|
256
|
+
*
|
|
257
|
+
* Both halves, because a label nobody checks is a label that can lie. `/a/`
|
|
258
|
+
* means this administers and `/v/` means this only views; handed the other
|
|
259
|
+
* key, a path would otherwise have said one thing and done the other -- and
|
|
260
|
+
* the dangerous direction is real: a `/v/` link built around the control key
|
|
261
|
+
* reads as view-only to the person you send it to and hands them the controls.
|
|
262
|
+
*/
|
|
255
263
|
export function keyInPath(path) {
|
|
256
264
|
for (const prefix of KEY_PATHS) {
|
|
257
265
|
if (!path.startsWith(prefix))
|
|
@@ -259,12 +267,15 @@ export function keyInPath(path) {
|
|
|
259
267
|
const rest = path.slice(prefix.length).replace(/\/+$/, "");
|
|
260
268
|
if (rest === "" || rest.includes("/"))
|
|
261
269
|
return null;
|
|
270
|
+
let key = rest;
|
|
262
271
|
try {
|
|
263
|
-
|
|
272
|
+
key = decodeURIComponent(rest);
|
|
264
273
|
}
|
|
265
274
|
catch {
|
|
266
|
-
|
|
275
|
+
// A key that is not valid percent-encoding is taken as written; it will
|
|
276
|
+
// fail to match anything, which is the right answer either way.
|
|
267
277
|
}
|
|
278
|
+
return { key, wants: prefix === "/a/" ? "control" : "listen" };
|
|
268
279
|
}
|
|
269
280
|
return null;
|
|
270
281
|
}
|
package/package.json
CHANGED
package/src/server.ts
CHANGED
|
@@ -966,6 +966,8 @@ export interface HandlerOptions {
|
|
|
966
966
|
broadcaster?: Broadcaster;
|
|
967
967
|
/** Where a broadcast should send, and what it should look like. */
|
|
968
968
|
broadcast?: () => { destinations: Destination[]; settings: EncoderSettings };
|
|
969
|
+
/** What this server calls itself, for the list of what is live on it. */
|
|
970
|
+
serverName?: string;
|
|
969
971
|
/**
|
|
970
972
|
* Going live: whether this server is listed, and how to change that.
|
|
971
973
|
*
|
|
@@ -1074,10 +1076,17 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
|
|
|
1074
1076
|
// cookie, so every later fetch, EventSource and <audio src> carries it
|
|
1075
1077
|
// without the page knowing anything about keys. Either key works here, and
|
|
1076
1078
|
// which one was used decides what the browser can then do.
|
|
1077
|
-
const
|
|
1078
|
-
if (
|
|
1079
|
-
const offered =
|
|
1080
|
-
|
|
1079
|
+
const inPath = key !== null ? keyInPath(path) : null;
|
|
1080
|
+
if (inPath !== null) {
|
|
1081
|
+
const offered = inPath.key;
|
|
1082
|
+
// The path has to be telling the truth about the key it carries. A `/v/`
|
|
1083
|
+
// link holding the control key would read as view-only to whoever you
|
|
1084
|
+
// sent it to and hand them the controls, which is the whole reason for
|
|
1085
|
+
// naming the two shapes in the first place.
|
|
1086
|
+
//
|
|
1087
|
+
// A mismatch answers exactly as a wrong key does, so nothing is learned
|
|
1088
|
+
// from the difference between the two.
|
|
1089
|
+
if (scopeOf(offered, key, listenKey) !== inPath.wants) {
|
|
1081
1090
|
json(response, 404, { error: "not found" });
|
|
1082
1091
|
return;
|
|
1083
1092
|
}
|
|
@@ -1979,6 +1988,45 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
|
|
|
1979
1988
|
// is a worse answer than a 401 to someone who has neither.
|
|
1980
1989
|
if (options.paywall && (await options.paywall(request, response, path))) return;
|
|
1981
1990
|
|
|
1991
|
+
/**
|
|
1992
|
+
* What is live on this server, for somebody deciding what to watch.
|
|
1993
|
+
*
|
|
1994
|
+
* Two different things are, and they were only ever visible in two
|
|
1995
|
+
* different places: the server's own stream -- its playlist, which is what
|
|
1996
|
+
* it is listed in the directory as -- and any channels being published
|
|
1997
|
+
* into it from OBS or a phone. A person connected to a server had no way
|
|
1998
|
+
* to see either as a list, so "what is on here?" had no answer.
|
|
1999
|
+
*
|
|
2000
|
+
* Readable by anyone holding either link, because this is the viewing
|
|
2001
|
+
* side: it names streams and how to reach them, and says nothing about
|
|
2002
|
+
* how to change them.
|
|
2003
|
+
*/
|
|
2004
|
+
if (path === "/api/streams") {
|
|
2005
|
+
const now = engine.snapshot(false);
|
|
2006
|
+
const state = options.live?.status();
|
|
2007
|
+
json(response, 200, {
|
|
2008
|
+
server: {
|
|
2009
|
+
name: options.serverName ?? "this server",
|
|
2010
|
+
nowPlaying: engine.snapshot().tracks?.[now.index]?.title ?? "",
|
|
2011
|
+
tracks: now.trackCount,
|
|
2012
|
+
playing: now.playing,
|
|
2013
|
+
// Only when it has been published: a code is a thing you dial, and
|
|
2014
|
+
// one nobody can dial is not worth showing.
|
|
2015
|
+
live: state?.live === true,
|
|
2016
|
+
code: state?.code ?? "",
|
|
2017
|
+
url: state?.url ?? "",
|
|
2018
|
+
},
|
|
2019
|
+
channels: (options.channels?.list() ?? []).map((one) => ({
|
|
2020
|
+
id: one.id,
|
|
2021
|
+
name: one.name,
|
|
2022
|
+
via: one.via,
|
|
2023
|
+
listeners: one.listeners,
|
|
2024
|
+
startedAt: one.startedAt,
|
|
2025
|
+
})),
|
|
2026
|
+
});
|
|
2027
|
+
return;
|
|
2028
|
+
}
|
|
2029
|
+
|
|
1982
2030
|
// --- several streams at once ------------------------------------------
|
|
1983
2031
|
//
|
|
1984
2032
|
// A channel is one publisher and everybody listening to them. Two or three
|
|
@@ -3075,6 +3123,7 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
|
|
|
3075
3123
|
owner,
|
|
3076
3124
|
channels,
|
|
3077
3125
|
publishUrls: () => publishUrls,
|
|
3126
|
+
serverName: options.name || hostname(),
|
|
3078
3127
|
live: {
|
|
3079
3128
|
status: () => ({
|
|
3080
3129
|
live: publisher !== null,
|
package/src/share.ts
CHANGED
|
@@ -282,17 +282,28 @@ export function reachableAddresses(
|
|
|
282
282
|
*/
|
|
283
283
|
export const KEY_PATHS = ["/a/", "/v/"] as const;
|
|
284
284
|
|
|
285
|
-
/**
|
|
286
|
-
|
|
285
|
+
/**
|
|
286
|
+
* The key in a share link, and what the link claimed to be.
|
|
287
|
+
*
|
|
288
|
+
* Both halves, because a label nobody checks is a label that can lie. `/a/`
|
|
289
|
+
* means this administers and `/v/` means this only views; handed the other
|
|
290
|
+
* key, a path would otherwise have said one thing and done the other -- and
|
|
291
|
+
* the dangerous direction is real: a `/v/` link built around the control key
|
|
292
|
+
* reads as view-only to the person you send it to and hands them the controls.
|
|
293
|
+
*/
|
|
294
|
+
export function keyInPath(path: string): { key: string; wants: Scope } | null {
|
|
287
295
|
for (const prefix of KEY_PATHS) {
|
|
288
296
|
if (!path.startsWith(prefix)) continue;
|
|
289
297
|
const rest = path.slice(prefix.length).replace(/\/+$/, "");
|
|
290
298
|
if (rest === "" || rest.includes("/")) return null;
|
|
299
|
+
let key = rest;
|
|
291
300
|
try {
|
|
292
|
-
|
|
301
|
+
key = decodeURIComponent(rest);
|
|
293
302
|
} catch {
|
|
294
|
-
|
|
303
|
+
// A key that is not valid percent-encoding is taken as written; it will
|
|
304
|
+
// fail to match anything, which is the right answer either way.
|
|
295
305
|
}
|
|
306
|
+
return { key, wants: prefix === "/a/" ? "control" : "listen" };
|
|
296
307
|
}
|
|
297
308
|
return null;
|
|
298
309
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{t as e}from"./index-
|
|
1
|
+
import{t as e}from"./index-DH3yIa7s.js";var t=3;async function n(n){let{media:r,src:i,isTv:a}=n,{default:o}=await e(async()=>{let{default:e}=await import(`./hls-n74Cnh8A.js`);return{default:e}},[]);if(!o.isSupported())return n.onError(`This browser cannot play HLS streams.`),{destroy:()=>void 0,levels:()=>[]};let s=new o({...a?{maxBufferLength:60,maxMaxBufferLength:120,backBufferLength:30,liveSyncDurationCount:4}:{backBufferLength:90},enableWorker:!0}),c=0,l=!1;s.on(o.Events.ERROR,(e,r)=>{if(!l&&r.fatal){if(c>=t){n.onError(`This stream kept failing and has been stopped.`),s.destroy();return}switch(c+=1,r.type){case o.ErrorTypes.NETWORK_ERROR:n.onNotice(`Reconnecting…`),s.startLoad();break;case o.ErrorTypes.MEDIA_ERROR:n.onNotice(`Recovering…`),s.recoverMediaError();break;default:n.onError(`This stream could not be played.`),s.destroy()}}}),s.on(o.Events.MANIFEST_PARSED,()=>{l||(n.onNotice(null),n.onReady?.({live:s.levels.length>0&&!Number.isFinite(r.duration),levels:u()}))}),s.on(o.Events.LEVEL_LOADED,(e,t)=>{l||n.onReady?.({live:t.details.live,levels:u()})}),s.on(o.Events.FRAG_BUFFERED,()=>{l||n.onNotice(null)});function u(){return s.levels.map((e,t)=>({index:t,height:e.height||null,bitrate:e.bitrate||null,label:e.height?`${String(e.height)}p`:`${String(Math.round((e.bitrate||0)/1e3))}k`}))}return s.loadSource(i),s.attachMedia(r),{destroy(){l=!0,s.destroy()},levels:u,setLevel(e){s.currentLevel=e},currentLevel:()=>s.autoLevelEnabled?-1:s.currentLevel}}export{n as createHlsEngine};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();function e(e){if(!Number.isFinite(e)||e<0)return`--:--`;let t=Math.floor(e),n=Math.floor(t/60),r=t%60;return`${String(n).padStart(2,`0`)}:${String(r).padStart(2,`0`)}`}function t(e){return e.artist?`${e.artist} — ${e.title}`:e.title}function n(e){let t=e.split(`/`).pop()??e,n=t.lastIndexOf(`.`);return n>0?t.slice(0,n):t}var r=new Set([`mp4`,`webm`,`mkv`,`mov`,`m4v`,`ogv`,`avi`]);function i(e,t=``){if(t.startsWith(`video/`))return!0;if(t.startsWith(`audio/`))return!1;let n=e.lastIndexOf(`.`);return n>0&&r.has(e.slice(n+1).toLowerCase())}var a=`modulepreload`,o=function(e){return`/`+e},s={},c=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),c=i?.nonce||i?.getAttribute(`nonce`);function l(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function u(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,import.meta.url).href}r=l(t.map(t=>{if(t=o(t,n),t=u(t),t in s)return;s[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:a,r||(i.as=`script`),i.crossOrigin=``,i.href=t,c&&i.setAttribute(`nonce`,c),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},l=[[/\.m3u8$/i,`hls`],[/\.(ts|mts|m2ts|mpegts)$/i,`mpegts`],[/\.(mp3|m4a|aac|oga|ogg|opus|wav|flac)$/i,`audio`],[/\.(mp4|m4v|webm|mov|ogv)$/i,`mp4`]],u=[[/mpegurl/i,`hls`],[/mp2t|mpeg-?ts/i,`mpegts`],[/^audio\//i,`audio`],[/^video\//i,`mp4`]];function d(e){if(e.kind)return e.kind;if(e.mimeType){for(let[t,n]of u)if(t.test(e.mimeType))return n}let t=f(e.src);for(let[e,n]of l)if(e.test(t))return n;return`unknown`}function f(e){try{return new URL(e,`https://placeholder.invalid`).pathname}catch{return e.split(/[?#]/)[0]??e}}function p(e=globalThis,t=null){let n=e.MediaSource!==void 0,r=!1;try{let e=t??(typeof document>`u`?null:document.createElement(`video`));r=e?e.canPlayType(`application/vnd.apple.mpegurl`)!==``||e.canPlayType(`application/x-mpegURL`)!==``:!1}catch{r=!1}return{mediaSource:n,nativeHls:r}}function m(e,t){let n=d(e);switch(n){case`hls`:return t.mediaSource?{engine:`hls`,kind:n}:t.nativeHls?{engine:`native`,kind:n}:{engine:`native`,kind:n,unplayable:`This browser cannot play HLS streams.`};case`mpegts`:return t.mediaSource?{engine:`mpegts`,kind:n}:{engine:`mpegts`,kind:n,unplayable:`This browser cannot play transport streams.`};default:return{engine:`native`,kind:n}}}async function h(e,t){let n=t.capabilities??p(),r=m({src:t.src,...t.kind?{kind:t.kind}:{},...t.mimeType?{mimeType:t.mimeType}:{}},n),i=()=>void 0,a={media:e,src:t.src,isTv:t.isTv??!1,live:t.live??r.kind===`mpegts`,onError:t.onError??i,onNotice:t.onNotice??i,...t.onReady?{onReady:t.onReady}:{}};if(r.unplayable)return t.onError?.(r.unplayable),{destroy:i,engine:r.engine,kind:r.kind,levels:()=>[],unplayable:r.unplayable};let o,s=t.engines?.[r.engine];if(s)o=await s(a);else if(r.engine===`hls`){let{createHlsEngine:e}=await c(async()=>{let{createHlsEngine:e}=await import(`./hls-3VKVEQE3-DadaxhMr.js`);return{createHlsEngine:e}},[]);o=await e(a)}else if(r.engine===`mpegts`){let{createMpegtsEngine:e}=await c(async()=>{let{createMpegtsEngine:e}=await import(`./mpegts-LO6RVLD6-Cgabq96z.js`);return{createMpegtsEngine:e}},[]);o=await e(a,{withCredentials:t.withCredentials??!1,unplayableAdvice:t.unplayableAdvice??``})}else{let{createNativeEngine:e}=await c(async()=>{let{createNativeEngine:e}=await import(`./native-C7JTKWJH-BUyIoj0P.js`);return{createNativeEngine:e}},[]);o=await e(a)}return{destroy:()=>{o.destroy()},engine:r.engine,kind:r.kind,levels:o.levels,...o.setLevel?{setLevel:o.setLevel}:{},...o.currentLevel?{currentLevel:o.currentLevel}:{}}}var g=new Set([`mp3`,`flac`,`ogg`,`oga`,`opus`,`m4a`,`aac`,`wav`,`wma`,`aiff`,`aif`,`alac`,`mp4`,`webm`,`mkv`,`mov`,`m4v`,`ogv`]);function _(e,t=``){if(t.startsWith(`audio/`)||t.startsWith(`video/`))return!0;let n=e.lastIndexOf(`.`);return n>0&&g.has(e.slice(n+1).toLowerCase())}function v(e,t){return e.localeCompare(t,void 0,{numeric:!0,sensitivity:`base`})}function ee(e){return e.filter(e=>_(e.name,e.type)).sort((e,t)=>v(y(e),y(t))).map(e=>({title:n(e.name),artist:``,album:b(y(e)),duration:0,url:URL.createObjectURL(e),video:i(e.name,e.type),objectUrl:!0}))}function y(e){return e.webkitRelativePath||e.name}function b(e){let t=e.split(`/`);return t.length>1?t[t.length-2]:``}function te(e){for(let t of e)t.objectUrl&&URL.revokeObjectURL(t.url)}var x=2048;function S(e,t){return e||t===`hls`||t===`mpegts`}var ne=class{elements;handlers;attached=null;context=null;analyser=null;wired=new WeakSet;active;frequencies=new Uint8Array;constructor(e,t){this.elements=e,this.handlers=t,this.active=e.audio;for(let t of[e.audio,e.video])t.crossOrigin=`anonymous`,t.addEventListener(`timeupdate`,()=>{t===this.active&&this.handlers.onTime(t.currentTime,Number.isFinite(t.duration)?t.duration:0)}),t.addEventListener(`loadedmetadata`,()=>{t===this.active&&this.handlers.onTime(t.currentTime,Number.isFinite(t.duration)?t.duration:0)}),t.addEventListener(`ended`,()=>{t===this.active&&this.handlers.onEnded()}),t.addEventListener(`play`,()=>{t===this.active&&this.handlers.onState(!0)}),t.addEventListener(`pause`,()=>{t===this.active&&this.handlers.onState(!1)}),t.addEventListener(`error`,()=>{t===this.active&&this.handlers.onError(C(t))})}get playing(){return!this.active.paused&&!this.active.ended}get position(){return this.active.currentTime}get duration(){return Number.isFinite(this.active.duration)?this.active.duration:0}get showingVideo(){return this.active===this.elements.video}ensureGraph(e){let t=globalThis.AudioContext??globalThis.webkitAudioContext;if(t){if(this.context??=new t,this.analyser||(this.analyser=this.context.createAnalyser(),this.analyser.fftSize=x,this.analyser.smoothingTimeConstant=.6,this.analyser.connect(this.context.destination),this.frequencies=new Uint8Array(this.analyser.frequencyBinCount)),!this.wired.has(e))try{this.context.createMediaElementSource(e).connect(this.analyser),this.wired.add(e)}catch{this.wired.add(e)}this.context.resume()}}read(){return this.analyser&&this.analyser.getByteFrequencyData(this.frequencies),this.frequencies}levels(){if(!this.analyser)return[0,0];let e=this.read(),t=0;for(let n of e)t+=n;let n=e.length===0?0:t/e.length/255;return[Math.min(1,n*2.2),Math.min(1,n*2.2)]}async load(e,t){let n=e.objectUrl?e.video?`mp4`:`audio`:d({src:e.url}),r=S(e.video,n)?this.elements.video:this.elements.audio;r!==this.active&&(this.active.pause(),this.active.removeAttribute(`src`),this.active.load(),this.active=r),this.attached?.destroy(),this.attached=null;try{this.attached=await h(this.active,{src:e.url,kind:n,unplayableAdvice:`VLC or mpv will play it; nixamp can only hand it to your browser.`,onError:e=>this.handlers.onError(e),onNotice:e=>{e&&this.handlers.onError(e)}})}catch(e){this.handlers.onError(e instanceof Error?e.message:`that would not play`);return}t&&await this.play()}async play(){this.ensureGraph(this.active);try{await this.active.play()}catch(e){this.handlers.onError(e instanceof Error?e.message:`playback was refused`)}}pause(){this.active.pause()}stop(){this.active.pause(),this.active.currentTime=0,this.attached?.destroy(),this.attached=null}seek(e){Number.isFinite(e)&&(this.active.currentTime=Math.max(0,e))}set volume(e){this.elements.audio.volume=e,this.elements.video.volume=e}get volume(){return this.active.volume}};function C(e){switch(e.error?.code){case MediaError.MEDIA_ERR_ABORTED:return`playback was aborted`;case MediaError.MEDIA_ERR_NETWORK:return`the network dropped mid-track`;case MediaError.MEDIA_ERR_DECODE:return`this browser could not decode that`;case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED:return`this browser cannot play that format`;default:return`playback failed`}}function w(){return{revision:0,tracks:[],trackCount:0,index:0,playing:!1,position:0,bars:[],levels:[0,0],silent:!0,note:``,root:``}}function re(e,t){return{...t,tracks:t.tracks??e.tracks}}function T(e){let t=e.trim();if(t===``)return``;/^https?:\/\//i.test(t)||(t=`http://${t}`);let n;try{n=new URL(t)}catch{return``}let r=n.pathname.replace(/\/+$/,``);return r=r.replace(/\/api(\/.*)?$/,``),`${n.origin}${r}`}function E(e,t,n=``){let r=`${e===``?``:T(e)}${t.startsWith(`/`)?t:`/${t}`}`;return n?`${r}${r.includes(`?`)?`&`:`?`}k=${encodeURIComponent(n)}`:r}function ie(e){let t=e.trim();if(t===``)return{base:``,key:``};let n;try{n=new URL(/^https?:\/\//i.test(t)?t:`http://${t}`)}catch{return{base:``,key:``}}let r=/^\/[av]\/([^/]+)\/?$/.exec(n.pathname),i=r?.[1]??n.searchParams.get(`k`)??``;return r&&(n.pathname=`/`),n.searchParams.delete(`k`),{base:T(`${n.origin}${n.pathname}`),key:decodeURIComponent(i)}}function D(e,t,n=0,r=``){return E(e,n>0?`/api/media/${t}?kbps=${Math.round(n)}`:`/api/media/${t}`,r)}function O(e){if(typeof e!=`object`||!e)return null;let t=e,n=w(),r=(e,t)=>typeof e==`number`&&Number.isFinite(e)?e:t,i=Array.isArray(t.levels)?t.levels:[],a=Array.isArray(t.tracks)?t.tracks.map(e=>{let t=typeof e==`object`&&e?e:{};return{title:typeof t.title==`string`?t.title:`Untitled`,artist:typeof t.artist==`string`?t.artist:``,album:typeof t.album==`string`?t.album:``,duration:r(t.duration,0),...t.video===!0?{video:!0}:{},...typeof t.group==`string`&&t.group!==``?{group:t.group}:{}}}):void 0;return{revision:r(t.revision,0),...a?{tracks:a}:{},trackCount:r(t.trackCount,a?.length??0),index:r(t.index,0),playing:t.playing===!0,position:r(t.position,0),bars:Array.isArray(t.bars)?t.bars.map(e=>r(e,0)):[],levels:[r(i[0],0),r(i[1],0)],silent:t.silent===!0,note:typeof t.note==`string`?t.note:``,root:typeof t.root==`string`?t.root:n.root}}var ae=class{handlers;source=null;base=``;key=``;shape=`/a/`;lastRevision=-1;constructor(e){this.handlers=e}get address(){return this.base}url(e){return E(this.base,e,this.key)}get shareLink(){return this.base===``?``:this.key===``?this.base:`${this.base}${this.shape}${this.key}`}get connected(){return this.source!==null}connect(e){let{base:t,key:n}=ie(e);this.close(),this.base=t,this.key=n,this.shape=/\/v\/[^/]+\/?$/.test(e.trim())?`/v/`:`/a/`,this.lastRevision=-1,this.handlers.onStatus(`connecting`);let r=new EventSource(E(t,`/api/events`,n));this.source=r,r.onopen=()=>this.handlers.onStatus(`live`),r.onmessage=e=>{let t=O(k(e.data));t&&(t.revision<this.lastRevision||(this.lastRevision=t.revision,this.handlers.onStatus(`live`),this.handlers.onSnapshot(t)))},r.onerror=()=>{this.handlers.onStatus(`error`,`reconnecting…`)}}async send(e){if(this.base===``&&!this.connected)return;let t=await fetch(E(this.base,`/api/command`,this.key),{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify(e)});if(!t.ok){this.handlers.onStatus(`error`,`command refused (${t.status})`);return}let n=O(await t.json());n&&this.handlers.onSnapshot(n)}media(e,t=0){return D(this.base,e,t,this.key)}close(){this.source?.close(),this.source=null}};function k(e){try{return JSON.parse(e)}catch{return null}}async function oe(e,t,n=``){try{let r=await fetch(E(e,`/api/state`,n),{signal:t});return r.ok?O(await r.json()):null}catch{return null}}function se(e){if(!/^https:\/\//i.test(e))return``;let t;try{t=new URL(e).hostname.replace(/^\[|\]$/g,``)}catch{return``}return/^\d{1,3}(\.\d{1,3}){3}$/.test(t)||t.includes(`:`)?`That is an https address for a bare IP, and a certificate is issued for a name — a browser refuses it before it asks anything. Use the server's name instead (the address it printed first), or connect over http.`:``}async function ce(e,t=``,n){let r;try{r=await fetch(E(e,`/api/state`,t),{signal:n})}catch{return``}return r.ok?``:r.status===401?t===``?`That server needs its share link. Paste the whole link — the one with /a/ or /v/ in it — or sign in as its owner.`:`That share link is not accepted by that server. It may have been restarted, which gives it a new one.`:r.status===403?`That link can listen but not drive this server.`:r.status===429?`That server is asking us to slow down. Try again in a moment.`:``}async function A(e,t,n=``){try{let r=await fetch(E(e,`/api/health`,n),{signal:t});if(!r.ok)return null;let i=await r.json();return i.name===`nixamp`?i.version??`unknown`:null}catch{return null}}function le(e,t=globalThis.location?.protocol){return t!==`https:`||!/^http:\/\//i.test(e.trim())?``:`This page is https, and a browser refuses every request from an https page to an http one. Open that address directly, or give the server a certificate: nixamp serve --tls-cert cert.pem --tls-key key.pem.`}var j=.14,M=.02;function ue(e,t){let n=[];for(let r=0;r<=e;r++){let i=r/e,a=Math.round(1*(t/1)**i),o=n[n.length-1];n.push(o===void 0?a:Math.max(a,o+1))}return n}function de(e,t){let n=[];for(let r=0;r+1<t.length;r++){let i=Math.min(t[r],e.length),a=Math.min(Math.max(t[r+1],i+1),e.length),o=0,s=0;for(let t=i;t<a;t++)o+=e[t],s++;n.push(s===0?0:o/s/255)}return n}function fe(e,t,n=j){return t.map((t,r)=>{let i=e[r]??0;return t>=i?t:Math.max(t,i-n)})}function N(e,t,n=M){return t.map((t,r)=>Math.max(t,(e[r]??0)-n))}function pe(e,t,n,r,i){let{width:a,height:o}=t;if(a<=0||o<=0||n.length===0)return;e.clearRect(0,0,a,o);let s=a/n.length,c=Math.max(1,s*.72),l=Math.max(2,o*.012);e.fillStyle=i.bar,n.forEach((t,n)=>{let r=Math.max(1,t*(o-l*2));e.fillRect(n*s+(s-c)/2,o-r,c,r)}),e.fillStyle=i.peak,r.forEach((t,n)=>{let r=o-Math.max(1,t*(o-l*2))-l*2;e.fillRect(n*s+(s-c)/2,Math.max(0,r),c,l)})}var me=`nixamp.remote`,he=`nixamp.volume`,P=`nixamp.listenHere`;function F(e){let t=document.getElementById(e);if(!t)throw Error(`nixamp: #${e} is missing from the shell`);return t}function I(){let n={status:F(`status`),source:F(`source`),install:F(`install`),video:F(`video`),audio:F(`audio`),title:F(`title-line`),album:F(`album-line`),elapsed:F(`elapsed`),total:F(`total`),seek:F(`seek`),fullscreen:F(`fullscreen`),canvas:F(`spectrum`),glyphs:F(`glyphs`),levels:F(`levels`),playlist:F(`playlist`),playlistTitle:F(`playlist-panel`),note:F(`note`),files:F(`files`),folder:F(`folder`),remoteUrl:F(`remote-url`),remoteForm:F(`remote-form`),remoteState:F(`remote-state`),disconnect:F(`disconnect`),browse:F(`browse`),accountForm:F(`account-form`),accountEmail:F(`account-email`),accountPassword:F(`account-password`),accountSubmit:F(`account-submit`),accountToggle:F(`account-toggle`),accountProviders:F(`account-providers`),accountPanel:F(`account-panel`),accountElsewhere:F(`account-elsewhere`),accountSignOut:F(`account-signout`),accountNote:F(`account-note`),adminPanel:F(`admin-panel`),adminNote:F(`admin-note`),adminSaid:F(`admin-said`),adminConnections:F(`admin-connections`),publishPanel:F(`publish-panel`),publishNote:F(`publish-note`),publishList:F(`publish-list`),adminRestream:F(`admin-restream`),adminReplace:F(`admin-replace`),adminSource:F(`admin-source`),directory:F(`directory`),recentNote:F(`recent-note`),recentList:F(`recent-list`),followingNote:F(`following-note`),followingList:F(`following-list`),serversPanel:F(`servers-panel`),serversNote:F(`servers-note`),serversList:F(`servers-list`),notifyPanel:F(`notify-panel`),notifyNote:F(`notify-note`),notifyWeb:F(`notify-web`),notifyEmail:F(`notify-email`),notifySms:F(`notify-sms`),notifyPhone:F(`notify-phone`),notifyPhoneForm:F(`notify-phone-form`),notifyPhoneNote:F(`notify-phone-note`),directoryNote:F(`directory-note`),directoryList:F(`directory-list`),onairPanel:F(`onair-panel`),onairNote:F(`onair-note`),onairList:F(`onair-list`),sharePanel:F(`share-panel`),shareNote:F(`share-note`),shareLink:F(`share-link`),shareCopy:F(`share-copy`),sharePhone:F(`share-phone`),shareSend:F(`share-send`),liveControls:F(`live-controls`),goLive:F(`go-live`),stopLive:F(`stop-live`),shareTo:F(`share-to`),listenOnly:F(`listen-only`),listenHere:F(`listen-here`),volume:F(`volume`),prev:F(`prev`),playPause:F(`play-pause`),stop:F(`stop`),next:F(`next`)},r=`local`,i=[],a=0,o=w(),s=`idle`,c=``,l=`Pick files, or connect to a nixamp running somewhere else.`,u=!1,d=-1,f=``,p=Array(24).fill(0),m=Array(24).fill(0),h=[],g=()=>r===`remote`&&!n.listenHere.checked,_=new ne({audio:n.audio,video:n.video},{onTime:(e,t)=>{let n=i[a];r===`local`&&n&&t>0&&n.duration!==t&&(n.duration=t),L()},onEnded:()=>j(1),onState:()=>L(),onError:e=>{l=e,L()}}),v=new ae({onSnapshot:e=>{o=re(o,e),g()&&(p=e.bars.length>0?e.bars:p,m=N(m,p)),L()},onStatus:(e,t)=>{s=e,c=t??``,L()}}),y=()=>r===`remote`?o.tracks.length:i.length,b=()=>r===`remote`?g()||d<0?o.index:Math.min(d,Math.max(0,o.tracks.length-1)):a,x=()=>{let e=r===`remote`?o.tracks[b()]:i[b()];return e?t(e):`Nothing loaded.`},S=()=>(r===`remote`?o.tracks[b()]:i[b()])?.album||`—`,C=()=>g()?o.tracks[b()]?.duration??0:_.duration,T=()=>g()?o.position:_.position,E=()=>g()?o.playing:_.playing;async function D(e){if(r===`remote`){if(g()){await v.send({type:`play`,index:e});return}await O(e);return}let t=i[e];t&&(a=e,await _.load(t,!0),V(t.video),H(),L())}async function O(e){let t=o.tracks[e];t&&(d=e,await _.load({title:t.title,artist:t.artist,album:t.album,duration:t.duration,url:v.media(e,0),video:t.video===!0,objectUrl:!1},!0),V(t.video===!0),H())}async function k(){if(g()){await v.send({type:`toggle`});return}y()!==0&&(_.playing?_.pause():_.position>0?await _.play():await D(b()),L())}async function j(e){let t=y();if(t!==0){if(g()){await v.send({type:e>0?`next`:`prev`});return}await D((b()+e+t)%t)}}async function M(){if(g()){await v.send({type:`stop`});return}_.stop(),p=Array(24).fill(0),m=[...p],L()}let I=e=>`▁▂▃▄▅▆▇█`[Math.max(0,Math.min(7,Math.round(e*7)))];function L(){let t=y(),a=E();n.status.textContent=a?`▶ PLAYING`:`■ STOPPED`,n.status.dataset.playing=String(a),n.title.textContent=x(),n.album.textContent=S();let d=T(),f=C();n.elapsed.textContent=e(d),n.total.textContent=f>0?e(f):`--:--`,u||(n.seek.value=String(f>0?Math.round(d/f*1e3):0),n.seek.disabled=f<=0||g()),n.playPause.textContent=a?`❚❚`:`▶`,n.playPause.setAttribute(`aria-label`,a?`Pause`:`Play`),n.playlistTitle.dataset.title=`Playlist (${t})`,n.source.textContent=r===`remote`?`remote · ${v.address.replace(/^https?:\/\//,``)||`—`}`:i.length>0?`local · ${i.length} files`:`no source`,n.remoteState.textContent=r===`remote`?`${s}${c?` — ${c}`:``}`:`not connected`,n.remoteState.dataset.status=r===`remote`?s:`idle`,n.disconnect.hidden=r!==`remote`;let m=r===`remote`&&o.note!==``?o.note:l;n.note.textContent=m,n.note.hidden=m===``,ge(),n.glyphs.textContent=p.map(I).join(``);let[h,ee]=g()?o.levels:_.levels();n.levels.textContent=`L${`▮`.repeat(Math.round(h*6)).padEnd(6,`·`)} R${`▮`.repeat(Math.round(ee*6)).padEnd(6,`·`)}`}let R=``,z=-1;function ge(){let a=r===`remote`?o.tracks.map(e=>({name:t(e),seconds:e.duration,group:e.group??``})):i.map(e=>({name:t(e),seconds:e.duration,group:``})),s=`${r}:${a.map(e=>`${e.name}@${e.seconds}@${e.group}`).join(`|`)}`;if(s!==R){R=s;let t=[],r=``,i=a.some(e=>e.group!==``);a.forEach((n,a)=>{n.group!==r&&(i||n.group!==``)&&(r=n.group,t.push(_e(n.group)));let o=document.createElement(`li`);o.className=`row`,o.dataset.index=String(a);let s=document.createElement(`span`);s.className=`n`,s.textContent=String(a+1).padStart(2,` `);let c=document.createElement(`span`);c.className=`name`,c.textContent=n.name;let l=document.createElement(`span`);l.className=`time`,l.textContent=n.seconds>0?e(n.seconds):`--:--`,o.append(s,c,l),t.push(o)}),n.playlist.replaceChildren(...t)}let c=b(),l=E(),u;for(let e of Array.from(n.playlist.children)){let t=e,n=Number(t.dataset.index),r=Number.isInteger(n)&&n===c;t.classList.toggle(`selected`,r),t.classList.toggle(`playing`,r&&l),r&&(u=t)}c!==z&&(z=c,u?.scrollIntoView({block:`nearest`}))}function _e(e){let t=document.createElement(`li`);t.className=`group`;let r=document.createElement(`span`);if(r.className=`group-name`,r.textContent=e===``?`This server's library`:e,t.append(r),e!==``&&!n.adminPanel.hidden){let n=document.createElement(`button`);n.type=`button`,n.className=`group-remove`,n.textContent=`×`,n.title=`Remove ${e} from the playlist`,n.setAttribute(`aria-label`,`Remove ${e} from the playlist`),n.addEventListener(`click`,t=>{t.stopPropagation(),ve(e)}),t.append(n)}return t}async function ve(e){try{let t=await fetch(v.url(`/api/source/remove`),{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({group:e})}),n=await t.json();W(t.ok?`Removed ${n.removed??0} tracks from ${e}.`:n.error??`that did not work`)}catch{W(`could not reach the server`)}}function B(){let t=n.canvas,r=Math.min(2,globalThis.devicePixelRatio||1),i=Math.round(t.clientWidth*r),a=Math.round(t.clientHeight*r);i>0&&a>0&&(t.width!==i||t.height!==a)&&(t.width=i,t.height=a);let s=t.getContext(`2d`);if(g())m=N(m,p);else{let e=_.read();e.length>0&&(h.length!==25&&(h=ue(24,e.length)),p=fe(p,de(e,h)),m=N(m,p))}if(s){let e=getComputedStyle(document.documentElement);pe(s,{width:t.width,height:t.height},p,m,{bar:e.getPropertyValue(`--green`).trim()||`#4af689`,peak:e.getPropertyValue(`--green-dim`).trim()||`#227a4a`,background:`transparent`})}if(E()){n.glyphs.textContent=p.map(I).join(``);let[t,r]=g()?o.levels:_.levels();n.levels.textContent=`L${`▮`.repeat(Math.round(t*6)).padEnd(6,`·`)} R${`▮`.repeat(Math.round(r*6)).padEnd(6,`·`)}`,n.elapsed.textContent=e(T());let i=C();!u&&i>0&&(n.seek.value=String(Math.round(T()/i*1e3)))}requestAnimationFrame(B)}function V(e){n.video.hidden=!e,n.fullscreen.hidden=!e}function H(){`mediaSession`in navigator&&(navigator.mediaSession.metadata=new MediaMetadata({title:x(),album:S(),artist:`nixamp`,artwork:[{src:`/icons/icon-512.png`,sizes:`512x512`,type:`image/png`}]}),navigator.mediaSession.setActionHandler(`play`,()=>void k()),navigator.mediaSession.setActionHandler(`pause`,()=>void k()),navigator.mediaSession.setActionHandler(`nexttrack`,()=>void j(1)),navigator.mediaSession.setActionHandler(`previoustrack`,()=>void j(-1)))}n.playlist.addEventListener(`click`,e=>{let t=e.target.closest(`li`),n=Number(t?.dataset.index);Number.isInteger(n)&&D(n)}),n.fullscreen.addEventListener(`click`,()=>{let e=n.video;if(document.fullscreenElement){document.exitFullscreen().catch(()=>{});return}if(typeof e.requestFullscreen==`function`){e.requestFullscreen().catch(()=>{e.webkitEnterFullscreen?.()});return}e.webkitEnterFullscreen?.()}),n.prev.addEventListener(`click`,()=>void j(-1)),n.next.addEventListener(`click`,()=>void j(1)),n.stop.addEventListener(`click`,()=>void M()),n.playPause.addEventListener(`click`,()=>void k()),n.seek.addEventListener(`input`,()=>{u=!0}),n.seek.addEventListener(`change`,()=>{let e=C();e>0&&_.seek(Number(n.seek.value)/1e3*e),u=!1}),n.volume.addEventListener(`input`,()=>{let e=Number(n.volume.value)/100;_.volume=e;try{localStorage.setItem(he,String(e))}catch{}});let ye=e=>{e.addEventListener(`change`,()=>{let t=ee(Array.from(e.files??[]));if(t.length===0){l=`Nothing playable in that selection.`,L();return}te(i),i=t,a=0,r=`local`,v.close(),l=``,D(0)})};ye(n.files),ye(n.folder),n.remoteForm.addEventListener(`submit`,e=>{e.preventDefault();let t=n.remoteUrl.value,{base:i,key:a}=ie(t);if(i===``){l=`That is not an address.`,L();return}(async()=>{s=`connecting`,L();let e=le(i);if(e){s=`error`,c=e,l=e,r=`local`,L();return}if(await A(i,void 0,a)===null){s=`error`;let e=se(i);c=e?`needs the server's name`:`not answering`,l=e||`Nothing answered at ${i}. If that is your machine, it is off or nixamp is not running on it; otherwise check the address.`,r=`local`,L();return}let n=await ce(i,a);if(n){s=`error`,c=n,l=n,r=`local`,L();return}r=`remote`,l=``;try{localStorage.setItem(me,t.trim())}catch{}v.connect(t),Q(),He(!0),G(),X(),L()})()});let be=async()=>{n.directory.hidden=!1,n.directoryNote.textContent=`Looking for live streams…`,n.directoryList.replaceChildren();let e;try{let t=await fetch(`/api/directory`);if(!t.ok)throw Error(String(t.status));let n=await t.json();e=n.streams??[],ke(n.recent??[])}catch{n.directoryNote.textContent=`The directory is not answering. Type an address instead.`;return}if(e.length===0){n.directoryNote.textContent=`Nobody is streaming right now.`;return}n.directoryNote.textContent=`${e.length} live ${e.length===1?`stream`:`streams`}:`;for(let t of e){let e=document.createElement(`li`),r=document.createElement(`button`);r.type=`button`;let i=document.createElement(`span`);i.className=`name`,i.textContent=t.name;let a=document.createElement(`span`);a.className=`detail`;let o=[t.nowPlaying,`${t.tracks} tracks`].filter(Boolean);t.code&&o.push(t.callers?`☎ ${t.code} · ${t.callers} on the phone`:`☎ ${t.code}`),a.textContent=o.join(` · `),r.append(i,a),r.addEventListener(`click`,()=>{n.remoteUrl.value=t.url,n.directory.hidden=!0,n.remoteForm.requestSubmit()}),e.append(r),t.ownerId&&J&&t.ownerId!==J&&e.append(Me(t.ownerId,t.name)),n.directoryList.append(e)}};if(location.pathname.replace(/\/+$/,``)===`/directory`){document.body.classList.add(`route-directory`);let e=document.getElementById(`directory-back`);e&&(e.hidden=!1),be()}let U=null,xe=e=>e===`cgnat`?`mobile or tailscale`:e===`private`?`your network`:e===`local`?`this machine`:e===`public`?`the internet`:e,Se=e=>e===`events`?`watching the panel`:e===`page`?`opened the page`:e===`media`?`playing a track`:e===`stream`?`listening live`:e,Ce=``,we=``,Te=e=>{let t=e.map(e=>`${e.address}|${e.kind}|${e.track}|${Math.round(e.bytes/4096)}|${e.endedAt}`).join(`~`);if(t===Ce)return;Ce=t,n.adminConnections.replaceChildren();let r=document.createElement(`tr`);for(let e of[`Where`,`Network`,`Kind`,`Client`,`Track`,`Sent`]){let t=document.createElement(`th`);t.textContent=e,r.append(t)}n.adminConnections.append(r);for(let t of e.slice(0,40)){let e=document.createElement(`tr`);t.endedAt!==null&&(e.className=`ended`);let r=[[t.address,``],[xe(t.network),`network-${t.network}`],[Se(t.kind),``],[t.agent,``],[t.track||`—`,``],[`${Math.round(t.bytes/1024)} KiB`,``]];for(let[t,n]of r){let r=document.createElement(`td`);r.textContent=t,n&&(r.className=n),e.append(r)}n.adminConnections.append(e)}};function W(e){n.adminSaid.textContent=e,n.adminSaid.hidden=e===``}let Ee=async()=>{try{let e=await fetch(v.url(`/api/connections`));if(!e.ok)return;let t=await e.json(),r=(t.connections??[]).filter(e=>e.endedAt===null&&e.kind!==`media`&&e.kind!==`stream`).length,i=t.active??0;n.adminNote.textContent=r===0?`${i} listening now.`:`${i} listening now, and ${r} with the page open.`,Te(t.connections??[]),De(t.publish??[],(t.channels??[]).map(e=>e.id)),Q()}catch{n.adminNote.textContent=`lost touch with the server`}};function De(e,t){n.publishPanel.hidden=e.length===0;let r=`${e.map(e=>`${e.id}=${e.url}`).join(`~`)}::${t.join(`,`)}`;if(r===we)return;if(we=r,e.length===0){n.publishList.replaceChildren();return}let i=e.length-t.length;n.publishNote.textContent=`Point OBS, Larix or ffmpeg at one of these. One publisher per URL — ${e.length} at once, ${i} free right now.`,n.publishList.replaceChildren(...e.map(e=>{let n=t.includes(e.id),r=document.createElement(`li`);n&&(r.className=`in-use`);let i=document.createElement(`span`);i.className=`slot`,i.textContent=n?`${e.id} · live`:e.id;let a=document.createElement(`input`);a.type=`text`,a.readOnly=!0,a.value=e.url,a.setAttribute(`aria-label`,`RTMP URL for ${e.id}`);let o=document.createElement(`button`);return o.type=`button`,o.className=`ghost`,o.textContent=`Copy`,o.addEventListener(`click`,()=>{a.select(),navigator.clipboard?.writeText(e.url).catch(()=>{})}),r.append(i,a,o),r}))}let G=async()=>{if(r!==`remote`){n.adminPanel.hidden=!0,n.publishPanel.hidden=!0,U&&clearInterval(U),U=null;return}let e=!1,t=null,i=!1;try{let n=await fetch(v.url(`/api/admin`));if(n.ok){let r=await n.json();e=r.allowed===!0,t=r.as??null,i=r.claimed===!0}}catch{e=!1}if(n.adminPanel.hidden=!e,U&&clearInterval(U),U=null,X(),n.listenOnly.hidden=e,!e){n.listenOnly.textContent=i?`This is a listen-only link: you can hear this server but not change what it plays. Use its control link — the first one it printed — or sign in as its owner.`:`This is a listen-only link: you can hear this server but not change what it plays. Use its control link, the first one it printed.`;return}n.adminNote.textContent=t===`owner`?`You own this server.`:`You hold this server's control link.`,Ee(),U=setInterval(()=>void Ee(),2e3)};n.adminRestream.addEventListener(`submit`,e=>{e.preventDefault();let t=n.adminSource.value.trim();if(!t)return;W(`Reading ${t}…`);let r=n.adminReplace.checked;(async()=>{try{let e=await fetch(v.url(`/api/source`),{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({source:t,...r?{replace:!0}:{}})}),i=await e.json();W(e.ok?r?`Now serving ${t}.`:i.added===0?`Everything there was already in the playlist.`:`Added ${i.added??0} tracks from ${t}.`:i.error??`that did not work`),e.ok&&(n.adminSource.value=``,X(),Q())}catch{W(`could not reach the server`)}})()});let Oe=e=>{let t=Math.max(1,Math.round((Date.now()-e)/6e4));if(t<60)return`${t} minute${t===1?``:`s`} ago`;let n=Math.round(t/60);return`${n} hour${n===1?``:`s`} ago`},ke=e=>{n.recentList.replaceChildren();let t=J?e.filter(e=>e.ownerId&&e.ownerId!==J):[];if(n.recentNote.hidden=t.length===0,t.length!==0)for(let e of t){let t=document.createElement(`li`),r=document.createElement(`span`);r.className=`recent-label`;let i=document.createElement(`span`);i.className=`name`,i.textContent=e.name;let a=document.createElement(`span`);a.className=`detail`,a.textContent=e.nowPlaying?`${e.nowPlaying} · ended ${Oe(e.endedAt)}`:`ended ${Oe(e.endedAt)}`,r.append(i,a),t.append(r,Me(e.ownerId,e.name)),n.recentList.append(t)}},Ae=async()=>{n.serversList.replaceChildren();try{let e=await fetch(`/api/v1/servers`);if(!e.ok){n.serversPanel.hidden=!0;return}let t=(await e.json()).servers??[];n.serversPanel.hidden=!1,n.serversNote.textContent=t.length===0?"No servers yet. `nixamp server add --here` remembers the one you are running.":`The machines on your account. Open one, or forget it.`;for(let e of t){let t=document.createElement(`li`),r=document.createElement(`span`);r.className=`recent-label`;let i=document.createElement(`span`);i.className=`name`,i.textContent=e.name;let a=document.createElement(`span`);a.className=`detail`,a.textContent=e.url,r.append(i,a);let o=document.createElement(`button`);o.type=`button`,o.className=`button`,o.textContent=`Open`,o.addEventListener(`click`,()=>{n.remoteUrl.value=e.key?`${e.url}/a/${e.key}`:e.url,n.remoteForm.requestSubmit()}),A(e.url).then(n=>{if(n!==null){a.textContent=`${e.url} · ${n}`;return}a.textContent=`${e.url} · not answering`,t.classList.add(`offline`),o.disabled=!0,o.title=`That machine is not answering. Start nixamp on it.`});let s=document.createElement(`button`);s.type=`button`,s.className=`ghost`,s.textContent=`Forget`,s.addEventListener(`click`,()=>{(async()=>{s.disabled=!0;try{await fetch(`/api/v1/servers/${encodeURIComponent(e.id)}`,{method:`DELETE`}),await Ae()}catch{s.disabled=!1}})()}),t.append(r,o,s),n.serversList.append(t)}}catch{n.serversPanel.hidden=!0}},je=async()=>{n.followingList.replaceChildren();try{let e=await fetch(`/api/v1/follows`);if(!e.ok){n.followingNote.hidden=!0;return}let t=(await e.json()).following??[];n.followingNote.hidden=t.length===0;for(let e of t){let t=document.createElement(`li`),r=document.createElement(`span`);r.className=`recent-label`;let i=document.createElement(`span`);i.className=`name`,i.textContent=e.name||`a nixamp`;let a=document.createElement(`span`);a.className=`detail`,a.textContent=e.live?`live now`:`not streaming`,r.append(i,a);let o=document.createElement(`button`);o.type=`button`,o.className=`ghost follow`,o.textContent=`Unfollow`,o.addEventListener(`click`,()=>{(async()=>{o.disabled=!0;try{await fetch(`/api/v1/follows/${encodeURIComponent(e.id)}`,{method:`DELETE`}),t.remove(),n.followingList.children.length===0&&(n.followingNote.hidden=!0)}finally{o.disabled=!1}})()}),t.append(r,o),n.followingList.append(t)}}catch{n.followingNote.hidden=!0}},Me=(e,t)=>{let n=document.createElement(`button`);n.type=`button`,n.className=`ghost follow`,n.textContent=`Follow`,n.setAttribute(`aria-label`,`Follow ${t}`);let r=e=>{n.textContent=e?`Following`:`Follow`,n.dataset.following=e?`yes`:`no`};return(async()=>{try{let t=await fetch(`/api/v1/follows/${encodeURIComponent(e)}`);t.ok&&r((await t.json()).following===!0)}catch{}})(),n.addEventListener(`click`,()=>{(async()=>{let t=n.dataset.following===`yes`;n.disabled=!0;try{(await fetch(`/api/v1/follows/${encodeURIComponent(e)}`,{method:t?`DELETE`:`PUT`,headers:{"content-type":`application/json`},body:t?void 0:`{}`})).ok&&(r(!t),je())}catch{}finally{n.disabled=!1}})()}),n},Ne=e=>{let t=(e+`=`.repeat((4-e.length%4)%4)).replace(/-/g,`+`).replace(/_/g,`/`),n=atob(t),r=new Uint8Array(new ArrayBuffer(n.length));for(let e=0;e<n.length;e+=1)r[e]=n.charCodeAt(e);return r},Pe=()=>`serviceWorker`in navigator&&`PushManager`in window&&`Notification`in window,Fe=async()=>{if(!Pe())return n.notifyNote.textContent=`This browser cannot show notifications.`,!1;if(Notification.permission===`denied`)return n.notifyNote.textContent=`This browser is blocking notifications. Allow them in site settings first.`,!1;if(await Notification.requestPermission()!==`granted`)return n.notifyNote.textContent=`Not allowed, so nothing will be sent here.`,!1;try{let e=await navigator.serviceWorker.ready,{publicKey:t}=await(await fetch(`/api/v1/notify/key`)).json();if(!t)return n.notifyNote.textContent=`This server is not set up to send notifications.`,!1;let r=await e.pushManager.getSubscription()??await e.pushManager.subscribe({userVisibleOnly:!0,applicationServerKey:Ne(t)}),i=await fetch(`/api/v1/notify/subscribe`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify(r.toJSON())});if(!i.ok)throw Error(String(i.status));return n.notifyNote.textContent=`This device will be told.`,!0}catch{return n.notifyNote.textContent=`Could not set this device up.`,!1}},Ie=async()=>{try{let e=await(await navigator.serviceWorker.ready).pushManager.getSubscription();if(!e)return;await fetch(`/api/v1/notify/subscribe?endpoint=${encodeURIComponent(e.endpoint)}`,{method:`DELETE`}),await e.unsubscribe()}catch{}},K=async e=>{try{let t=await fetch(`/api/v1/notify/prefs`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify(e)}),r=await t.json();n.notifyPhoneNote.textContent=t.ok?``:r.error??`that did not save`,t.ok&&typeof r.phone==`string`&&(n.notifyPhone.value=r.phone)}catch{n.notifyPhoneNote.textContent=`could not reach nixamp.com`}},Le=async()=>{try{let e=await fetch(`/api/v1/notify/prefs`);if(!e.ok)return;let t=await e.json();n.notifyEmail.checked=t.wantsEmail!==!1,n.notifySms.checked=t.wantsSms===!0,n.notifyPhone.value=t.phone??``;let r=Pe()&&Notification.permission===`granted`?await(await navigator.serviceWorker.ready).pushManager.getSubscription()!==null:!1;n.notifyWeb.checked=t.wantsWeb!==!1&&r,n.notifyNote.textContent=r?`Get told when someone you follow goes live.`:`Turn on “On this device” to be told here.`}catch{}};n.notifyWeb.addEventListener(`change`,()=>{(async()=>{if(n.notifyWeb.checked){let e=await Fe();n.notifyWeb.checked=e,await K({wantsWeb:e});return}await Ie(),await K({wantsWeb:!1}),n.notifyNote.textContent=`Turn on “On this device” to be told here.`})()}),n.notifyEmail.addEventListener(`change`,()=>{K({wantsEmail:n.notifyEmail.checked})}),n.notifySms.addEventListener(`change`,()=>{(async()=>{if(n.notifySms.checked&&!n.notifyPhone.value.trim()){n.notifyPhoneNote.textContent=`Add a phone number first.`,n.notifySms.checked=!1,n.notifyPhone.focus();return}await K({wantsSms:n.notifySms.checked})})()}),n.notifyPhoneForm.addEventListener(`submit`,e=>{e.preventDefault(),K({phone:n.notifyPhone.value.trim()})});let q=!1,J=``,Y=e=>{let t=e!==null;n.notifyPanel.hidden=!t,t?(Le(),je(),Ae()):(n.serversPanel.hidden=!0,n.followingNote.hidden=!0,n.followingList.replaceChildren(),n.recentNote.hidden=!0,n.recentList.replaceChildren()),n.accountForm.hidden=t,n.accountProviders.hidden=t||n.accountProviders.childElementCount===0,n.accountSignOut.hidden=!t,n.accountNote.textContent=t?`Signed in as ${e}.`:q?`Create an account on nixamp.com.`:`Sign in to nixamp.com to publish and get paid.`,n.accountSubmit.textContent=q?`Create account`:`Sign in`,n.accountToggle.textContent=q?`I have one`:`Create one`,n.accountPassword.autocomplete=q?`new-password`:`current-password`},Re=async()=>{let e=[],t=!1;try{let n=await fetch(`/api/v1/auth/providers`);n.ok&&(t=!0,e=(await n.json()).providers??[])}catch{}n.accountProviders.replaceChildren(),n.accountProviders.hidden=e.length===0,n.accountPanel.hidden=!t,n.accountElsewhere.hidden=t;for(let t of e){let e=document.createElement(`a`);e.className=`button`,e.href=`/api/v1/${encodeURIComponent(t.id)}/oauth/start`,e.textContent=`Continue with ${t.name}`,n.accountProviders.append(e)}},ze=async()=>{try{let e=await fetch(`/api/v1/auth/me`),t=await e.json();J=e.ok?t.account?.id??``:``,Y(e.ok?t.account?.email??`you`:null)}catch{J=``,Y(null)}Be()};function Be(){if(f===``)return;if(J===``){n.accountNote.textContent=`Sign in to watch the stream you were sent.`,n.accountPanel.scrollIntoView({behavior:`smooth`,block:`center`});return}let e=f;f=``,n.remoteUrl.value=e,n.remoteForm.requestSubmit()}n.accountToggle.addEventListener(`click`,()=>{q=!q,Y(null)}),n.accountForm.addEventListener(`submit`,e=>{e.preventDefault();let t=n.accountEmail.value.trim(),r=n.accountPassword.value;(async()=>{n.accountSubmit.disabled=!0;try{let e=await fetch(`/api/v1/auth/${q?`signup`:`login`}`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({email:t,password:r})}),i=await e.json();if(!e.ok){n.accountNote.textContent=i.error??`that did not work`;return}J=i.account?.id??``,n.accountPassword.value=``,Y(i.account?.email??t),G(),Be()}catch{n.accountNote.textContent=`could not reach nixamp.com`}finally{n.accountSubmit.disabled=!1}})()}),n.accountSignOut.addEventListener(`click`,()=>{(async()=>{try{await fetch(`/api/v1/auth/logout`,{method:`POST`})}catch{}J=``,Y(null),G()})()});try{let e=new URL(globalThis.location.href).searchParams.get(`url`)??``;e!==``&&(f=e,n.remoteUrl.value=e,l=`Sign in to watch this stream.`,globalThis.history?.replaceState(null,``,globalThis.location.pathname))}catch{}Re(),ze(),G(),n.browse.addEventListener(`click`,()=>{if(!n.directory.hidden){n.directory.hidden=!0;return}be(),n.directory.scrollIntoView({behavior:`smooth`,block:`nearest`})}),n.disconnect.addEventListener(`click`,()=>{v.close(),n.listenOnly.hidden=!0,d=-1,n.sharePanel.hidden=!0,n.publishPanel.hidden=!0,n.adminPanel.hidden=!0,n.onairPanel.hidden=!0,He(!1),r=`local`,s=`idle`,c=``,L()});async function X(){if(r!==`remote`||v.shareLink===``){n.sharePanel.hidden=!0;return}n.sharePanel.hidden=!1;let e=v.shareLink,t=globalThis.location.origin;n.shareLink.value=e.startsWith(`https://`)?`${t}/?url=${encodeURIComponent(e)}`:e,n.shareNote.textContent=`Anyone with this link can watch. They sign in once, then it opens.`,n.sharePhone.hidden=!0,n.shareSend.hidden=!0,n.liveControls.hidden=!0;let i=``;try{let e=await fetch(`/api/directory`);e.ok&&(i=(await e.json()).callIn??``)}catch{}let a=null;try{let r=await fetch(v.url(`/api/live/state`));r.ok&&(a=await r.json()),a?.url&&(e=a.url,n.shareLink.value=e.startsWith(`https://`)?`${t}/?url=${encodeURIComponent(e)}`:e)}catch{}if(a){if(n.liveControls.hidden=n.adminPanel.hidden||!a.possible,n.goLive.hidden=a.live,n.stopLive.hidden=!a.live,n.sharePhone.hidden=!1,!a.live){n.sharePhone.textContent=a.possible?`Not listed, so nobody can find this in the directory. Go live to list it, with a phone number and a code anyone can call.`:`This machine has no address the world can reach, so it cannot be listed.`;return}if(!i){n.sharePhone.textContent=`Listed. The code for the phone line is ${a.code}.`,n.shareSend.hidden=!1;return}n.sharePhone.replaceChildren(document.createTextNode(`To talk about it, call `),Ge(i),document.createTextNode(` and key `),Ge(a.code),document.createTextNode(`. That is a room with everyone else watching — not the stream itself.`)),n.shareSend.hidden=!1}}let Ve=``,Z=null,He=e=>{Z&&clearInterval(Z),Z=null,e&&(Z=setInterval(()=>void Q(),6e3))};async function Q(){if(r!==`remote`){n.onairPanel.hidden=!0;return}let e;try{let t=await fetch(v.url(`/api/streams`));if(!t.ok){n.onairPanel.hidden=!0;return}e=await t.json()}catch{n.onairPanel.hidden=!0;return}n.onairPanel.hidden=!1;let t=JSON.stringify(e);if(t===Ve)return;Ve=t,n.onairNote.textContent=e.channels.length===0?`One stream, from this server's own playlist.`:`${e.channels.length+1} streams: this server's playlist, and ${e.channels.length} publishing into it.`;let i=[];i.push(Ue({title:e.server.name,detail:[e.server.nowPlaying||`${e.server.tracks} tracks`,e.server.playing?`playing`:`stopped`,e.server.live&&e.server.code?`☎ ${e.server.code}`:`not listed`].join(` · `),onPlay:()=>{D(b())},link:e.server.live?e.server.url:``}));for(let t of e.channels)i.push(Ue({title:t.name,detail:`live over ${t.via} · ${t.listeners} listening`,onPlay:()=>{_.load({title:t.name,artist:``,album:``,duration:0,url:v.url(`/api/channels/${encodeURIComponent(t.id)}`),video:!0,objectUrl:!1},!0),V(!0)},link:``}));n.onairList.replaceChildren(...i)}function Ue(e){let t=document.createElement(`li`),n=document.createElement(`span`);n.className=`recent-label`;let r=document.createElement(`span`);r.className=`name`,r.textContent=e.title;let i=document.createElement(`span`);i.className=`detail`,i.textContent=e.detail,n.append(r,i);let a=document.createElement(`button`);if(a.type=`button`,a.className=`button`,a.textContent=`Play`,a.addEventListener(`click`,e.onPlay),t.append(n,a),e.link){let n=document.createElement(`button`);n.type=`button`,n.className=`ghost`,n.textContent=`Copy link`,n.addEventListener(`click`,()=>{let t=globalThis.location.origin,n=e.link.startsWith(`https://`)?`${t}/?url=${encodeURIComponent(e.link)}`:e.link;navigator.clipboard?.writeText(n).catch(()=>{})}),t.append(n)}return t}let We=async e=>{n.goLive.disabled=!0,n.stopLive.disabled=!0,n.shareNote.textContent=e?`Going live…`:`Taking it off the list…`;try{let t=await fetch(v.url(e?`/api/live/start`:`/api/live/stop`),{method:`POST`}),r=await t.json();n.shareNote.textContent=t.ok?e?`Live. Anyone can call and key ${r.code??``} to talk about it.`:`Taken off the list. The link still works for anybody who has it.`:r.error??`that did not work`}catch{n.shareNote.textContent=`could not reach the server`}finally{n.goLive.disabled=!1,n.stopLive.disabled=!1,await X()}};n.goLive.addEventListener(`click`,()=>void We(!0)),n.stopLive.addEventListener(`click`,()=>void We(!1));function Ge(e){let t=document.createElement(`b`);return t.textContent=e,t}n.shareCopy.addEventListener(`click`,()=>{n.shareLink.select(),navigator.clipboard?.writeText(n.shareLink.value).then(()=>{n.shareNote.textContent=`Copied. Send it to anybody.`},()=>{n.shareNote.textContent=`Copy it from the box above.`})}),n.shareSend.addEventListener(`submit`,e=>{e.preventDefault();let t=n.shareTo.value.trim();t!==``&&(async()=>{n.shareNote.textContent=`Sending…`;try{let e=await fetch(`/api/v1/invite`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({to:t,stream:v.shareLink})}),r=await e.json();n.shareNote.textContent=e.ok?`Sent to ${r.sent??t}.`:r.error??`that did not send`,e.ok&&(n.shareTo.value=``)}catch{n.shareNote.textContent=`could not send that`}})()}),n.listenHere.addEventListener(`change`,()=>{try{localStorage.setItem(P,n.listenHere.checked?`1`:`0`)}catch{}r===`remote`&&(async()=>{n.listenHere.checked?(await v.send({type:`stop`}),await O(o.index)):(_.stop(),d=-1),L()})()}),document.addEventListener(`keydown`,e=>{let t=e.target;if(!(t&&/^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName)))switch(e.key){case` `:e.preventDefault(),k();return;case`s`:M();return;case`n`:case`ArrowRight`:j(1);return;case`p`:case`ArrowLeft`:j(-1);return;case`ArrowDown`:e.preventDefault(),D(Math.min(y()-1,b()+1));return;case`ArrowUp`:e.preventDefault(),D(Math.max(0,b()-1));return}});let $=null;globalThis.addEventListener(`beforeinstallprompt`,e=>{e.preventDefault(),$=e,n.install.hidden=!1}),n.install.addEventListener(`click`,()=>{$?.prompt(),$=null,n.install.hidden=!0});try{let e=localStorage.getItem(he);e!==null&&(n.volume.value=String(Math.round(Number(e)*100)),_.volume=Number(e));let t=localStorage.getItem(me);t&&(n.remoteUrl.value=t),localStorage.getItem(P)===`0`&&(n.listenHere.checked=!1)}catch{}(async()=>{if(n.remoteUrl.value!==``)return;let e=globalThis.location.origin;if(await A(e)===null)return;let t=await oe(e);t&&t.trackCount!==0&&(n.remoteUrl.value=e,r=`remote`,l=``,v.connect(e),L())})(),L(),requestAnimationFrame(B)}I(),`serviceWorker`in navigator&&globalThis.addEventListener(`load`,()=>{navigator.serviceWorker.register(`/sw.js`).catch(()=>{})});export{c as t};
|